@aiready/context-analyzer 0.21.5 → 0.21.6
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/.aiready/aiready-report-20260314-222254.json +39216 -0
- package/.aiready/aiready-report-20260314-223947.json +3413 -0
- package/.aiready/aiready-report-20260314-224112.json +3413 -0
- package/.aiready/aiready-report-20260314-224302.json +2973 -0
- package/.aiready/aiready-report-20260314-224939.json +3092 -0
- package/.aiready/aiready-report-20260314-225154.json +3092 -0
- package/.turbo/turbo-build.log +26 -24
- package/.turbo/turbo-test.log +41 -119
- package/dist/__tests__/analyzer.test.js +55 -14
- package/dist/__tests__/analyzer.test.js.map +1 -1
- package/dist/__tests__/cluster-detector.test.d.ts +2 -0
- package/dist/__tests__/cluster-detector.test.d.ts.map +1 -0
- package/dist/__tests__/cluster-detector.test.js +121 -0
- package/dist/__tests__/cluster-detector.test.js.map +1 -0
- package/dist/__tests__/contract.test.d.ts +2 -0
- package/dist/__tests__/contract.test.d.ts.map +1 -0
- package/dist/__tests__/contract.test.js +59 -0
- package/dist/__tests__/contract.test.js.map +1 -0
- package/dist/__tests__/enhanced-cohesion.test.js +12 -2
- package/dist/__tests__/enhanced-cohesion.test.js.map +1 -1
- package/dist/__tests__/file-classification.test.d.ts +2 -0
- package/dist/__tests__/file-classification.test.d.ts.map +1 -0
- package/dist/__tests__/file-classification.test.js +749 -0
- package/dist/__tests__/file-classification.test.js.map +1 -0
- package/dist/__tests__/fragmentation-advanced.test.js +2 -8
- package/dist/__tests__/fragmentation-advanced.test.js.map +1 -1
- package/dist/__tests__/fragmentation-coupling.test.js +2 -2
- package/dist/__tests__/fragmentation-coupling.test.js.map +1 -1
- package/dist/__tests__/fragmentation-log.test.js +3 -7
- package/dist/__tests__/fragmentation-log.test.js.map +1 -1
- package/dist/__tests__/provider.test.d.ts +2 -0
- package/dist/__tests__/provider.test.d.ts.map +1 -0
- package/dist/__tests__/provider.test.js +72 -0
- package/dist/__tests__/provider.test.js.map +1 -0
- package/dist/__tests__/remediation.test.d.ts +2 -0
- package/dist/__tests__/remediation.test.d.ts.map +1 -0
- package/dist/__tests__/remediation.test.js +61 -0
- package/dist/__tests__/remediation.test.js.map +1 -0
- package/dist/__tests__/scoring.test.js +196 -16
- package/dist/__tests__/scoring.test.js.map +1 -1
- package/dist/__tests__/structural-cohesion.test.js +8 -2
- package/dist/__tests__/structural-cohesion.test.js.map +1 -1
- package/dist/analyzer.d.ts +31 -94
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +260 -678
- package/dist/analyzer.js.map +1 -1
- package/dist/analyzers/python-context.d.ts.map +1 -1
- package/dist/analyzers/python-context.js +10 -8
- package/dist/analyzers/python-context.js.map +1 -1
- package/dist/ast-utils.d.ts +16 -0
- package/dist/ast-utils.d.ts.map +1 -0
- package/dist/ast-utils.js +81 -0
- package/dist/ast-utils.js.map +1 -0
- package/dist/chunk-64U3PNO3.mjs +94 -0
- package/dist/chunk-CDIVYADN.mjs +2110 -0
- package/dist/chunk-D3SIHB2V.mjs +2118 -0
- package/dist/chunk-FNPSK3CG.mjs +1760 -0
- package/dist/chunk-GXTGOLZT.mjs +92 -0
- package/dist/chunk-LERPI33Y.mjs +2060 -0
- package/dist/chunk-MZP3G7TF.mjs +2118 -0
- package/dist/chunk-NOHK5DLU.mjs +2173 -0
- package/dist/chunk-ORLC5Y4J.mjs +1787 -0
- package/dist/chunk-OTCQL7DY.mjs +2045 -0
- package/dist/chunk-SFK6XTJE.mjs +2110 -0
- package/dist/chunk-U5R2FTCR.mjs +1803 -0
- package/dist/chunk-UU4HZ7ZT.mjs +1849 -0
- package/dist/chunk-WKOZOHOU.mjs +2060 -0
- package/dist/chunk-XIXAWCMS.mjs +1760 -0
- package/dist/classifier.d.ts +114 -0
- package/dist/classifier.d.ts.map +1 -0
- package/dist/classifier.js +439 -0
- package/dist/classifier.js.map +1 -0
- package/dist/cli.js +590 -1071
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +63 -533
- package/dist/cluster-detector.d.ts +8 -0
- package/dist/cluster-detector.d.ts.map +1 -0
- package/dist/cluster-detector.js +70 -0
- package/dist/cluster-detector.js.map +1 -0
- package/dist/defaults.d.ts +7 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +54 -0
- package/dist/defaults.js.map +1 -0
- package/dist/graph-builder.d.ts +33 -0
- package/dist/graph-builder.d.ts.map +1 -0
- package/dist/graph-builder.js +225 -0
- package/dist/graph-builder.js.map +1 -0
- package/dist/index.d.mts +24 -31
- package/dist/index.d.ts +24 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +788 -569
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +265 -8
- package/dist/metrics.d.ts +34 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +170 -0
- package/dist/metrics.js.map +1 -0
- package/dist/provider.d.ts +6 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +48 -0
- package/dist/provider.js.map +1 -0
- package/dist/python-context-3GZKN3LR.mjs +162 -0
- package/dist/python-context-O2EN3M6Z.mjs +162 -0
- package/dist/remediation.d.ts +25 -0
- package/dist/remediation.d.ts.map +1 -0
- package/dist/remediation.js +98 -0
- package/dist/remediation.js.map +1 -0
- package/dist/scoring.d.ts +3 -7
- package/dist/scoring.d.ts.map +1 -1
- package/dist/scoring.js +57 -48
- package/dist/scoring.js.map +1 -1
- package/dist/semantic-analysis.d.ts +12 -23
- package/dist/semantic-analysis.d.ts.map +1 -1
- package/dist/semantic-analysis.js +172 -110
- package/dist/semantic-analysis.js.map +1 -1
- package/dist/summary.d.ts +6 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +92 -0
- package/dist/summary.js.map +1 -0
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/output-formatter.d.ts +14 -0
- package/dist/utils/output-formatter.d.ts.map +1 -0
- package/dist/utils/output-formatter.js +338 -0
- package/dist/utils/output-formatter.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +1 -1
- package/src/__tests__/auto-detection.test.ts +1 -1
- package/src/__tests__/contract.test.ts +1 -1
- package/src/__tests__/enhanced-cohesion.test.ts +1 -1
- package/src/__tests__/file-classification.test.ts +1 -1
- package/src/__tests__/fragmentation-advanced.test.ts +1 -1
- package/src/__tests__/fragmentation-coupling.test.ts +1 -1
- package/src/__tests__/fragmentation-log.test.ts +1 -1
- package/src/__tests__/provider.test.ts +1 -1
- package/src/__tests__/structural-cohesion.test.ts +1 -1
- package/src/analyzer.ts +96 -309
- package/src/analyzers/python-context.ts +7 -76
- package/src/cli-action.ts +103 -0
- package/src/cli.ts +12 -693
- package/src/cluster-detector.ts +1 -1
- package/src/graph-builder.ts +9 -85
- package/src/index.ts +6 -0
- package/src/issue-analyzer.ts +143 -0
- package/src/semantic-analysis.ts +1 -14
- package/src/summary.ts +62 -106
- package/src/utils/dependency-graph-utils.ts +126 -0
- package/src/utils/output-formatter.ts +411 -0
- package/src/utils/string-utils.ts +17 -0
package/src/cluster-detector.ts
CHANGED
|
@@ -35,7 +35,7 @@ export function detectModuleClusters(
|
|
|
35
35
|
(f) => new Set(graph.nodes.get(f)?.imports || [])
|
|
36
36
|
);
|
|
37
37
|
let intersection = new Set(allImportSets[0]);
|
|
38
|
-
|
|
38
|
+
const union = new Set(allImportSets[0]);
|
|
39
39
|
|
|
40
40
|
for (let i = 1; i < allImportSets.length; i++) {
|
|
41
41
|
const nextSet = allImportSets[i];
|
package/src/graph-builder.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { estimateTokens, parseFileExports } from '@aiready/core';
|
|
2
|
+
import { singularize } from './utils/string-utils';
|
|
3
|
+
import {
|
|
4
|
+
calculateImportDepthFromEdges,
|
|
5
|
+
detectGraphCycles,
|
|
6
|
+
getTransitiveDependenciesFromEdges,
|
|
7
|
+
} from './utils/dependency-graph-utils';
|
|
2
8
|
import type { DependencyGraph, DependencyNode } from './types';
|
|
3
9
|
import {
|
|
4
10
|
buildCoUsageMatrix,
|
|
@@ -94,25 +100,6 @@ export function extractDomainKeywordsFromPaths(files: FileContent[]): string[] {
|
|
|
94
100
|
return Array.from(folderNames);
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
/**
|
|
98
|
-
* Simple singularization for common English plurals
|
|
99
|
-
*/
|
|
100
|
-
function singularize(word: string): string {
|
|
101
|
-
const irregulars: Record<string, string> = {
|
|
102
|
-
people: 'person',
|
|
103
|
-
children: 'child',
|
|
104
|
-
men: 'man',
|
|
105
|
-
women: 'woman',
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
if (irregulars[word]) return irregulars[word];
|
|
109
|
-
if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
|
|
110
|
-
if (word.endsWith('ses')) return word.slice(0, -2);
|
|
111
|
-
if (word.endsWith('s') && word.length > 3) return word.slice(0, -1);
|
|
112
|
-
|
|
113
|
-
return word;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
103
|
/**
|
|
117
104
|
* Build a dependency graph from file contents
|
|
118
105
|
*/
|
|
@@ -196,23 +183,7 @@ export function calculateImportDepth(
|
|
|
196
183
|
visited = new Set<string>(),
|
|
197
184
|
depth = 0
|
|
198
185
|
): number {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const dependencies = graph.edges.get(file);
|
|
202
|
-
if (!dependencies || dependencies.size === 0) return depth;
|
|
203
|
-
|
|
204
|
-
visited.add(file);
|
|
205
|
-
let maxDepth = depth;
|
|
206
|
-
|
|
207
|
-
for (const dep of dependencies) {
|
|
208
|
-
maxDepth = Math.max(
|
|
209
|
-
maxDepth,
|
|
210
|
-
calculateImportDepth(dep, graph, visited, depth + 1)
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
visited.delete(file);
|
|
215
|
-
return maxDepth;
|
|
186
|
+
return calculateImportDepthFromEdges(file, graph.edges, visited, depth);
|
|
216
187
|
}
|
|
217
188
|
|
|
218
189
|
/**
|
|
@@ -223,19 +194,7 @@ export function getTransitiveDependencies(
|
|
|
223
194
|
graph: DependencyGraph,
|
|
224
195
|
visited = new Set<string>()
|
|
225
196
|
): string[] {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
visited.add(file);
|
|
229
|
-
const dependencies = graph.edges.get(file);
|
|
230
|
-
if (!dependencies || dependencies.size === 0) return [];
|
|
231
|
-
|
|
232
|
-
const allDeps: string[] = [];
|
|
233
|
-
for (const dep of dependencies) {
|
|
234
|
-
allDeps.push(dep);
|
|
235
|
-
allDeps.push(...getTransitiveDependencies(dep, graph, visited));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return [...new Set(allDeps)];
|
|
197
|
+
return getTransitiveDependenciesFromEdges(file, graph.edges, visited);
|
|
239
198
|
}
|
|
240
199
|
|
|
241
200
|
/**
|
|
@@ -265,40 +224,5 @@ export function calculateContextBudget(
|
|
|
265
224
|
* Detect circular dependencies
|
|
266
225
|
*/
|
|
267
226
|
export function detectCircularDependencies(graph: DependencyGraph): string[][] {
|
|
268
|
-
|
|
269
|
-
const visited = new Set<string>();
|
|
270
|
-
const recursionStack = new Set<string>();
|
|
271
|
-
|
|
272
|
-
function dfs(file: string, path: string[]): void {
|
|
273
|
-
if (recursionStack.has(file)) {
|
|
274
|
-
const cycleStart = path.indexOf(file);
|
|
275
|
-
if (cycleStart !== -1) {
|
|
276
|
-
cycles.push([...path.slice(cycleStart), file]);
|
|
277
|
-
}
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (visited.has(file)) return;
|
|
282
|
-
|
|
283
|
-
visited.add(file);
|
|
284
|
-
recursionStack.add(file);
|
|
285
|
-
path.push(file);
|
|
286
|
-
|
|
287
|
-
const dependencies = graph.edges.get(file);
|
|
288
|
-
if (dependencies) {
|
|
289
|
-
for (const dep of dependencies) {
|
|
290
|
-
dfs(dep, [...path]);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
recursionStack.delete(file);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
for (const file of graph.nodes.keys()) {
|
|
298
|
-
if (!visited.has(file)) {
|
|
299
|
-
dfs(file, []);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return cycles;
|
|
227
|
+
return detectGraphCycles(graph.edges);
|
|
304
228
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,15 @@ import { ContextAnalyzerProvider } from './provider';
|
|
|
5
5
|
ToolRegistry.register(ContextAnalyzerProvider);
|
|
6
6
|
|
|
7
7
|
export * from './analyzer';
|
|
8
|
+
export * from './graph-builder';
|
|
9
|
+
export * from './metrics';
|
|
10
|
+
export * from './classifier';
|
|
11
|
+
export * from './cluster-detector';
|
|
12
|
+
export * from './remediation';
|
|
8
13
|
export * from './scoring';
|
|
9
14
|
export * from './defaults';
|
|
10
15
|
export * from './summary';
|
|
11
16
|
export * from './types';
|
|
12
17
|
export * from './semantic-analysis';
|
|
13
18
|
export { ContextAnalyzerProvider };
|
|
19
|
+
export * from './utils/output-formatter';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Severity } from '@aiready/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal issue analysis logic
|
|
5
|
+
*/
|
|
6
|
+
export function analyzeIssues(params: {
|
|
7
|
+
file: string;
|
|
8
|
+
importDepth: number;
|
|
9
|
+
contextBudget: number;
|
|
10
|
+
cohesionScore: number;
|
|
11
|
+
fragmentationScore: number;
|
|
12
|
+
maxDepth: number;
|
|
13
|
+
maxContextBudget: number;
|
|
14
|
+
minCohesion: number;
|
|
15
|
+
maxFragmentation: number;
|
|
16
|
+
circularDeps: string[][];
|
|
17
|
+
}): {
|
|
18
|
+
severity: Severity;
|
|
19
|
+
issues: string[];
|
|
20
|
+
recommendations: string[];
|
|
21
|
+
potentialSavings: number;
|
|
22
|
+
} {
|
|
23
|
+
const {
|
|
24
|
+
file,
|
|
25
|
+
importDepth,
|
|
26
|
+
contextBudget,
|
|
27
|
+
cohesionScore,
|
|
28
|
+
fragmentationScore,
|
|
29
|
+
maxDepth,
|
|
30
|
+
maxContextBudget,
|
|
31
|
+
minCohesion,
|
|
32
|
+
maxFragmentation,
|
|
33
|
+
circularDeps,
|
|
34
|
+
} = params;
|
|
35
|
+
|
|
36
|
+
const issues: string[] = [];
|
|
37
|
+
const recommendations: string[] = [];
|
|
38
|
+
let severity: Severity = Severity.Info;
|
|
39
|
+
let potentialSavings = 0;
|
|
40
|
+
|
|
41
|
+
// Check circular dependencies (CRITICAL)
|
|
42
|
+
if (circularDeps.length > 0) {
|
|
43
|
+
severity = Severity.Critical;
|
|
44
|
+
issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
|
|
45
|
+
recommendations.push(
|
|
46
|
+
'Break circular dependencies by extracting interfaces or using dependency injection'
|
|
47
|
+
);
|
|
48
|
+
potentialSavings += contextBudget * 0.2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check import depth
|
|
52
|
+
if (importDepth > maxDepth * 1.5) {
|
|
53
|
+
severity = Severity.Critical;
|
|
54
|
+
issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
|
|
55
|
+
recommendations.push('Flatten dependency tree or use facade pattern');
|
|
56
|
+
potentialSavings += contextBudget * 0.3;
|
|
57
|
+
} else if (importDepth > maxDepth) {
|
|
58
|
+
if (severity !== Severity.Critical) severity = Severity.Major;
|
|
59
|
+
issues.push(
|
|
60
|
+
`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
|
|
61
|
+
);
|
|
62
|
+
recommendations.push('Consider reducing dependency depth');
|
|
63
|
+
potentialSavings += contextBudget * 0.15;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check context budget
|
|
67
|
+
if (contextBudget > maxContextBudget * 1.5) {
|
|
68
|
+
severity = Severity.Critical;
|
|
69
|
+
issues.push(
|
|
70
|
+
`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
71
|
+
);
|
|
72
|
+
recommendations.push(
|
|
73
|
+
'Split into smaller modules or reduce dependency tree'
|
|
74
|
+
);
|
|
75
|
+
potentialSavings += contextBudget * 0.4;
|
|
76
|
+
} else if (contextBudget > maxContextBudget) {
|
|
77
|
+
if (severity !== Severity.Critical) severity = Severity.Major;
|
|
78
|
+
issues.push(
|
|
79
|
+
`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
80
|
+
);
|
|
81
|
+
recommendations.push('Reduce file size or dependencies');
|
|
82
|
+
potentialSavings += contextBudget * 0.2;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check cohesion
|
|
86
|
+
if (cohesionScore < minCohesion * 0.5) {
|
|
87
|
+
if (severity !== Severity.Critical) severity = Severity.Major;
|
|
88
|
+
issues.push(
|
|
89
|
+
`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
|
|
90
|
+
);
|
|
91
|
+
recommendations.push(
|
|
92
|
+
'Split file by domain - separate unrelated functionality'
|
|
93
|
+
);
|
|
94
|
+
potentialSavings += contextBudget * 0.25;
|
|
95
|
+
} else if (cohesionScore < minCohesion) {
|
|
96
|
+
if (severity === Severity.Info) severity = Severity.Minor;
|
|
97
|
+
issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
|
|
98
|
+
recommendations.push('Consider grouping related exports together');
|
|
99
|
+
potentialSavings += contextBudget * 0.1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check fragmentation
|
|
103
|
+
if (fragmentationScore > maxFragmentation) {
|
|
104
|
+
if (severity === Severity.Info || severity === Severity.Minor)
|
|
105
|
+
severity = Severity.Minor;
|
|
106
|
+
issues.push(
|
|
107
|
+
`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
|
|
108
|
+
);
|
|
109
|
+
recommendations.push('Consolidate with related files in same domain');
|
|
110
|
+
potentialSavings += contextBudget * 0.3;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (issues.length === 0) {
|
|
114
|
+
issues.push('No significant issues detected');
|
|
115
|
+
recommendations.push('File is well-structured for AI context usage');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Detect build artifacts
|
|
119
|
+
if (isBuildArtifact(file)) {
|
|
120
|
+
issues.push('Detected build artifact (bundled/output file)');
|
|
121
|
+
recommendations.push('Exclude build outputs from analysis');
|
|
122
|
+
severity = Severity.Info;
|
|
123
|
+
potentialSavings = 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
severity,
|
|
128
|
+
issues,
|
|
129
|
+
recommendations,
|
|
130
|
+
potentialSavings: Math.floor(potentialSavings),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isBuildArtifact(filePath: string): boolean {
|
|
135
|
+
const lower = filePath.toLowerCase();
|
|
136
|
+
return (
|
|
137
|
+
lower.includes('/node_modules/') ||
|
|
138
|
+
lower.includes('/dist/') ||
|
|
139
|
+
lower.includes('/build/') ||
|
|
140
|
+
lower.includes('/out/') ||
|
|
141
|
+
lower.includes('/.next/')
|
|
142
|
+
);
|
|
143
|
+
}
|
package/src/semantic-analysis.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
DomainSignals,
|
|
6
6
|
ExportInfo,
|
|
7
7
|
} from './types';
|
|
8
|
+
import { singularize } from './utils/string-utils';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Build co-usage matrix: track which files are imported together
|
|
@@ -312,20 +313,6 @@ export function inferDomain(
|
|
|
312
313
|
return 'unknown';
|
|
313
314
|
}
|
|
314
315
|
|
|
315
|
-
function singularize(word: string): string {
|
|
316
|
-
const irregulars: Record<string, string> = {
|
|
317
|
-
people: 'person',
|
|
318
|
-
children: 'child',
|
|
319
|
-
men: 'man',
|
|
320
|
-
women: 'woman',
|
|
321
|
-
};
|
|
322
|
-
if (irregulars[word]) return irregulars[word];
|
|
323
|
-
if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
|
|
324
|
-
if (word.endsWith('ses')) return word.slice(0, -2);
|
|
325
|
-
if (word.endsWith('s') && word.length > 3) return word.slice(0, -1);
|
|
326
|
-
return word;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
316
|
export function getCoUsageData(
|
|
330
317
|
file: string,
|
|
331
318
|
coUsageMatrix: Map<string, Map<string, number>>
|
package/src/summary.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
ContextSummary,
|
|
4
4
|
ModuleCluster,
|
|
5
5
|
} from './types';
|
|
6
|
-
import { calculatePathEntropy, calculateDirectoryDistance } from './
|
|
6
|
+
import { calculatePathEntropy, calculateDirectoryDistance } from './metrics';
|
|
7
7
|
import { GLOBAL_SCAN_OPTIONS } from '@aiready/core';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -11,7 +11,7 @@ import { GLOBAL_SCAN_OPTIONS } from '@aiready/core';
|
|
|
11
11
|
*/
|
|
12
12
|
export function generateSummary(
|
|
13
13
|
results: ContextAnalysisResult[],
|
|
14
|
-
options
|
|
14
|
+
options: any = {}
|
|
15
15
|
): ContextSummary {
|
|
16
16
|
const config = options
|
|
17
17
|
? Object.fromEntries(
|
|
@@ -19,137 +19,92 @@ export function generateSummary(
|
|
|
19
19
|
([key]) => !GLOBAL_SCAN_OPTIONS.includes(key) || key === 'rootDir'
|
|
20
20
|
)
|
|
21
21
|
)
|
|
22
|
-
:
|
|
23
|
-
|
|
24
|
-
if (results.length === 0) {
|
|
25
|
-
return {
|
|
26
|
-
totalFiles: 0,
|
|
27
|
-
totalTokens: 0,
|
|
28
|
-
avgContextBudget: 0,
|
|
29
|
-
maxContextBudget: 0,
|
|
30
|
-
avgImportDepth: 0,
|
|
31
|
-
maxImportDepth: 0,
|
|
32
|
-
deepFiles: [],
|
|
33
|
-
avgFragmentation: 0,
|
|
34
|
-
fragmentedModules: [],
|
|
35
|
-
avgCohesion: 0,
|
|
36
|
-
lowCohesionFiles: [],
|
|
37
|
-
criticalIssues: 0,
|
|
38
|
-
majorIssues: 0,
|
|
39
|
-
minorIssues: 0,
|
|
40
|
-
totalPotentialSavings: 0,
|
|
41
|
-
topExpensiveFiles: [],
|
|
42
|
-
config,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
22
|
+
: {};
|
|
45
23
|
|
|
46
24
|
const totalFiles = results.length;
|
|
47
25
|
const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const avgContextBudget = totalContextBudget / totalFiles;
|
|
53
|
-
const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
|
|
54
|
-
|
|
55
|
-
const avgImportDepth =
|
|
56
|
-
results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
|
|
57
|
-
const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
|
|
26
|
+
const avgContextBudget =
|
|
27
|
+
totalFiles > 0
|
|
28
|
+
? results.reduce((sum, r) => sum + r.contextBudget, 0) / totalFiles
|
|
29
|
+
: 0;
|
|
58
30
|
|
|
31
|
+
// Find deep files
|
|
59
32
|
const deepFiles = results
|
|
60
|
-
.filter((r) => r.importDepth
|
|
61
|
-
.map((r) => ({ file: r.file, depth: r.importDepth }))
|
|
62
|
-
.sort((a, b) => b.depth - a.depth)
|
|
63
|
-
.slice(0, 10);
|
|
33
|
+
.filter((r) => r.importDepth > 5)
|
|
34
|
+
.map((r) => ({ file: r.file, depth: r.importDepth }));
|
|
64
35
|
|
|
65
|
-
const
|
|
66
|
-
results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
|
|
36
|
+
const maxImportDepth = Math.max(0, ...results.map((r) => r.importDepth));
|
|
67
37
|
|
|
38
|
+
// Find fragmented modules (clusters)
|
|
68
39
|
const moduleMap = new Map<string, ContextAnalysisResult[]>();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
40
|
+
results.forEach((r) => {
|
|
41
|
+
const parts = r.file.split('/');
|
|
42
|
+
// Try to identify domain/module (e.g., packages/core, src/utils)
|
|
43
|
+
let domain = 'root';
|
|
44
|
+
if (parts.length > 2) {
|
|
45
|
+
domain = parts.slice(0, 2).join('/');
|
|
73
46
|
}
|
|
74
|
-
|
|
47
|
+
if (!moduleMap.has(domain)) moduleMap.set(domain, []);
|
|
48
|
+
moduleMap.get(domain)!.push(r);
|
|
49
|
+
});
|
|
75
50
|
|
|
76
51
|
const fragmentedModules: ModuleCluster[] = [];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const fragmentationScore =
|
|
80
|
-
files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
|
|
81
|
-
if (fragmentationScore < 0.3) continue;
|
|
82
|
-
|
|
83
|
-
const totalTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
|
|
84
|
-
const avgCohesion =
|
|
85
|
-
files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
|
|
86
|
-
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
87
|
-
|
|
52
|
+
moduleMap.forEach((files, domain) => {
|
|
53
|
+
const clusterTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
|
|
88
54
|
const filePaths = files.map((f) => f.file);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
55
|
+
const avgEntropy = calculatePathEntropy(filePaths);
|
|
56
|
+
|
|
57
|
+
// A module is fragmented if it has many files with high directory distance
|
|
58
|
+
// and relatively low cohesion
|
|
59
|
+
const fragmentationScore = Math.min(1, avgEntropy * (files.length / 10));
|
|
60
|
+
|
|
61
|
+
if (fragmentationScore > 0.4) {
|
|
62
|
+
fragmentedModules.push({
|
|
63
|
+
domain,
|
|
64
|
+
files: filePaths,
|
|
65
|
+
fragmentationScore,
|
|
66
|
+
totalTokens: clusterTokens,
|
|
67
|
+
avgCohesion:
|
|
68
|
+
files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length,
|
|
69
|
+
suggestedStructure: {
|
|
70
|
+
targetFiles: Math.ceil(files.length / 2),
|
|
71
|
+
consolidationPlan: [
|
|
72
|
+
`Consolidate ${files.length} files in ${domain} into fewer modules`,
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
99
76
|
}
|
|
77
|
+
});
|
|
100
78
|
|
|
101
|
-
|
|
102
|
-
let importPairs = 0;
|
|
103
|
-
for (let i = 0; i < files.length; i++) {
|
|
104
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
105
|
-
importSimTotal += jaccard(
|
|
106
|
-
files[i].dependencyList || [],
|
|
107
|
-
files[j].dependencyList || []
|
|
108
|
-
);
|
|
109
|
-
importPairs++;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
79
|
+
fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
112
80
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
totalTokens,
|
|
119
|
-
fragmentationScore,
|
|
120
|
-
avgCohesion,
|
|
121
|
-
importCohesion,
|
|
122
|
-
pathEntropy,
|
|
123
|
-
directoryDistance,
|
|
124
|
-
suggestedStructure: {
|
|
125
|
-
targetFiles,
|
|
126
|
-
consolidationPlan: [
|
|
127
|
-
`Consolidate ${files.length} files across ${new Set(files.map((f) => f.file.split('/').slice(0, -1).join('/'))).size} directories`,
|
|
128
|
-
`Target ~${targetFiles} core modules to reduce context switching`,
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
}
|
|
81
|
+
const avgFragmentation =
|
|
82
|
+
fragmentedModules.length > 0
|
|
83
|
+
? fragmentedModules.reduce((sum, m) => sum + m.fragmentationScore, 0) /
|
|
84
|
+
fragmentedModules.length
|
|
85
|
+
: 0;
|
|
133
86
|
|
|
87
|
+
// Cohesion
|
|
134
88
|
const avgCohesion =
|
|
135
|
-
results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
|
|
89
|
+
results.reduce((sum, r) => sum + r.cohesionScore, 0) / (totalFiles || 1);
|
|
90
|
+
|
|
136
91
|
const lowCohesionFiles = results
|
|
137
92
|
.filter((r) => r.cohesionScore < 0.4)
|
|
138
|
-
.map((r) => ({ file: r.file, score: r.cohesionScore }))
|
|
139
|
-
.sort((a, b) => a.score - b.score)
|
|
140
|
-
.slice(0, 10);
|
|
93
|
+
.map((r) => ({ file: r.file, score: r.cohesionScore }));
|
|
141
94
|
|
|
95
|
+
// Issues
|
|
142
96
|
const criticalIssues = results.filter(
|
|
143
97
|
(r) => r.severity === 'critical'
|
|
144
98
|
).length;
|
|
145
99
|
const majorIssues = results.filter((r) => r.severity === 'major').length;
|
|
146
100
|
const minorIssues = results.filter((r) => r.severity === 'minor').length;
|
|
101
|
+
|
|
147
102
|
const totalPotentialSavings = results.reduce(
|
|
148
|
-
(sum, r) => sum + r.potentialSavings,
|
|
103
|
+
(sum, r) => sum + (r.potentialSavings || 0),
|
|
149
104
|
0
|
|
150
105
|
);
|
|
151
106
|
|
|
152
|
-
const topExpensiveFiles = results
|
|
107
|
+
const topExpensiveFiles = [...results]
|
|
153
108
|
.sort((a, b) => b.contextBudget - a.contextBudget)
|
|
154
109
|
.slice(0, 10)
|
|
155
110
|
.map((r) => ({
|
|
@@ -162,8 +117,9 @@ export function generateSummary(
|
|
|
162
117
|
totalFiles,
|
|
163
118
|
totalTokens,
|
|
164
119
|
avgContextBudget,
|
|
165
|
-
maxContextBudget,
|
|
166
|
-
avgImportDepth
|
|
120
|
+
maxContextBudget: Math.max(0, ...results.map((r) => r.contextBudget)),
|
|
121
|
+
avgImportDepth:
|
|
122
|
+
results.reduce((sum, r) => sum + r.importDepth, 0) / (totalFiles || 1),
|
|
167
123
|
maxImportDepth,
|
|
168
124
|
deepFiles,
|
|
169
125
|
avgFragmentation,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared dependency graph utilities used by context analyzers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function calculateImportDepthFromEdges(
|
|
6
|
+
file: string,
|
|
7
|
+
edges: Map<string, Set<string>>,
|
|
8
|
+
visited = new Set<string>(),
|
|
9
|
+
depth = 0
|
|
10
|
+
): number {
|
|
11
|
+
if (visited.has(file)) return depth;
|
|
12
|
+
|
|
13
|
+
const dependencies = edges.get(file);
|
|
14
|
+
if (!dependencies || dependencies.size === 0) return depth;
|
|
15
|
+
|
|
16
|
+
const nextVisited = new Set(visited);
|
|
17
|
+
nextVisited.add(file);
|
|
18
|
+
|
|
19
|
+
let maxDepth = depth;
|
|
20
|
+
for (const dep of dependencies) {
|
|
21
|
+
maxDepth = Math.max(
|
|
22
|
+
maxDepth,
|
|
23
|
+
calculateImportDepthFromEdges(dep, edges, nextVisited, depth + 1)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return maxDepth;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getTransitiveDependenciesFromEdges(
|
|
31
|
+
file: string,
|
|
32
|
+
edges: Map<string, Set<string>>,
|
|
33
|
+
visited = new Set<string>()
|
|
34
|
+
): string[] {
|
|
35
|
+
if (visited.has(file)) return [];
|
|
36
|
+
|
|
37
|
+
const nextVisited = new Set(visited);
|
|
38
|
+
nextVisited.add(file);
|
|
39
|
+
|
|
40
|
+
const dependencies = edges.get(file);
|
|
41
|
+
if (!dependencies || dependencies.size === 0) return [];
|
|
42
|
+
|
|
43
|
+
const allDeps: string[] = [];
|
|
44
|
+
for (const dep of dependencies) {
|
|
45
|
+
allDeps.push(dep);
|
|
46
|
+
allDeps.push(
|
|
47
|
+
...getTransitiveDependenciesFromEdges(dep, edges, nextVisited)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...new Set(allDeps)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function detectGraphCycles(edges: Map<string, Set<string>>): string[][] {
|
|
55
|
+
const cycles: string[][] = [];
|
|
56
|
+
const visited = new Set<string>();
|
|
57
|
+
const recursionStack = new Set<string>();
|
|
58
|
+
|
|
59
|
+
function dfs(file: string, path: string[]): void {
|
|
60
|
+
if (recursionStack.has(file)) {
|
|
61
|
+
const cycleStart = path.indexOf(file);
|
|
62
|
+
if (cycleStart !== -1) {
|
|
63
|
+
cycles.push([...path.slice(cycleStart), file]);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (visited.has(file)) return;
|
|
69
|
+
|
|
70
|
+
visited.add(file);
|
|
71
|
+
recursionStack.add(file);
|
|
72
|
+
|
|
73
|
+
const dependencies = edges.get(file);
|
|
74
|
+
if (dependencies) {
|
|
75
|
+
for (const dep of dependencies) {
|
|
76
|
+
dfs(dep, [...path, file]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
recursionStack.delete(file);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const file of edges.keys()) {
|
|
84
|
+
if (!visited.has(file)) {
|
|
85
|
+
dfs(file, []);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return cycles;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function detectGraphCyclesFromFile(
|
|
93
|
+
file: string,
|
|
94
|
+
edges: Map<string, Set<string>>
|
|
95
|
+
): string[][] {
|
|
96
|
+
const cycles: string[][] = [];
|
|
97
|
+
const visited = new Set<string>();
|
|
98
|
+
const recursionStack = new Set<string>();
|
|
99
|
+
|
|
100
|
+
function dfs(current: string, path: string[]): void {
|
|
101
|
+
if (recursionStack.has(current)) {
|
|
102
|
+
const cycleStart = path.indexOf(current);
|
|
103
|
+
if (cycleStart !== -1) {
|
|
104
|
+
cycles.push([...path.slice(cycleStart), current]);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (visited.has(current)) return;
|
|
110
|
+
|
|
111
|
+
visited.add(current);
|
|
112
|
+
recursionStack.add(current);
|
|
113
|
+
|
|
114
|
+
const dependencies = edges.get(current);
|
|
115
|
+
if (dependencies) {
|
|
116
|
+
for (const dep of dependencies) {
|
|
117
|
+
dfs(dep, [...path, current]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
recursionStack.delete(current);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
dfs(file, []);
|
|
125
|
+
return cycles;
|
|
126
|
+
}
|