@aiready/context-analyzer 0.9.41 → 0.9.42
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/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-test.log +19 -19
- package/dist/chunk-4SYIJ7CU.mjs +1538 -0
- package/dist/chunk-4XQVYYPC.mjs +1470 -0
- package/dist/chunk-5CLU3HYU.mjs +1475 -0
- package/dist/chunk-5K73Q3OQ.mjs +1520 -0
- package/dist/chunk-6AVS4KTM.mjs +1536 -0
- package/dist/chunk-6I4552YB.mjs +1467 -0
- package/dist/chunk-6LPITDKG.mjs +1539 -0
- package/dist/chunk-AECWO7NQ.mjs +1539 -0
- package/dist/chunk-AJC3FR6G.mjs +1509 -0
- package/dist/chunk-CVGIDSMN.mjs +1522 -0
- package/dist/chunk-DXG5NIYL.mjs +1527 -0
- package/dist/chunk-G3CCJCBI.mjs +1521 -0
- package/dist/chunk-GFADGYXZ.mjs +1752 -0
- package/dist/chunk-GTRIBVS6.mjs +1467 -0
- package/dist/chunk-H4HWBQU6.mjs +1530 -0
- package/dist/chunk-JH535NPP.mjs +1619 -0
- package/dist/chunk-KGFWKSGJ.mjs +1442 -0
- package/dist/chunk-N2GQWNFG.mjs +1527 -0
- package/dist/chunk-NQA3F2HJ.mjs +1532 -0
- package/dist/chunk-NXXQ2U73.mjs +1467 -0
- package/dist/chunk-QDGPR3L6.mjs +1518 -0
- package/dist/chunk-SAVOSPM3.mjs +1522 -0
- package/dist/chunk-SIX4KMF2.mjs +1468 -0
- package/dist/chunk-SPAM2YJE.mjs +1537 -0
- package/dist/chunk-UG7OPVHB.mjs +1521 -0
- package/dist/chunk-VIJTZPBI.mjs +1470 -0
- package/dist/chunk-W37E7MW5.mjs +1403 -0
- package/dist/chunk-W76FEISE.mjs +1538 -0
- package/dist/chunk-WCFQYXQA.mjs +1532 -0
- package/dist/chunk-XY77XABG.mjs +1545 -0
- package/dist/chunk-YCGDIGOG.mjs +1467 -0
- package/dist/cli.js +768 -1160
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +196 -64
- package/dist/index.d.ts +196 -64
- package/dist/index.js +937 -1209
- package/dist/index.mjs +65 -3
- package/package.json +2 -2
- package/src/analyzer.ts +143 -2177
- package/src/ast-utils.ts +94 -0
- package/src/classifier.ts +497 -0
- package/src/cluster-detector.ts +100 -0
- package/src/defaults.ts +59 -0
- package/src/graph-builder.ts +272 -0
- package/src/index.ts +30 -519
- package/src/metrics.ts +231 -0
- package/src/remediation.ts +139 -0
- package/src/scoring.ts +12 -34
- package/src/semantic-analysis.ts +192 -126
- package/src/summary.ts +168 -0
package/src/index.ts
CHANGED
|
@@ -6,126 +6,28 @@ import {
|
|
|
6
6
|
calculateContextBudget,
|
|
7
7
|
detectCircularDependencies,
|
|
8
8
|
calculateCohesion,
|
|
9
|
-
calculateFragmentation,
|
|
10
9
|
detectModuleClusters,
|
|
11
|
-
calculatePathEntropy,
|
|
12
|
-
calculateDirectoryDistance,
|
|
13
10
|
classifyFile,
|
|
14
11
|
adjustCohesionForClassification,
|
|
15
12
|
adjustFragmentationForClassification,
|
|
16
13
|
getClassificationRecommendations,
|
|
14
|
+
analyzeIssues,
|
|
17
15
|
} from './analyzer';
|
|
18
16
|
import { calculateContextScore } from './scoring';
|
|
17
|
+
import { getSmartDefaults } from './defaults';
|
|
18
|
+
import { generateSummary } from './summary';
|
|
19
19
|
import type {
|
|
20
20
|
ContextAnalyzerOptions,
|
|
21
21
|
ContextAnalysisResult,
|
|
22
22
|
ContextSummary,
|
|
23
|
-
ModuleCluster,
|
|
24
|
-
DomainAssignment,
|
|
25
|
-
DomainSignals,
|
|
26
|
-
CoUsageData,
|
|
27
|
-
TypeDependency,
|
|
28
|
-
FileClassification,
|
|
29
23
|
} from './types';
|
|
30
|
-
import {
|
|
31
|
-
buildCoUsageMatrix,
|
|
32
|
-
buildTypeGraph,
|
|
33
|
-
findSemanticClusters,
|
|
34
|
-
calculateDomainConfidence,
|
|
35
|
-
inferDomainFromSemantics,
|
|
36
|
-
getCoUsageData,
|
|
37
|
-
findConsolidationCandidates,
|
|
38
|
-
} from './semantic-analysis';
|
|
39
|
-
// Reference optional imports used by future heuristics to avoid lint warnings
|
|
40
|
-
void calculateFragmentation;
|
|
41
|
-
|
|
42
|
-
export type {
|
|
43
|
-
ContextAnalyzerOptions,
|
|
44
|
-
ContextAnalysisResult,
|
|
45
|
-
ContextSummary,
|
|
46
|
-
ModuleCluster,
|
|
47
|
-
DomainAssignment,
|
|
48
|
-
DomainSignals,
|
|
49
|
-
CoUsageData,
|
|
50
|
-
TypeDependency,
|
|
51
|
-
FileClassification,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export { classifyFile, adjustFragmentationForClassification };
|
|
55
24
|
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
getCoUsageData,
|
|
63
|
-
findConsolidationCandidates,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Generate smart defaults for context analysis based on repository size
|
|
68
|
-
* Automatically tunes thresholds to target ~10 most serious issues
|
|
69
|
-
*/
|
|
70
|
-
async function getSmartDefaults(
|
|
71
|
-
directory: string,
|
|
72
|
-
userOptions: Partial<ContextAnalyzerOptions>
|
|
73
|
-
): Promise<ContextAnalyzerOptions> {
|
|
74
|
-
// Estimate repository size by scanning files
|
|
75
|
-
const files = await scanFiles({
|
|
76
|
-
rootDir: directory,
|
|
77
|
-
include: userOptions.include,
|
|
78
|
-
exclude: userOptions.exclude,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const estimatedBlocks = files.length;
|
|
82
|
-
|
|
83
|
-
// Smart defaults based on repository size
|
|
84
|
-
// Adjusted to be stricter (higher thresholds) to catch only serious issues
|
|
85
|
-
// This targets ~10 most critical issues instead of showing all files
|
|
86
|
-
let maxDepth: number;
|
|
87
|
-
let maxContextBudget: number;
|
|
88
|
-
let minCohesion: number;
|
|
89
|
-
let maxFragmentation: number;
|
|
90
|
-
|
|
91
|
-
if (estimatedBlocks < 100) {
|
|
92
|
-
// Small project - be more lenient
|
|
93
|
-
maxDepth = 4;
|
|
94
|
-
maxContextBudget = 8000;
|
|
95
|
-
minCohesion = 0.5;
|
|
96
|
-
maxFragmentation = 0.5;
|
|
97
|
-
} else if (estimatedBlocks < 500) {
|
|
98
|
-
// Medium project - moderate strictness
|
|
99
|
-
maxDepth = 5;
|
|
100
|
-
maxContextBudget = 15000;
|
|
101
|
-
minCohesion = 0.45;
|
|
102
|
-
maxFragmentation = 0.6;
|
|
103
|
-
} else if (estimatedBlocks < 2000) {
|
|
104
|
-
// Large project - stricter to focus on worst issues
|
|
105
|
-
maxDepth = 7;
|
|
106
|
-
maxContextBudget = 25000;
|
|
107
|
-
minCohesion = 0.4;
|
|
108
|
-
maxFragmentation = 0.7;
|
|
109
|
-
} else {
|
|
110
|
-
// Enterprise project - very strict to show only critical issues
|
|
111
|
-
maxDepth = 10;
|
|
112
|
-
maxContextBudget = 40000;
|
|
113
|
-
minCohesion = 0.35;
|
|
114
|
-
maxFragmentation = 0.8;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
maxDepth,
|
|
119
|
-
maxContextBudget,
|
|
120
|
-
minCohesion,
|
|
121
|
-
maxFragmentation,
|
|
122
|
-
focus: 'all',
|
|
123
|
-
includeNodeModules: false,
|
|
124
|
-
rootDir: userOptions.rootDir || directory,
|
|
125
|
-
include: userOptions.include,
|
|
126
|
-
exclude: userOptions.exclude,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
25
|
+
export * from './analyzer';
|
|
26
|
+
export * from './scoring';
|
|
27
|
+
export * from './defaults';
|
|
28
|
+
export * from './summary';
|
|
29
|
+
export * from './types';
|
|
30
|
+
export * from './semantic-analysis';
|
|
129
31
|
|
|
130
32
|
/**
|
|
131
33
|
* Analyze AI context window cost for a codebase
|
|
@@ -143,13 +45,8 @@ export async function analyzeContext(
|
|
|
143
45
|
...scanOptions
|
|
144
46
|
} = options;
|
|
145
47
|
|
|
146
|
-
// Scan files (supports .ts, .js, .tsx, .jsx, .py)
|
|
147
|
-
// Note: scanFiles now automatically merges user excludes with DEFAULT_EXCLUDE
|
|
148
48
|
const files = await scanFiles({
|
|
149
49
|
...scanOptions,
|
|
150
|
-
// Only add node_modules to exclude if includeNodeModules is false
|
|
151
|
-
// The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
|
|
152
|
-
// if user overrides the default exclude list
|
|
153
50
|
exclude:
|
|
154
51
|
includeNodeModules && scanOptions.exclude
|
|
155
52
|
? scanOptions.exclude.filter(
|
|
@@ -158,12 +55,7 @@ export async function analyzeContext(
|
|
|
158
55
|
: scanOptions.exclude,
|
|
159
56
|
});
|
|
160
57
|
|
|
161
|
-
// Separate files by language
|
|
162
58
|
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith('.py'));
|
|
163
|
-
const tsJsFiles = files.filter((f) => !f.toLowerCase().endsWith('.py'));
|
|
164
|
-
void tsJsFiles;
|
|
165
|
-
|
|
166
|
-
// Read all file contents
|
|
167
59
|
const fileContents = await Promise.all(
|
|
168
60
|
files.map(async (file) => ({
|
|
169
61
|
file,
|
|
@@ -171,12 +63,10 @@ export async function analyzeContext(
|
|
|
171
63
|
}))
|
|
172
64
|
);
|
|
173
65
|
|
|
174
|
-
// Build dependency graph (TS/JS)
|
|
175
66
|
const graph = buildDependencyGraph(
|
|
176
67
|
fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
|
|
177
68
|
);
|
|
178
69
|
|
|
179
|
-
// Analyze Python files separately (if any)
|
|
180
70
|
let pythonResults: ContextAnalysisResult[] = [];
|
|
181
71
|
if (pythonFiles.length > 0) {
|
|
182
72
|
const { analyzePythonContext } = await import('./analyzers/python-context');
|
|
@@ -185,7 +75,6 @@ export async function analyzeContext(
|
|
|
185
75
|
scanOptions.rootDir || options.rootDir || '.'
|
|
186
76
|
);
|
|
187
77
|
|
|
188
|
-
// Convert Python metrics to ContextAnalysisResult format
|
|
189
78
|
pythonResults = pythonMetrics.map((metric) => {
|
|
190
79
|
const { severity, issues, recommendations, potentialSavings } =
|
|
191
80
|
analyzeIssues({
|
|
@@ -193,7 +82,7 @@ export async function analyzeContext(
|
|
|
193
82
|
importDepth: metric.importDepth,
|
|
194
83
|
contextBudget: metric.contextBudget,
|
|
195
84
|
cohesionScore: metric.cohesion,
|
|
196
|
-
fragmentationScore: 0,
|
|
85
|
+
fragmentationScore: 0,
|
|
197
86
|
maxDepth,
|
|
198
87
|
maxContextBudget,
|
|
199
88
|
minCohesion,
|
|
@@ -207,7 +96,7 @@ export async function analyzeContext(
|
|
|
207
96
|
file: metric.file,
|
|
208
97
|
tokenCost: Math.floor(
|
|
209
98
|
metric.contextBudget / (1 + metric.imports.length || 1)
|
|
210
|
-
),
|
|
99
|
+
),
|
|
211
100
|
linesOfCode: metric.metrics.linesOfCode,
|
|
212
101
|
importDepth: metric.importDepth,
|
|
213
102
|
dependencyCount: metric.imports.length,
|
|
@@ -218,12 +107,12 @@ export async function analyzeContext(
|
|
|
218
107
|
cycle.split(' → ')
|
|
219
108
|
),
|
|
220
109
|
cohesionScore: metric.cohesion,
|
|
221
|
-
domains: ['python'],
|
|
110
|
+
domains: ['python'],
|
|
222
111
|
exportCount: metric.exports.length,
|
|
223
112
|
contextBudget: metric.contextBudget,
|
|
224
113
|
fragmentationScore: 0,
|
|
225
114
|
relatedFiles: [],
|
|
226
|
-
fileClassification: 'unknown' as const,
|
|
115
|
+
fileClassification: 'unknown' as const,
|
|
227
116
|
severity,
|
|
228
117
|
issues,
|
|
229
118
|
recommendations,
|
|
@@ -232,12 +121,8 @@ export async function analyzeContext(
|
|
|
232
121
|
});
|
|
233
122
|
}
|
|
234
123
|
|
|
235
|
-
// Detect circular dependencies (TS/JS)
|
|
236
124
|
const circularDeps = detectCircularDependencies(graph);
|
|
237
|
-
|
|
238
|
-
// Detect module clusters for fragmentation analysis
|
|
239
|
-
// Enable log-scaling for fragmentation by default on medium+ repos
|
|
240
|
-
const useLogScale = files.length >= 500; // medium and larger projects
|
|
125
|
+
const useLogScale = files.length >= 500;
|
|
241
126
|
const clusters = detectModuleClusters(graph, { useLogScale });
|
|
242
127
|
const fragmentationMap = new Map<string, number>();
|
|
243
128
|
for (const cluster of clusters) {
|
|
@@ -246,27 +131,22 @@ export async function analyzeContext(
|
|
|
246
131
|
}
|
|
247
132
|
}
|
|
248
133
|
|
|
249
|
-
// Analyze each file
|
|
250
134
|
const results: ContextAnalysisResult[] = [];
|
|
251
135
|
|
|
252
136
|
for (const { file } of fileContents) {
|
|
253
137
|
const node = graph.nodes.get(file);
|
|
254
138
|
if (!node) continue;
|
|
255
139
|
|
|
256
|
-
// Calculate metrics based on focus
|
|
257
140
|
const importDepth =
|
|
258
141
|
focus === 'depth' || focus === 'all'
|
|
259
142
|
? calculateImportDepth(file, graph)
|
|
260
143
|
: 0;
|
|
261
|
-
|
|
262
144
|
const dependencyList =
|
|
263
145
|
focus === 'depth' || focus === 'all'
|
|
264
146
|
? getTransitiveDependencies(file, graph)
|
|
265
147
|
: [];
|
|
266
|
-
|
|
267
148
|
const contextBudget =
|
|
268
149
|
focus === 'all' ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
269
|
-
|
|
270
150
|
const cohesionScore =
|
|
271
151
|
focus === 'cohesion' || focus === 'all'
|
|
272
152
|
? calculateCohesion(node.exports, file, {
|
|
@@ -275,8 +155,6 @@ export async function analyzeContext(
|
|
|
275
155
|
: 1;
|
|
276
156
|
|
|
277
157
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
278
|
-
|
|
279
|
-
// Find related files (files in same domain cluster)
|
|
280
158
|
const relatedFiles: string[] = [];
|
|
281
159
|
for (const cluster of clusters) {
|
|
282
160
|
if (cluster.files.includes(file)) {
|
|
@@ -285,55 +163,38 @@ export async function analyzeContext(
|
|
|
285
163
|
}
|
|
286
164
|
}
|
|
287
165
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
circularDeps,
|
|
301
|
-
});
|
|
302
|
-
// Some returned fields are not needed for the Python mapping here
|
|
303
|
-
void severity;
|
|
304
|
-
void issues;
|
|
305
|
-
void recommendations;
|
|
306
|
-
void potentialSavings;
|
|
166
|
+
const { issues } = analyzeIssues({
|
|
167
|
+
file,
|
|
168
|
+
importDepth,
|
|
169
|
+
contextBudget,
|
|
170
|
+
cohesionScore,
|
|
171
|
+
fragmentationScore,
|
|
172
|
+
maxDepth,
|
|
173
|
+
maxContextBudget,
|
|
174
|
+
minCohesion,
|
|
175
|
+
maxFragmentation,
|
|
176
|
+
circularDeps,
|
|
177
|
+
});
|
|
307
178
|
|
|
308
|
-
// Get domains from exports
|
|
309
179
|
const domains = [
|
|
310
180
|
...new Set(node.exports.map((e) => e.inferredDomain || 'unknown')),
|
|
311
181
|
];
|
|
312
|
-
|
|
313
|
-
// Classify the file to help distinguish real issues from false positives
|
|
314
|
-
const fileClassification = classifyFile(node, cohesionScore, domains);
|
|
315
|
-
|
|
316
|
-
// Adjust cohesion based on classification (utility/service/handler files get boosted)
|
|
182
|
+
const fileClassification = classifyFile(node);
|
|
317
183
|
const adjustedCohesionScore = adjustCohesionForClassification(
|
|
318
184
|
cohesionScore,
|
|
319
185
|
fileClassification,
|
|
320
186
|
node
|
|
321
187
|
);
|
|
322
|
-
|
|
323
|
-
// Adjust fragmentation based on classification
|
|
324
188
|
const adjustedFragmentationScore = adjustFragmentationForClassification(
|
|
325
189
|
fragmentationScore,
|
|
326
190
|
fileClassification
|
|
327
191
|
);
|
|
328
|
-
|
|
329
|
-
// Get classification-specific recommendations
|
|
330
192
|
const classificationRecommendations = getClassificationRecommendations(
|
|
331
193
|
fileClassification,
|
|
332
194
|
file,
|
|
333
195
|
issues
|
|
334
196
|
);
|
|
335
197
|
|
|
336
|
-
// Re-analyze issues with adjusted cohesion and fragmentation
|
|
337
198
|
const {
|
|
338
199
|
severity: adjustedSeverity,
|
|
339
200
|
issues: adjustedIssues,
|
|
@@ -343,7 +204,7 @@ export async function analyzeContext(
|
|
|
343
204
|
file,
|
|
344
205
|
importDepth,
|
|
345
206
|
contextBudget,
|
|
346
|
-
cohesionScore: adjustedCohesionScore,
|
|
207
|
+
cohesionScore: adjustedCohesionScore,
|
|
347
208
|
fragmentationScore: adjustedFragmentationScore,
|
|
348
209
|
maxDepth,
|
|
349
210
|
maxContextBudget,
|
|
@@ -360,7 +221,7 @@ export async function analyzeContext(
|
|
|
360
221
|
dependencyCount: dependencyList.length,
|
|
361
222
|
dependencyList,
|
|
362
223
|
circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
|
|
363
|
-
cohesionScore: adjustedCohesionScore,
|
|
224
|
+
cohesionScore: adjustedCohesionScore,
|
|
364
225
|
domains,
|
|
365
226
|
exportCount: node.exports.length,
|
|
366
227
|
contextBudget,
|
|
@@ -377,361 +238,11 @@ export async function analyzeContext(
|
|
|
377
238
|
});
|
|
378
239
|
}
|
|
379
240
|
|
|
380
|
-
// Merge Python and TS/JS results
|
|
381
241
|
const allResults = [...results, ...pythonResults];
|
|
382
|
-
|
|
383
|
-
// Keep ALL results including info severity for visualization purposes
|
|
384
|
-
// The visualizer needs to show all files as nodes
|
|
385
|
-
const sorted = allResults.sort((a, b) => {
|
|
242
|
+
return allResults.sort((a, b) => {
|
|
386
243
|
const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
|
|
387
244
|
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
388
245
|
if (severityDiff !== 0) return severityDiff;
|
|
389
246
|
return b.contextBudget - a.contextBudget;
|
|
390
247
|
});
|
|
391
|
-
|
|
392
|
-
return sorted;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Generate summary of context analysis results
|
|
397
|
-
*/
|
|
398
|
-
export function generateSummary(
|
|
399
|
-
results: ContextAnalysisResult[]
|
|
400
|
-
): ContextSummary {
|
|
401
|
-
if (results.length === 0) {
|
|
402
|
-
return {
|
|
403
|
-
totalFiles: 0,
|
|
404
|
-
totalTokens: 0,
|
|
405
|
-
avgContextBudget: 0,
|
|
406
|
-
maxContextBudget: 0,
|
|
407
|
-
avgImportDepth: 0,
|
|
408
|
-
maxImportDepth: 0,
|
|
409
|
-
deepFiles: [],
|
|
410
|
-
avgFragmentation: 0,
|
|
411
|
-
fragmentedModules: [],
|
|
412
|
-
avgCohesion: 0,
|
|
413
|
-
lowCohesionFiles: [],
|
|
414
|
-
criticalIssues: 0,
|
|
415
|
-
majorIssues: 0,
|
|
416
|
-
minorIssues: 0,
|
|
417
|
-
totalPotentialSavings: 0,
|
|
418
|
-
topExpensiveFiles: [],
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const totalFiles = results.length;
|
|
423
|
-
const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
|
|
424
|
-
const totalContextBudget = results.reduce(
|
|
425
|
-
(sum, r) => sum + r.contextBudget,
|
|
426
|
-
0
|
|
427
|
-
);
|
|
428
|
-
const avgContextBudget = totalContextBudget / totalFiles;
|
|
429
|
-
const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
|
|
430
|
-
|
|
431
|
-
const avgImportDepth =
|
|
432
|
-
results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
|
|
433
|
-
const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
|
|
434
|
-
|
|
435
|
-
const deepFiles = results
|
|
436
|
-
.filter((r) => r.importDepth >= 5)
|
|
437
|
-
.map((r) => ({ file: r.file, depth: r.importDepth }))
|
|
438
|
-
.sort((a, b) => b.depth - a.depth)
|
|
439
|
-
.slice(0, 10);
|
|
440
|
-
|
|
441
|
-
const avgFragmentation =
|
|
442
|
-
results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
|
|
443
|
-
|
|
444
|
-
// Get unique module clusters
|
|
445
|
-
const moduleMap = new Map<string, ContextAnalysisResult[]>();
|
|
446
|
-
for (const result of results) {
|
|
447
|
-
for (const domain of result.domains) {
|
|
448
|
-
if (!moduleMap.has(domain)) {
|
|
449
|
-
moduleMap.set(domain, []);
|
|
450
|
-
}
|
|
451
|
-
moduleMap.get(domain)!.push(result);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const fragmentedModules: ModuleCluster[] = [];
|
|
456
|
-
for (const [domain, files] of moduleMap.entries()) {
|
|
457
|
-
if (files.length < 2) continue;
|
|
458
|
-
const fragmentationScore =
|
|
459
|
-
files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
|
|
460
|
-
if (fragmentationScore < 0.3) continue; // Skip well-organized modules
|
|
461
|
-
|
|
462
|
-
const totalTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
|
|
463
|
-
const avgCohesion =
|
|
464
|
-
files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
|
|
465
|
-
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
466
|
-
|
|
467
|
-
// Compute path entropy and directory distance for reporting
|
|
468
|
-
const filePaths = files.map((f) => f.file);
|
|
469
|
-
const pathEntropy = calculatePathEntropy(filePaths);
|
|
470
|
-
const directoryDistance = calculateDirectoryDistance(filePaths);
|
|
471
|
-
|
|
472
|
-
// Compute import cohesion based on dependency lists (Jaccard similarity)
|
|
473
|
-
function jaccard(a: string[], b: string[]) {
|
|
474
|
-
const s1 = new Set(a || []);
|
|
475
|
-
const s2 = new Set(b || []);
|
|
476
|
-
if (s1.size === 0 && s2.size === 0) return 0;
|
|
477
|
-
const inter = new Set([...s1].filter((x) => s2.has(x)));
|
|
478
|
-
const uni = new Set([...s1, ...s2]);
|
|
479
|
-
return uni.size === 0 ? 0 : inter.size / uni.size;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
let importSimTotal = 0;
|
|
483
|
-
let importPairs = 0;
|
|
484
|
-
for (let i = 0; i < files.length; i++) {
|
|
485
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
486
|
-
importSimTotal += jaccard(
|
|
487
|
-
files[i].dependencyList || [],
|
|
488
|
-
files[j].dependencyList || []
|
|
489
|
-
);
|
|
490
|
-
importPairs++;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
|
|
495
|
-
|
|
496
|
-
fragmentedModules.push({
|
|
497
|
-
domain,
|
|
498
|
-
files: files.map((f) => f.file),
|
|
499
|
-
totalTokens,
|
|
500
|
-
fragmentationScore,
|
|
501
|
-
pathEntropy,
|
|
502
|
-
directoryDistance,
|
|
503
|
-
importCohesion,
|
|
504
|
-
avgCohesion,
|
|
505
|
-
suggestedStructure: {
|
|
506
|
-
targetFiles,
|
|
507
|
-
consolidationPlan: [
|
|
508
|
-
`Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s)`,
|
|
509
|
-
`Current token cost: ${totalTokens.toLocaleString()}`,
|
|
510
|
-
`Estimated savings: ${Math.floor(totalTokens * 0.3).toLocaleString()} tokens (30%)`,
|
|
511
|
-
],
|
|
512
|
-
},
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
517
|
-
|
|
518
|
-
const avgCohesion =
|
|
519
|
-
results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
|
|
520
|
-
|
|
521
|
-
const lowCohesionFiles = results
|
|
522
|
-
.filter((r) => r.cohesionScore < 0.6)
|
|
523
|
-
.map((r) => ({ file: r.file, score: r.cohesionScore }))
|
|
524
|
-
.sort((a, b) => a.score - b.score)
|
|
525
|
-
.slice(0, 10);
|
|
526
|
-
|
|
527
|
-
const criticalIssues = results.filter(
|
|
528
|
-
(r) => r.severity === 'critical'
|
|
529
|
-
).length;
|
|
530
|
-
const majorIssues = results.filter((r) => r.severity === 'major').length;
|
|
531
|
-
const minorIssues = results.filter((r) => r.severity === 'minor').length;
|
|
532
|
-
|
|
533
|
-
const totalPotentialSavings = results.reduce(
|
|
534
|
-
(sum, r) => sum + r.potentialSavings,
|
|
535
|
-
0
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
const topExpensiveFiles = results
|
|
539
|
-
.sort((a, b) => b.contextBudget - a.contextBudget)
|
|
540
|
-
.slice(0, 10)
|
|
541
|
-
.map((r) => ({
|
|
542
|
-
file: r.file,
|
|
543
|
-
contextBudget: r.contextBudget,
|
|
544
|
-
severity: r.severity,
|
|
545
|
-
}));
|
|
546
|
-
|
|
547
|
-
return {
|
|
548
|
-
totalFiles,
|
|
549
|
-
totalTokens,
|
|
550
|
-
avgContextBudget,
|
|
551
|
-
maxContextBudget,
|
|
552
|
-
avgImportDepth,
|
|
553
|
-
maxImportDepth,
|
|
554
|
-
deepFiles,
|
|
555
|
-
avgFragmentation,
|
|
556
|
-
fragmentedModules: fragmentedModules.slice(0, 10),
|
|
557
|
-
avgCohesion,
|
|
558
|
-
lowCohesionFiles,
|
|
559
|
-
criticalIssues,
|
|
560
|
-
majorIssues,
|
|
561
|
-
minorIssues,
|
|
562
|
-
totalPotentialSavings,
|
|
563
|
-
topExpensiveFiles,
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Analyze issues for a single file
|
|
569
|
-
*/
|
|
570
|
-
function analyzeIssues(params: {
|
|
571
|
-
file: string;
|
|
572
|
-
importDepth: number;
|
|
573
|
-
contextBudget: number;
|
|
574
|
-
cohesionScore: number;
|
|
575
|
-
fragmentationScore: number;
|
|
576
|
-
maxDepth: number;
|
|
577
|
-
maxContextBudget: number;
|
|
578
|
-
minCohesion: number;
|
|
579
|
-
maxFragmentation: number;
|
|
580
|
-
circularDeps: string[][];
|
|
581
|
-
}): {
|
|
582
|
-
severity: ContextAnalysisResult['severity'];
|
|
583
|
-
issues: string[];
|
|
584
|
-
recommendations: string[];
|
|
585
|
-
potentialSavings: number;
|
|
586
|
-
} {
|
|
587
|
-
const {
|
|
588
|
-
file,
|
|
589
|
-
importDepth,
|
|
590
|
-
contextBudget,
|
|
591
|
-
cohesionScore,
|
|
592
|
-
fragmentationScore,
|
|
593
|
-
maxDepth,
|
|
594
|
-
maxContextBudget,
|
|
595
|
-
minCohesion,
|
|
596
|
-
maxFragmentation,
|
|
597
|
-
circularDeps,
|
|
598
|
-
} = params;
|
|
599
|
-
|
|
600
|
-
const issues: string[] = [];
|
|
601
|
-
const recommendations: string[] = [];
|
|
602
|
-
let severity: ContextAnalysisResult['severity'] = 'info';
|
|
603
|
-
let potentialSavings = 0;
|
|
604
|
-
|
|
605
|
-
// Check circular dependencies (CRITICAL)
|
|
606
|
-
if (circularDeps.length > 0) {
|
|
607
|
-
severity = 'critical';
|
|
608
|
-
issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
|
|
609
|
-
recommendations.push(
|
|
610
|
-
'Break circular dependencies by extracting interfaces or using dependency injection'
|
|
611
|
-
);
|
|
612
|
-
potentialSavings += contextBudget * 0.2; // Estimate 20% savings
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Check import depth
|
|
616
|
-
if (importDepth > maxDepth * 1.5) {
|
|
617
|
-
severity = severity === 'critical' ? 'critical' : 'critical';
|
|
618
|
-
issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
|
|
619
|
-
recommendations.push('Flatten dependency tree or use facade pattern');
|
|
620
|
-
potentialSavings += contextBudget * 0.3; // Estimate 30% savings
|
|
621
|
-
} else if (importDepth > maxDepth) {
|
|
622
|
-
severity = severity === 'critical' ? 'critical' : 'major';
|
|
623
|
-
issues.push(
|
|
624
|
-
`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
|
|
625
|
-
);
|
|
626
|
-
recommendations.push('Consider reducing dependency depth');
|
|
627
|
-
potentialSavings += contextBudget * 0.15;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Check context budget
|
|
631
|
-
if (contextBudget > maxContextBudget * 1.5) {
|
|
632
|
-
severity = severity === 'critical' ? 'critical' : 'critical';
|
|
633
|
-
issues.push(
|
|
634
|
-
`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
635
|
-
);
|
|
636
|
-
recommendations.push(
|
|
637
|
-
'Split into smaller modules or reduce dependency tree'
|
|
638
|
-
);
|
|
639
|
-
potentialSavings += contextBudget * 0.4; // Significant savings possible
|
|
640
|
-
} else if (contextBudget > maxContextBudget) {
|
|
641
|
-
severity =
|
|
642
|
-
severity === 'critical' || severity === 'major' ? severity : 'major';
|
|
643
|
-
issues.push(
|
|
644
|
-
`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
645
|
-
);
|
|
646
|
-
recommendations.push('Reduce file size or dependencies');
|
|
647
|
-
potentialSavings += contextBudget * 0.2;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Check cohesion
|
|
651
|
-
if (cohesionScore < minCohesion * 0.5) {
|
|
652
|
-
severity = severity === 'critical' ? 'critical' : 'major';
|
|
653
|
-
issues.push(
|
|
654
|
-
`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
|
|
655
|
-
);
|
|
656
|
-
recommendations.push(
|
|
657
|
-
'Split file by domain - separate unrelated functionality'
|
|
658
|
-
);
|
|
659
|
-
potentialSavings += contextBudget * 0.25;
|
|
660
|
-
} else if (cohesionScore < minCohesion) {
|
|
661
|
-
severity =
|
|
662
|
-
severity === 'critical' || severity === 'major' ? severity : 'minor';
|
|
663
|
-
issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
|
|
664
|
-
recommendations.push('Consider grouping related exports together');
|
|
665
|
-
potentialSavings += contextBudget * 0.1;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Check fragmentation
|
|
669
|
-
if (fragmentationScore > maxFragmentation) {
|
|
670
|
-
severity =
|
|
671
|
-
severity === 'critical' || severity === 'major' ? severity : 'minor';
|
|
672
|
-
issues.push(
|
|
673
|
-
`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
|
|
674
|
-
);
|
|
675
|
-
recommendations.push('Consolidate with related files in same domain');
|
|
676
|
-
potentialSavings += contextBudget * 0.3;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (issues.length === 0) {
|
|
680
|
-
issues.push('No significant issues detected');
|
|
681
|
-
recommendations.push('File is well-structured for AI context usage');
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Detect build artifacts and downgrade severity to reduce noise
|
|
685
|
-
if (isBuildArtifact(file)) {
|
|
686
|
-
issues.push('Detected build artifact (bundled/output file)');
|
|
687
|
-
recommendations.push(
|
|
688
|
-
'Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis'
|
|
689
|
-
);
|
|
690
|
-
severity = downgradeSeverity(severity);
|
|
691
|
-
// Build artifacts do not represent actionable savings
|
|
692
|
-
potentialSavings = 0;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return {
|
|
696
|
-
severity,
|
|
697
|
-
issues,
|
|
698
|
-
recommendations,
|
|
699
|
-
potentialSavings: Math.floor(potentialSavings),
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
export { getSmartDefaults };
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Heuristic: identify common build artifact paths
|
|
707
|
-
*/
|
|
708
|
-
function isBuildArtifact(filePath: string): boolean {
|
|
709
|
-
const lower = filePath.toLowerCase();
|
|
710
|
-
return (
|
|
711
|
-
lower.includes('/node_modules/') ||
|
|
712
|
-
lower.includes('/dist/') ||
|
|
713
|
-
lower.includes('/build/') ||
|
|
714
|
-
lower.includes('/out/') ||
|
|
715
|
-
lower.includes('/output/') ||
|
|
716
|
-
lower.includes('/cdk.out/') ||
|
|
717
|
-
lower.includes('/.next/') ||
|
|
718
|
-
/\/asset\.[^/]+\//.test(lower) // e.g., cdk.out/asset.*
|
|
719
|
-
);
|
|
720
248
|
}
|
|
721
|
-
|
|
722
|
-
function downgradeSeverity(
|
|
723
|
-
s: ContextAnalysisResult['severity']
|
|
724
|
-
): ContextAnalysisResult['severity'] {
|
|
725
|
-
switch (s) {
|
|
726
|
-
case 'critical':
|
|
727
|
-
return 'minor';
|
|
728
|
-
case 'major':
|
|
729
|
-
return 'minor';
|
|
730
|
-
case 'minor':
|
|
731
|
-
return 'info';
|
|
732
|
-
default:
|
|
733
|
-
return 'info';
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export { calculateContextScore };
|