@aiready/context-analyzer 0.21.10 → 0.21.12
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 +11 -11
- package/.turbo/turbo-test.log +28 -28
- package/dist/chunk-BW463GQB.mjs +1767 -0
- package/dist/chunk-CAX2MOUZ.mjs +1801 -0
- package/dist/chunk-J5TA3AZU.mjs +1795 -0
- package/dist/chunk-UXC6QUZ7.mjs +1801 -0
- package/dist/chunk-WTQJNY4U.mjs +1785 -0
- package/dist/chunk-XBFM2Z4O.mjs +1792 -0
- package/dist/cli.js +282 -220
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +3 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +286 -224
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/cluster-detector.test.ts +5 -8
- package/src/__tests__/fragmentation-coupling.test.ts +6 -3
- package/src/analyzer.ts +1 -224
- package/src/classifier.ts +11 -0
- package/src/cluster-detector.ts +22 -5
- package/src/heuristics.ts +150 -81
- package/src/mapper.ts +118 -0
- package/src/metrics.ts +13 -12
- package/src/orchestrator.ts +136 -0
- package/src/remediation.ts +5 -0
- package/src/types.ts +1 -0
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/context-analyzer",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.12",
|
|
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.23.
|
|
52
|
+
"@aiready/core": "0.23.9"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^24.0.0",
|
|
@@ -89,14 +89,11 @@ describe('Cluster Detector', () => {
|
|
|
89
89
|
const clusters = detectModuleClusters(graph);
|
|
90
90
|
|
|
91
91
|
expect(clusters[0].domain).toBe('auth');
|
|
92
|
-
// fragmentation
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
).
|
|
97
|
-
expect(clusters[0].suggestedStructure.consolidationPlan[0]).toContain(
|
|
98
|
-
'Consolidate'
|
|
99
|
-
);
|
|
92
|
+
// fragmentation is reduced because files share imports (coupling discount)
|
|
93
|
+
// and are classified as cohesive modules (single domain 'auth')
|
|
94
|
+
// Final score: ~0.24 (raw 1.0 * 0.8 coupling discount * 0.3 cohesive multiplier)
|
|
95
|
+
expect(clusters[0].fragmentationScore).toBeLessThan(0.5);
|
|
96
|
+
expect(clusters[0].suggestedStructure.consolidationPlan.length).toBe(0); // No consolidation needed when fragmentation is low
|
|
100
97
|
});
|
|
101
98
|
|
|
102
99
|
it('should suggest boundary improvements for large domains', () => {
|
|
@@ -31,8 +31,11 @@ describe('fragmentation coupling discount', () => {
|
|
|
31
31
|
files.map((f) => f.file),
|
|
32
32
|
'billing'
|
|
33
33
|
);
|
|
34
|
-
//
|
|
35
|
-
|
|
34
|
+
// Adjusted fragmentation includes classification multiplier (0.3 for COHESIVE_MODULE)
|
|
35
|
+
const expected = base * 0.3;
|
|
36
|
+
|
|
37
|
+
// Allow small FP tolerance
|
|
38
|
+
expect(cluster!.fragmentationScore).toBeCloseTo(expected, 6);
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
it('applies up-to-20% discount when files share identical imports', () => {
|
|
@@ -60,7 +63,7 @@ describe('fragmentation coupling discount', () => {
|
|
|
60
63
|
files.map((f) => f.file),
|
|
61
64
|
'billing'
|
|
62
65
|
);
|
|
63
|
-
const expected = base * 0.8; // full cohesion => 20% discount
|
|
66
|
+
const expected = base * 0.8 * 0.3; // full cohesion => 20% discount AND 0.3 classification multiplier
|
|
64
67
|
|
|
65
68
|
// Allow small FP tolerance
|
|
66
69
|
expect(cluster!.fragmentationScore).toBeCloseTo(expected, 6);
|
package/src/analyzer.ts
CHANGED
|
@@ -1,224 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type {
|
|
3
|
-
ExportInfo,
|
|
4
|
-
ContextAnalysisResult,
|
|
5
|
-
ContextAnalyzerOptions,
|
|
6
|
-
FileClassification,
|
|
7
|
-
} from './types';
|
|
8
|
-
import { calculateEnhancedCohesion } from './metrics';
|
|
9
|
-
import { analyzeIssues } from './issue-analyzer';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
buildDependencyGraph,
|
|
13
|
-
calculateImportDepth,
|
|
14
|
-
getTransitiveDependencies,
|
|
15
|
-
calculateContextBudget,
|
|
16
|
-
detectCircularDependencies,
|
|
17
|
-
} from './graph-builder';
|
|
18
|
-
import { detectModuleClusters } from './cluster-detector';
|
|
19
|
-
import {
|
|
20
|
-
classifyFile,
|
|
21
|
-
adjustCohesionForClassification,
|
|
22
|
-
adjustFragmentationForClassification,
|
|
23
|
-
} from './classifier';
|
|
24
|
-
import { getClassificationRecommendations } from './remediation';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Calculate cohesion score (how related are exports in a file).
|
|
28
|
-
* Legacy wrapper for backward compatibility with exact test expectations.
|
|
29
|
-
*
|
|
30
|
-
* @param exports - List of exported symbols
|
|
31
|
-
* @param filePath - Path to the file being analyzed
|
|
32
|
-
* @param options - Additional options for cohesion calculation
|
|
33
|
-
* @returns Cohesion score between 0 and 1
|
|
34
|
-
*/
|
|
35
|
-
export function calculateCohesion(
|
|
36
|
-
exports: ExportInfo[],
|
|
37
|
-
filePath?: string,
|
|
38
|
-
options?: any
|
|
39
|
-
): number {
|
|
40
|
-
return calculateEnhancedCohesion(exports, filePath, options);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Analyze AI context window cost for a codebase
|
|
45
|
-
*/
|
|
46
|
-
/**
|
|
47
|
-
* Performs deep context analysis of a project.
|
|
48
|
-
* Scans files, builds a dependency graph, calculates context budgets,
|
|
49
|
-
* and identifies structural issues like high fragmentation or depth.
|
|
50
|
-
*
|
|
51
|
-
* @param options - Analysis parameters including root directory and focus areas
|
|
52
|
-
* @returns Comprehensive analysis results with metrics and identified issues
|
|
53
|
-
*/
|
|
54
|
-
export async function analyzeContext(
|
|
55
|
-
options: ContextAnalyzerOptions
|
|
56
|
-
): Promise<ContextAnalysisResult[]> {
|
|
57
|
-
const {
|
|
58
|
-
maxDepth = 5,
|
|
59
|
-
maxContextBudget = 25000,
|
|
60
|
-
minCohesion = 0.6,
|
|
61
|
-
maxFragmentation = 0.5,
|
|
62
|
-
includeNodeModules = false,
|
|
63
|
-
...scanOptions
|
|
64
|
-
} = options;
|
|
65
|
-
|
|
66
|
-
const files = await scanFiles({
|
|
67
|
-
...scanOptions,
|
|
68
|
-
exclude:
|
|
69
|
-
includeNodeModules && scanOptions.exclude
|
|
70
|
-
? scanOptions.exclude.filter(
|
|
71
|
-
(pattern) => pattern !== '**/node_modules/**'
|
|
72
|
-
)
|
|
73
|
-
: scanOptions.exclude,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith('.py'));
|
|
77
|
-
const fileContents = await Promise.all(
|
|
78
|
-
files.map(async (file) => ({
|
|
79
|
-
file,
|
|
80
|
-
content: await readFileContent(file),
|
|
81
|
-
}))
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const graph = buildDependencyGraph(
|
|
85
|
-
fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
let pythonResults: ContextAnalysisResult[] = [];
|
|
89
|
-
if (pythonFiles.length > 0) {
|
|
90
|
-
const { analyzePythonContext } = await import('./analyzers/python-context');
|
|
91
|
-
const pythonMetrics = await analyzePythonContext(
|
|
92
|
-
pythonFiles,
|
|
93
|
-
scanOptions.rootDir || options.rootDir || '.'
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
pythonResults = pythonMetrics.map((metric) => {
|
|
97
|
-
const { severity, issues, recommendations, potentialSavings } =
|
|
98
|
-
analyzeIssues({
|
|
99
|
-
file: metric.file,
|
|
100
|
-
importDepth: metric.importDepth,
|
|
101
|
-
contextBudget: metric.contextBudget,
|
|
102
|
-
cohesionScore: metric.cohesion,
|
|
103
|
-
fragmentationScore: 0,
|
|
104
|
-
maxDepth,
|
|
105
|
-
maxContextBudget,
|
|
106
|
-
minCohesion,
|
|
107
|
-
maxFragmentation,
|
|
108
|
-
circularDeps: [],
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
file: metric.file,
|
|
113
|
-
tokenCost: 0,
|
|
114
|
-
linesOfCode: 0, // Not provided by python context yet
|
|
115
|
-
importDepth: metric.importDepth,
|
|
116
|
-
dependencyCount: 0, // Not provided
|
|
117
|
-
dependencyList: [],
|
|
118
|
-
circularDeps: [],
|
|
119
|
-
cohesionScore: metric.cohesion,
|
|
120
|
-
domains: [],
|
|
121
|
-
exportCount: 0,
|
|
122
|
-
contextBudget: metric.contextBudget,
|
|
123
|
-
fragmentationScore: 0,
|
|
124
|
-
relatedFiles: [],
|
|
125
|
-
fileClassification: 'unknown' as FileClassification,
|
|
126
|
-
severity,
|
|
127
|
-
issues,
|
|
128
|
-
recommendations,
|
|
129
|
-
potentialSavings,
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const clusters = detectModuleClusters(graph);
|
|
135
|
-
const allCircularDeps = detectCircularDependencies(graph);
|
|
136
|
-
|
|
137
|
-
const results: ContextAnalysisResult[] = Array.from(graph.nodes.values()).map(
|
|
138
|
-
(node) => {
|
|
139
|
-
const file = node.file;
|
|
140
|
-
const tokenCost = node.tokenCost;
|
|
141
|
-
const importDepth = calculateImportDepth(file, graph);
|
|
142
|
-
const transitiveDeps = getTransitiveDependencies(file, graph);
|
|
143
|
-
const contextBudget = calculateContextBudget(file, graph);
|
|
144
|
-
const circularDeps = allCircularDeps.filter((cycle) =>
|
|
145
|
-
cycle.includes(file)
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// Find cluster for this file
|
|
149
|
-
const cluster = clusters.find((c) => c.files.includes(file));
|
|
150
|
-
const rawFragmentationScore = cluster ? cluster.fragmentationScore : 0;
|
|
151
|
-
|
|
152
|
-
// Cohesion
|
|
153
|
-
const rawCohesionScore = calculateEnhancedCohesion(
|
|
154
|
-
node.exports,
|
|
155
|
-
file,
|
|
156
|
-
options as any
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
// Initial classification
|
|
160
|
-
const fileClassification = classifyFile(node, rawCohesionScore);
|
|
161
|
-
|
|
162
|
-
// Adjust scores based on classification
|
|
163
|
-
const cohesionScore = adjustCohesionForClassification(
|
|
164
|
-
rawCohesionScore,
|
|
165
|
-
fileClassification
|
|
166
|
-
);
|
|
167
|
-
const fragmentationScore = adjustFragmentationForClassification(
|
|
168
|
-
rawFragmentationScore,
|
|
169
|
-
fileClassification
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const { severity, issues, recommendations, potentialSavings } =
|
|
173
|
-
analyzeIssues({
|
|
174
|
-
file,
|
|
175
|
-
importDepth,
|
|
176
|
-
contextBudget,
|
|
177
|
-
cohesionScore,
|
|
178
|
-
fragmentationScore,
|
|
179
|
-
maxDepth,
|
|
180
|
-
maxContextBudget,
|
|
181
|
-
minCohesion,
|
|
182
|
-
maxFragmentation,
|
|
183
|
-
circularDeps,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Add classification-specific recommendations
|
|
187
|
-
const classRecs = getClassificationRecommendations(
|
|
188
|
-
fileClassification,
|
|
189
|
-
file,
|
|
190
|
-
issues
|
|
191
|
-
);
|
|
192
|
-
const allRecommendations = Array.from(
|
|
193
|
-
new Set([...recommendations, ...classRecs])
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
file,
|
|
198
|
-
tokenCost,
|
|
199
|
-
linesOfCode: node.linesOfCode,
|
|
200
|
-
importDepth,
|
|
201
|
-
dependencyCount: transitiveDeps.length,
|
|
202
|
-
dependencyList: transitiveDeps,
|
|
203
|
-
circularDeps,
|
|
204
|
-
cohesionScore,
|
|
205
|
-
domains: Array.from(
|
|
206
|
-
new Set(
|
|
207
|
-
node.exports.flatMap((e) => e.domains?.map((d) => d.domain) || [])
|
|
208
|
-
)
|
|
209
|
-
),
|
|
210
|
-
exportCount: node.exports.length,
|
|
211
|
-
contextBudget,
|
|
212
|
-
fragmentationScore,
|
|
213
|
-
relatedFiles: cluster ? cluster.files : [],
|
|
214
|
-
fileClassification,
|
|
215
|
-
severity,
|
|
216
|
-
issues,
|
|
217
|
-
recommendations: allRecommendations,
|
|
218
|
-
potentialSavings,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
return [...results, ...pythonResults];
|
|
224
|
-
}
|
|
1
|
+
export * from './orchestrator';
|
package/src/classifier.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isSessionFile,
|
|
11
11
|
isUtilityModule,
|
|
12
12
|
isConfigFile,
|
|
13
|
+
isHubAndSpokeFile,
|
|
13
14
|
} from './heuristics';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -25,6 +26,7 @@ export const Classification = {
|
|
|
25
26
|
PARSER: 'parser-file' as const,
|
|
26
27
|
COHESIVE_MODULE: 'cohesive-module' as const,
|
|
27
28
|
UTILITY_MODULE: 'utility-module' as const,
|
|
29
|
+
SPOKE_MODULE: 'spoke-module' as const,
|
|
28
30
|
MIXED_CONCERNS: 'mixed-concerns' as const,
|
|
29
31
|
UNKNOWN: 'unknown' as const,
|
|
30
32
|
};
|
|
@@ -95,6 +97,11 @@ export function classifyFile(
|
|
|
95
97
|
return Classification.COHESIVE_MODULE;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
// 11. Detect Spoke modules in monorepo
|
|
101
|
+
if (isHubAndSpokeFile(node)) {
|
|
102
|
+
return Classification.SPOKE_MODULE;
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
// Cohesion and Domain heuristics
|
|
99
106
|
if (domains.length <= 1 && domains[0] !== 'unknown') {
|
|
100
107
|
return Classification.COHESIVE_MODULE;
|
|
@@ -152,6 +159,8 @@ export function adjustCohesionForClassification(
|
|
|
152
159
|
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
153
160
|
case Classification.PARSER:
|
|
154
161
|
return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
|
|
162
|
+
case Classification.SPOKE_MODULE:
|
|
163
|
+
return Math.max(baseCohesion, 0.6);
|
|
155
164
|
case Classification.COHESIVE_MODULE:
|
|
156
165
|
return Math.max(baseCohesion, 0.7);
|
|
157
166
|
case Classification.MIXED_CONCERNS:
|
|
@@ -237,6 +246,8 @@ export function adjustFragmentationForClassification(
|
|
|
237
246
|
case Classification.PARSER:
|
|
238
247
|
case Classification.NEXTJS_PAGE:
|
|
239
248
|
return baseFragmentation * 0.2;
|
|
249
|
+
case Classification.SPOKE_MODULE:
|
|
250
|
+
return baseFragmentation * 0.15; // Heavily discount intentional monorepo separation
|
|
240
251
|
case Classification.COHESIVE_MODULE:
|
|
241
252
|
return baseFragmentation * 0.3;
|
|
242
253
|
case Classification.MIXED_CONCERNS:
|
package/src/cluster-detector.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { DependencyGraph, ModuleCluster } from './types';
|
|
2
2
|
import { calculateFragmentation, calculateEnhancedCohesion } from './metrics';
|
|
3
|
+
import {
|
|
4
|
+
classifyFile,
|
|
5
|
+
adjustFragmentationForClassification,
|
|
6
|
+
} from './classifier';
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Group files by domain to detect module clusters
|
|
@@ -73,29 +77,42 @@ export function detectModuleClusters(
|
|
|
73
77
|
sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
const
|
|
80
|
+
const rawFragmentation = calculateFragmentation(files, domain, {
|
|
77
81
|
...options,
|
|
78
82
|
sharedImportRatio,
|
|
79
83
|
});
|
|
80
84
|
|
|
81
|
-
// Average cohesion for the cluster
|
|
85
|
+
// Average cohesion and adjusted fragmentation for the cluster
|
|
82
86
|
let totalCohesion = 0;
|
|
87
|
+
let totalAdjustedFragmentation = 0;
|
|
88
|
+
|
|
83
89
|
files.forEach((f) => {
|
|
84
90
|
const node = graph.nodes.get(f);
|
|
85
|
-
if (node)
|
|
91
|
+
if (node) {
|
|
92
|
+
const cohesion = calculateEnhancedCohesion(node.exports);
|
|
93
|
+
totalCohesion += cohesion;
|
|
94
|
+
|
|
95
|
+
const classification = classifyFile(node, cohesion);
|
|
96
|
+
totalAdjustedFragmentation += adjustFragmentationForClassification(
|
|
97
|
+
rawFragmentation,
|
|
98
|
+
classification
|
|
99
|
+
);
|
|
100
|
+
}
|
|
86
101
|
});
|
|
102
|
+
|
|
87
103
|
const avgCohesion = totalCohesion / files.length;
|
|
104
|
+
const fragmentationScore = totalAdjustedFragmentation / files.length;
|
|
88
105
|
|
|
89
106
|
clusters.push({
|
|
90
107
|
domain,
|
|
91
108
|
files,
|
|
92
109
|
totalTokens,
|
|
93
|
-
fragmentationScore
|
|
110
|
+
fragmentationScore,
|
|
94
111
|
avgCohesion,
|
|
95
112
|
suggestedStructure: generateSuggestedStructure(
|
|
96
113
|
files,
|
|
97
114
|
totalTokens,
|
|
98
|
-
|
|
115
|
+
fragmentationScore
|
|
99
116
|
),
|
|
100
117
|
});
|
|
101
118
|
}
|