@aiready/context-analyzer 0.19.18 → 0.19.21
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-lint.log +2 -22
- package/.turbo/turbo-test.log +25 -25
- package/coverage/analyzer.ts.html +1369 -0
- package/coverage/ast-utils.ts.html +382 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/classifier.ts.html +1771 -0
- package/coverage/clover.xml +1245 -0
- package/coverage/cluster-detector.ts.html +385 -0
- package/coverage/coverage-final.json +15 -0
- package/coverage/defaults.ts.html +262 -0
- package/coverage/favicon.png +0 -0
- package/coverage/graph-builder.ts.html +859 -0
- package/coverage/index.html +311 -0
- package/coverage/index.ts.html +124 -0
- package/coverage/metrics.ts.html +748 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/provider.ts.html +283 -0
- package/coverage/remediation.ts.html +502 -0
- package/coverage/scoring.ts.html +619 -0
- package/coverage/semantic-analysis.ts.html +1201 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/summary.ts.html +625 -0
- package/coverage/types.ts.html +571 -0
- package/dist/chunk-736QSHJP.mjs +1807 -0
- package/dist/chunk-CCBNKQYB.mjs +1812 -0
- package/dist/chunk-JUHHOSHG.mjs +1808 -0
- package/dist/cli.js +393 -379
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +65 -6
- package/dist/index.d.ts +65 -6
- package/dist/index.js +396 -380
- package/dist/index.mjs +3 -1
- package/package.json +2 -2
- package/src/__tests__/cluster-detector.test.ts +138 -0
- package/src/__tests__/provider.test.ts +78 -0
- package/src/__tests__/remediation.test.ts +94 -0
- package/src/analyzer.ts +244 -1
- package/src/classifier.ts +100 -35
- package/src/index.ts +1 -242
- package/src/provider.ts +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Classification,
|
|
2
3
|
ContextAnalyzerProvider,
|
|
3
4
|
adjustCohesionForClassification,
|
|
4
5
|
adjustFragmentationForClassification,
|
|
@@ -43,8 +44,9 @@ import {
|
|
|
43
44
|
isTypeDefinition,
|
|
44
45
|
isUtilityModule,
|
|
45
46
|
mapScoreToRating
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-CCBNKQYB.mjs";
|
|
47
48
|
export {
|
|
49
|
+
Classification,
|
|
48
50
|
ContextAnalyzerProvider,
|
|
49
51
|
adjustCohesionForClassification,
|
|
50
52
|
adjustFragmentationForClassification,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/context-analyzer",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.21",
|
|
4
4
|
"description": "AI context window cost analysis - detect fragmented code, deep import chains, and expensive context budgets",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"commander": "^14.0.0",
|
|
50
50
|
"chalk": "^5.3.0",
|
|
51
51
|
"prompts": "^2.4.2",
|
|
52
|
-
"@aiready/core": "0.21.
|
|
52
|
+
"@aiready/core": "0.21.21"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^24.0.0",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { detectModuleClusters } from '../cluster-detector';
|
|
3
|
+
import { DependencyGraph } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('Cluster Detector', () => {
|
|
6
|
+
it('should group files by domain into clusters', () => {
|
|
7
|
+
const graph: DependencyGraph = {
|
|
8
|
+
nodes: new Map([
|
|
9
|
+
[
|
|
10
|
+
'src/auth/login.ts',
|
|
11
|
+
{
|
|
12
|
+
file: 'src/auth/login.ts',
|
|
13
|
+
imports: [],
|
|
14
|
+
exports: [
|
|
15
|
+
{ name: 'login', type: 'function', inferredDomain: 'auth' },
|
|
16
|
+
],
|
|
17
|
+
tokenCost: 1000,
|
|
18
|
+
linesOfCode: 50,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
'src/auth/logout.ts',
|
|
23
|
+
{
|
|
24
|
+
file: 'src/auth/logout.ts',
|
|
25
|
+
imports: [],
|
|
26
|
+
exports: [
|
|
27
|
+
{ name: 'logout', type: 'function', inferredDomain: 'auth' },
|
|
28
|
+
],
|
|
29
|
+
tokenCost: 500,
|
|
30
|
+
linesOfCode: 20,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
'src/user/profile.ts',
|
|
35
|
+
{
|
|
36
|
+
file: 'src/user/profile.ts',
|
|
37
|
+
imports: [],
|
|
38
|
+
exports: [
|
|
39
|
+
{ name: 'getProfile', type: 'function', inferredDomain: 'user' },
|
|
40
|
+
],
|
|
41
|
+
tokenCost: 2000,
|
|
42
|
+
linesOfCode: 100,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
]),
|
|
46
|
+
edges: new Map(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const clusters = detectModuleClusters(graph);
|
|
50
|
+
|
|
51
|
+
// Should find 'auth' cluster (2 files), but skip 'user' (only 1 file)
|
|
52
|
+
expect(clusters.length).toBe(1);
|
|
53
|
+
expect(clusters[0].domain).toBe('auth');
|
|
54
|
+
expect(clusters[0].files).toHaveLength(2);
|
|
55
|
+
expect(clusters[0].totalTokens).toBe(1500);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should calculate fragmentation and suggest consolidation', () => {
|
|
59
|
+
const graph: DependencyGraph = {
|
|
60
|
+
nodes: new Map([
|
|
61
|
+
[
|
|
62
|
+
'src/auth/login.ts',
|
|
63
|
+
{
|
|
64
|
+
file: 'src/auth/login.ts',
|
|
65
|
+
imports: ['lib/common.ts'],
|
|
66
|
+
exports: [
|
|
67
|
+
{ name: 'login', type: 'function', inferredDomain: 'auth' },
|
|
68
|
+
],
|
|
69
|
+
tokenCost: 1000,
|
|
70
|
+
linesOfCode: 50,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
'src/utils/auth-helper.ts',
|
|
75
|
+
{
|
|
76
|
+
file: 'src/utils/auth-helper.ts',
|
|
77
|
+
imports: ['lib/common.ts'],
|
|
78
|
+
exports: [
|
|
79
|
+
{ name: 'helper', type: 'function', inferredDomain: 'auth' },
|
|
80
|
+
],
|
|
81
|
+
tokenCost: 500,
|
|
82
|
+
linesOfCode: 20,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
]),
|
|
86
|
+
edges: new Map(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const clusters = detectModuleClusters(graph);
|
|
90
|
+
|
|
91
|
+
expect(clusters[0].domain).toBe('auth');
|
|
92
|
+
// fragmentation should be high because they are in different directories
|
|
93
|
+
expect(clusters[0].fragmentationScore).toBeGreaterThan(0.5);
|
|
94
|
+
expect(
|
|
95
|
+
clusters[0].suggestedStructure.consolidationPlan.length
|
|
96
|
+
).toBeGreaterThan(0);
|
|
97
|
+
expect(clusters[0].suggestedStructure.consolidationPlan[0]).toContain(
|
|
98
|
+
'Consolidate'
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should suggest boundary improvements for large domains', () => {
|
|
103
|
+
const graph: DependencyGraph = {
|
|
104
|
+
nodes: new Map([
|
|
105
|
+
[
|
|
106
|
+
'src/big/part1.ts',
|
|
107
|
+
{
|
|
108
|
+
file: 'src/big/part1.ts',
|
|
109
|
+
imports: [],
|
|
110
|
+
exports: [{ name: 'p1', type: 'function', inferredDomain: 'big' }],
|
|
111
|
+
tokenCost: 15000,
|
|
112
|
+
linesOfCode: 500,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
'src/big/part2.ts',
|
|
117
|
+
{
|
|
118
|
+
file: 'src/big/part2.ts',
|
|
119
|
+
imports: [],
|
|
120
|
+
exports: [{ name: 'p2', type: 'function', inferredDomain: 'big' }],
|
|
121
|
+
tokenCost: 10000,
|
|
122
|
+
linesOfCode: 400,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
]),
|
|
126
|
+
edges: new Map(),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const clusters = detectModuleClusters(graph);
|
|
130
|
+
|
|
131
|
+
expect(clusters[0].totalTokens).toBe(25000);
|
|
132
|
+
expect(
|
|
133
|
+
clusters[0].suggestedStructure.consolidationPlan.some((p) =>
|
|
134
|
+
p.includes('Ensure clear sub-domain boundaries')
|
|
135
|
+
)
|
|
136
|
+
).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { ContextAnalyzerProvider } from '../provider';
|
|
3
|
+
import * as analyzer from '../analyzer';
|
|
4
|
+
import * as summary from '../summary';
|
|
5
|
+
|
|
6
|
+
vi.mock('../analyzer', async () => {
|
|
7
|
+
const actual = await vi.importActual('../analyzer');
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
analyzeContext: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
vi.mock('../summary', async () => {
|
|
15
|
+
const actual = await vi.importActual('../summary');
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
generateSummary: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('Context Analyzer Provider', () => {
|
|
23
|
+
it('should analyze and return SpokeOutput', async () => {
|
|
24
|
+
vi.mocked(analyzer.analyzeContext).mockResolvedValue([
|
|
25
|
+
{
|
|
26
|
+
file: 'file1.ts',
|
|
27
|
+
issues: ['issue'],
|
|
28
|
+
severity: 'major',
|
|
29
|
+
recommendations: ['fix'],
|
|
30
|
+
tokenCost: 100,
|
|
31
|
+
importDepth: 2,
|
|
32
|
+
contextBudget: 500,
|
|
33
|
+
cohesionScore: 0.8,
|
|
34
|
+
fragmentationScore: 0.1,
|
|
35
|
+
dependencyCount: 5,
|
|
36
|
+
dependencyList: [],
|
|
37
|
+
circularDeps: [],
|
|
38
|
+
domains: [],
|
|
39
|
+
exportCount: 1,
|
|
40
|
+
relatedFiles: [],
|
|
41
|
+
fileClassification: 'unknown',
|
|
42
|
+
potentialSavings: 0,
|
|
43
|
+
linesOfCode: 50,
|
|
44
|
+
} as any,
|
|
45
|
+
]);
|
|
46
|
+
vi.mocked(summary.generateSummary).mockReturnValue({
|
|
47
|
+
totalFiles: 1,
|
|
48
|
+
} as any);
|
|
49
|
+
|
|
50
|
+
const output = await ContextAnalyzerProvider.analyze({ rootDir: '.' });
|
|
51
|
+
|
|
52
|
+
expect(output.summary.totalFiles).toBe(1);
|
|
53
|
+
expect(output.results[0].fileName).toBe('file1.ts');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should score an output', () => {
|
|
57
|
+
const mockOutput = {
|
|
58
|
+
summary: {
|
|
59
|
+
score: 80,
|
|
60
|
+
avgContextBudget: 1000,
|
|
61
|
+
maxContextBudget: 5000,
|
|
62
|
+
avgImportDepth: 3,
|
|
63
|
+
maxImportDepth: 5,
|
|
64
|
+
avgFragmentation: 0.2,
|
|
65
|
+
criticalIssues: 0,
|
|
66
|
+
majorIssues: 0,
|
|
67
|
+
totalFiles: 10,
|
|
68
|
+
} as any,
|
|
69
|
+
results: [],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const scoring = ContextAnalyzerProvider.score(mockOutput as any, {
|
|
73
|
+
rootDir: '.',
|
|
74
|
+
});
|
|
75
|
+
expect(scoring.score).toBeDefined();
|
|
76
|
+
expect(scoring.toolName).toBe('context-analyzer');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
getClassificationRecommendations,
|
|
4
|
+
getGeneralRecommendations,
|
|
5
|
+
} from '../remediation';
|
|
6
|
+
|
|
7
|
+
describe('Remediation Logic', () => {
|
|
8
|
+
describe('getClassificationRecommendations', () => {
|
|
9
|
+
it('should return specific recommendations for barrel exports', () => {
|
|
10
|
+
const recs = getClassificationRecommendations(
|
|
11
|
+
'barrel-export',
|
|
12
|
+
'index.ts',
|
|
13
|
+
[]
|
|
14
|
+
);
|
|
15
|
+
expect(recs[0]).toContain('Barrel export file detected');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should return specific recommendations for type definitions', () => {
|
|
19
|
+
const recs = getClassificationRecommendations(
|
|
20
|
+
'type-definition',
|
|
21
|
+
'types.ts',
|
|
22
|
+
[]
|
|
23
|
+
);
|
|
24
|
+
expect(recs[0]).toContain('Type definition file');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return general issues if classification is unknown', () => {
|
|
28
|
+
const issues = ['High complexity'];
|
|
29
|
+
const recs = getClassificationRecommendations(
|
|
30
|
+
'unknown',
|
|
31
|
+
'file.ts',
|
|
32
|
+
issues
|
|
33
|
+
);
|
|
34
|
+
expect(recs).toEqual(issues);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getGeneralRecommendations', () => {
|
|
39
|
+
const thresholds = {
|
|
40
|
+
maxContextBudget: 10000,
|
|
41
|
+
maxDepth: 5,
|
|
42
|
+
minCohesion: 0.6,
|
|
43
|
+
maxFragmentation: 0.5,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
it('should identify high context budget as major severity', () => {
|
|
47
|
+
const result = getGeneralRecommendations(
|
|
48
|
+
{
|
|
49
|
+
contextBudget: 15000,
|
|
50
|
+
importDepth: 3,
|
|
51
|
+
circularDeps: [],
|
|
52
|
+
cohesionScore: 0.8,
|
|
53
|
+
fragmentationScore: 0.2,
|
|
54
|
+
},
|
|
55
|
+
thresholds
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(result.severity).toBe('major');
|
|
59
|
+
expect(result.issues[0]).toContain('High context budget');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should identify circular dependencies as critical severity', () => {
|
|
63
|
+
const result = getGeneralRecommendations(
|
|
64
|
+
{
|
|
65
|
+
contextBudget: 5000,
|
|
66
|
+
importDepth: 3,
|
|
67
|
+
circularDeps: [['a.ts', 'b.ts', 'a.ts']],
|
|
68
|
+
cohesionScore: 0.8,
|
|
69
|
+
fragmentationScore: 0.2,
|
|
70
|
+
},
|
|
71
|
+
thresholds
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(result.severity).toBe('critical');
|
|
75
|
+
expect(result.recommendations[0]).toContain('circular imports');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should identify low cohesion as minor severity', () => {
|
|
79
|
+
const result = getGeneralRecommendations(
|
|
80
|
+
{
|
|
81
|
+
contextBudget: 5000,
|
|
82
|
+
importDepth: 3,
|
|
83
|
+
circularDeps: [],
|
|
84
|
+
cohesionScore: 0.4,
|
|
85
|
+
fragmentationScore: 0.2,
|
|
86
|
+
},
|
|
87
|
+
thresholds
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(result.severity).toBe('minor');
|
|
91
|
+
expect(result.issues[0]).toContain('Low cohesion score');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/analyzer.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
estimateTokens,
|
|
3
|
+
Severity,
|
|
4
|
+
scanFiles,
|
|
5
|
+
readFileContent,
|
|
6
|
+
} from '@aiready/core';
|
|
2
7
|
import type {
|
|
3
8
|
DependencyGraph,
|
|
4
9
|
DependencyNode,
|
|
@@ -6,15 +11,34 @@ import type {
|
|
|
6
11
|
ModuleCluster,
|
|
7
12
|
FileClassification,
|
|
8
13
|
ContextAnalysisResult,
|
|
14
|
+
ContextAnalyzerOptions,
|
|
15
|
+
ContextSummary,
|
|
9
16
|
} from './types';
|
|
10
17
|
import { calculateEnhancedCohesion } from './metrics';
|
|
11
18
|
import { isTestFile } from './ast-utils';
|
|
19
|
+
import { calculateContextScore } from './scoring';
|
|
20
|
+
import { getSmartDefaults } from './defaults';
|
|
21
|
+
import { generateSummary } from './summary';
|
|
12
22
|
|
|
13
23
|
export * from './graph-builder';
|
|
14
24
|
export * from './metrics';
|
|
15
25
|
export * from './classifier';
|
|
16
26
|
export * from './cluster-detector';
|
|
17
27
|
export * from './remediation';
|
|
28
|
+
import {
|
|
29
|
+
buildDependencyGraph,
|
|
30
|
+
calculateImportDepth,
|
|
31
|
+
getTransitiveDependencies,
|
|
32
|
+
calculateContextBudget,
|
|
33
|
+
detectCircularDependencies,
|
|
34
|
+
} from './graph-builder';
|
|
35
|
+
import { detectModuleClusters } from './cluster-detector';
|
|
36
|
+
import {
|
|
37
|
+
classifyFile,
|
|
38
|
+
adjustCohesionForClassification,
|
|
39
|
+
adjustFragmentationForClassification,
|
|
40
|
+
} from './classifier';
|
|
41
|
+
import { getClassificationRecommendations } from './remediation';
|
|
18
42
|
|
|
19
43
|
/**
|
|
20
44
|
* Calculate cohesion score (how related are exports in a file)
|
|
@@ -184,3 +208,222 @@ function isBuildArtifact(filePath: string): boolean {
|
|
|
184
208
|
lower.includes('/.next/')
|
|
185
209
|
);
|
|
186
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Analyze AI context window cost for a codebase
|
|
214
|
+
*/
|
|
215
|
+
export async function analyzeContext(
|
|
216
|
+
options: ContextAnalyzerOptions
|
|
217
|
+
): Promise<ContextAnalysisResult[]> {
|
|
218
|
+
const {
|
|
219
|
+
maxDepth = 5,
|
|
220
|
+
maxContextBudget = 10000,
|
|
221
|
+
minCohesion = 0.6,
|
|
222
|
+
maxFragmentation = 0.5,
|
|
223
|
+
focus = 'all',
|
|
224
|
+
includeNodeModules = false,
|
|
225
|
+
...scanOptions
|
|
226
|
+
} = options;
|
|
227
|
+
|
|
228
|
+
const files = await scanFiles({
|
|
229
|
+
...scanOptions,
|
|
230
|
+
exclude:
|
|
231
|
+
includeNodeModules && scanOptions.exclude
|
|
232
|
+
? scanOptions.exclude.filter(
|
|
233
|
+
(pattern) => pattern !== '**/node_modules/**'
|
|
234
|
+
)
|
|
235
|
+
: scanOptions.exclude,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith('.py'));
|
|
239
|
+
const fileContents = await Promise.all(
|
|
240
|
+
files.map(async (file) => ({
|
|
241
|
+
file,
|
|
242
|
+
content: await readFileContent(file),
|
|
243
|
+
}))
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const graph = buildDependencyGraph(
|
|
247
|
+
fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
let pythonResults: ContextAnalysisResult[] = [];
|
|
251
|
+
if (pythonFiles.length > 0) {
|
|
252
|
+
const { analyzePythonContext } = await import('./analyzers/python-context');
|
|
253
|
+
const pythonMetrics = await analyzePythonContext(
|
|
254
|
+
pythonFiles,
|
|
255
|
+
scanOptions.rootDir || options.rootDir || '.'
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
pythonResults = pythonMetrics.map((metric) => {
|
|
259
|
+
const { severity, issues, recommendations, potentialSavings } =
|
|
260
|
+
analyzeIssues({
|
|
261
|
+
file: metric.file,
|
|
262
|
+
importDepth: metric.importDepth,
|
|
263
|
+
contextBudget: metric.contextBudget,
|
|
264
|
+
cohesionScore: metric.cohesion,
|
|
265
|
+
fragmentationScore: 0,
|
|
266
|
+
maxDepth,
|
|
267
|
+
maxContextBudget,
|
|
268
|
+
minCohesion,
|
|
269
|
+
maxFragmentation,
|
|
270
|
+
circularDeps: metric.metrics.circularDependencies.map((cycle) =>
|
|
271
|
+
cycle.split(' → ')
|
|
272
|
+
),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
file: metric.file,
|
|
277
|
+
tokenCost: Math.floor(
|
|
278
|
+
metric.contextBudget / (1 + metric.imports.length || 1)
|
|
279
|
+
),
|
|
280
|
+
linesOfCode: metric.metrics.linesOfCode,
|
|
281
|
+
importDepth: metric.importDepth,
|
|
282
|
+
dependencyCount: metric.imports.length,
|
|
283
|
+
dependencyList: metric.imports.map(
|
|
284
|
+
(imp) => imp.resolvedPath || imp.source
|
|
285
|
+
),
|
|
286
|
+
circularDeps: metric.metrics.circularDependencies.map((cycle) =>
|
|
287
|
+
cycle.split(' → ')
|
|
288
|
+
),
|
|
289
|
+
cohesionScore: metric.cohesion,
|
|
290
|
+
domains: ['python'],
|
|
291
|
+
exportCount: metric.exports.length,
|
|
292
|
+
contextBudget: metric.contextBudget,
|
|
293
|
+
fragmentationScore: 0,
|
|
294
|
+
relatedFiles: [],
|
|
295
|
+
fileClassification: 'unknown' as const,
|
|
296
|
+
severity,
|
|
297
|
+
issues,
|
|
298
|
+
recommendations,
|
|
299
|
+
potentialSavings,
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const circularDeps = detectCircularDependencies(graph);
|
|
305
|
+
const useLogScale = files.length >= 500;
|
|
306
|
+
const clusters = detectModuleClusters(graph, { useLogScale });
|
|
307
|
+
const fragmentationMap = new Map<string, number>();
|
|
308
|
+
for (const cluster of clusters) {
|
|
309
|
+
for (const file of cluster.files) {
|
|
310
|
+
fragmentationMap.set(file, cluster.fragmentationScore);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const results: ContextAnalysisResult[] = [];
|
|
315
|
+
|
|
316
|
+
for (const { file } of fileContents) {
|
|
317
|
+
const node = graph.nodes.get(file);
|
|
318
|
+
if (!node) continue;
|
|
319
|
+
|
|
320
|
+
const importDepth =
|
|
321
|
+
focus === 'depth' || focus === 'all'
|
|
322
|
+
? calculateImportDepth(file, graph)
|
|
323
|
+
: 0;
|
|
324
|
+
const dependencyList =
|
|
325
|
+
focus === 'depth' || focus === 'all'
|
|
326
|
+
? getTransitiveDependencies(file, graph)
|
|
327
|
+
: [];
|
|
328
|
+
const contextBudget =
|
|
329
|
+
focus === 'all' ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
330
|
+
const cohesionScore =
|
|
331
|
+
focus === 'cohesion' || focus === 'all'
|
|
332
|
+
? calculateCohesion(node.exports, file, {
|
|
333
|
+
coUsageMatrix: graph.coUsageMatrix,
|
|
334
|
+
})
|
|
335
|
+
: 1;
|
|
336
|
+
|
|
337
|
+
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
338
|
+
const relatedFiles: string[] = [];
|
|
339
|
+
for (const cluster of clusters) {
|
|
340
|
+
if (cluster.files.includes(file)) {
|
|
341
|
+
relatedFiles.push(...cluster.files.filter((f) => f !== file));
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const { issues } = analyzeIssues({
|
|
347
|
+
file,
|
|
348
|
+
importDepth,
|
|
349
|
+
contextBudget,
|
|
350
|
+
cohesionScore,
|
|
351
|
+
fragmentationScore,
|
|
352
|
+
maxDepth,
|
|
353
|
+
maxContextBudget,
|
|
354
|
+
minCohesion,
|
|
355
|
+
maxFragmentation,
|
|
356
|
+
circularDeps,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const domains = [
|
|
360
|
+
...new Set(node.exports.map((e) => e.inferredDomain || 'unknown')),
|
|
361
|
+
];
|
|
362
|
+
const fileClassification = classifyFile(node);
|
|
363
|
+
const adjustedCohesionScore = adjustCohesionForClassification(
|
|
364
|
+
cohesionScore,
|
|
365
|
+
fileClassification,
|
|
366
|
+
node
|
|
367
|
+
);
|
|
368
|
+
const adjustedFragmentationScore = adjustFragmentationForClassification(
|
|
369
|
+
fragmentationScore,
|
|
370
|
+
fileClassification
|
|
371
|
+
);
|
|
372
|
+
const classificationRecommendations = getClassificationRecommendations(
|
|
373
|
+
fileClassification,
|
|
374
|
+
file,
|
|
375
|
+
issues
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const {
|
|
379
|
+
severity: adjustedSeverity,
|
|
380
|
+
issues: adjustedIssues,
|
|
381
|
+
recommendations: finalRecommendations,
|
|
382
|
+
potentialSavings: adjustedSavings,
|
|
383
|
+
} = analyzeIssues({
|
|
384
|
+
file,
|
|
385
|
+
importDepth,
|
|
386
|
+
contextBudget,
|
|
387
|
+
cohesionScore: adjustedCohesionScore,
|
|
388
|
+
fragmentationScore: adjustedFragmentationScore,
|
|
389
|
+
maxDepth,
|
|
390
|
+
maxContextBudget,
|
|
391
|
+
minCohesion,
|
|
392
|
+
maxFragmentation,
|
|
393
|
+
circularDeps,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
results.push({
|
|
397
|
+
file,
|
|
398
|
+
tokenCost: node.tokenCost,
|
|
399
|
+
linesOfCode: node.linesOfCode,
|
|
400
|
+
importDepth,
|
|
401
|
+
dependencyCount: dependencyList.length,
|
|
402
|
+
dependencyList,
|
|
403
|
+
circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
|
|
404
|
+
cohesionScore: adjustedCohesionScore,
|
|
405
|
+
domains,
|
|
406
|
+
exportCount: node.exports.length,
|
|
407
|
+
contextBudget,
|
|
408
|
+
fragmentationScore: adjustedFragmentationScore,
|
|
409
|
+
relatedFiles,
|
|
410
|
+
fileClassification,
|
|
411
|
+
severity: adjustedSeverity,
|
|
412
|
+
issues: adjustedIssues,
|
|
413
|
+
recommendations: [
|
|
414
|
+
...finalRecommendations,
|
|
415
|
+
...classificationRecommendations.slice(0, 1),
|
|
416
|
+
],
|
|
417
|
+
potentialSavings: adjustedSavings,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const allResults = [...results, ...pythonResults];
|
|
422
|
+
const finalSummary = generateSummary(allResults, options);
|
|
423
|
+
return allResults.sort((a, b) => {
|
|
424
|
+
const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
|
|
425
|
+
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
426
|
+
if (severityDiff !== 0) return severityDiff;
|
|
427
|
+
return b.contextBudget - a.contextBudget;
|
|
428
|
+
});
|
|
429
|
+
}
|