@aiready/context-analyzer 0.1.0

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/src/index.ts ADDED
@@ -0,0 +1,396 @@
1
+ import { scanFiles, readFileContent } from '@aiready/core';
2
+ import type { ScanOptions } from '@aiready/core';
3
+ import {
4
+ buildDependencyGraph,
5
+ calculateImportDepth,
6
+ getTransitiveDependencies,
7
+ calculateContextBudget,
8
+ detectCircularDependencies,
9
+ calculateCohesion,
10
+ calculateFragmentation,
11
+ detectModuleClusters,
12
+ } from './analyzer';
13
+ import type {
14
+ ContextAnalyzerOptions,
15
+ ContextAnalysisResult,
16
+ ContextSummary,
17
+ ModuleCluster,
18
+ } from './types';
19
+
20
+ export type { ContextAnalyzerOptions, ContextAnalysisResult, ContextSummary, ModuleCluster };
21
+
22
+ /**
23
+ * Analyze AI context window cost for a codebase
24
+ */
25
+ export async function analyzeContext(
26
+ options: ContextAnalyzerOptions
27
+ ): Promise<ContextAnalysisResult[]> {
28
+ const {
29
+ maxDepth = 5,
30
+ maxContextBudget = 10000,
31
+ minCohesion = 0.6,
32
+ maxFragmentation = 0.5,
33
+ focus = 'all',
34
+ includeNodeModules = false,
35
+ ...scanOptions
36
+ } = options;
37
+
38
+ // Scan files
39
+ const files = await scanFiles({
40
+ ...scanOptions,
41
+ exclude: includeNodeModules
42
+ ? scanOptions.exclude
43
+ : [...(scanOptions.exclude || []), '**/node_modules/**'],
44
+ });
45
+
46
+ // Read all file contents
47
+ const fileContents = await Promise.all(
48
+ files.map(async (file) => ({
49
+ file,
50
+ content: await readFileContent(file),
51
+ }))
52
+ );
53
+
54
+ // Build dependency graph
55
+ const graph = buildDependencyGraph(fileContents);
56
+
57
+ // Detect circular dependencies
58
+ const circularDeps = detectCircularDependencies(graph);
59
+
60
+ // Detect module clusters for fragmentation analysis
61
+ const clusters = detectModuleClusters(graph);
62
+ const fragmentationMap = new Map<string, number>();
63
+ for (const cluster of clusters) {
64
+ for (const file of cluster.files) {
65
+ fragmentationMap.set(file, cluster.fragmentationScore);
66
+ }
67
+ }
68
+
69
+ // Analyze each file
70
+ const results: ContextAnalysisResult[] = [];
71
+
72
+ for (const { file } of fileContents) {
73
+ const node = graph.nodes.get(file);
74
+ if (!node) continue;
75
+
76
+ // Calculate metrics based on focus
77
+ const importDepth =
78
+ focus === 'depth' || focus === 'all'
79
+ ? calculateImportDepth(file, graph)
80
+ : 0;
81
+
82
+ const dependencyList =
83
+ focus === 'depth' || focus === 'all'
84
+ ? getTransitiveDependencies(file, graph)
85
+ : [];
86
+
87
+ const contextBudget =
88
+ focus === 'all' ? calculateContextBudget(file, graph) : node.tokenCost;
89
+
90
+ const cohesionScore =
91
+ focus === 'cohesion' || focus === 'all'
92
+ ? calculateCohesion(node.exports)
93
+ : 1;
94
+
95
+ const fragmentationScore = fragmentationMap.get(file) || 0;
96
+
97
+ // Find related files (files in same domain cluster)
98
+ const relatedFiles: string[] = [];
99
+ for (const cluster of clusters) {
100
+ if (cluster.files.includes(file)) {
101
+ relatedFiles.push(...cluster.files.filter((f) => f !== file));
102
+ break;
103
+ }
104
+ }
105
+
106
+ // Determine severity and generate issues/recommendations
107
+ const { severity, issues, recommendations, potentialSavings } =
108
+ analyzeIssues({
109
+ file,
110
+ importDepth,
111
+ contextBudget,
112
+ cohesionScore,
113
+ fragmentationScore,
114
+ maxDepth,
115
+ maxContextBudget,
116
+ minCohesion,
117
+ maxFragmentation,
118
+ circularDeps,
119
+ });
120
+
121
+ // Get domains from exports
122
+ const domains = [
123
+ ...new Set(node.exports.map((e) => e.inferredDomain || 'unknown')),
124
+ ];
125
+
126
+ results.push({
127
+ file,
128
+ tokenCost: node.tokenCost,
129
+ linesOfCode: node.linesOfCode,
130
+ importDepth,
131
+ dependencyCount: dependencyList.length,
132
+ dependencyList,
133
+ circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
134
+ cohesionScore,
135
+ domains,
136
+ exportCount: node.exports.length,
137
+ contextBudget,
138
+ fragmentationScore,
139
+ relatedFiles,
140
+ severity,
141
+ issues,
142
+ recommendations,
143
+ potentialSavings,
144
+ });
145
+ }
146
+
147
+ // Sort by severity and context budget
148
+ return results.sort((a, b) => {
149
+ const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
150
+ const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
151
+ if (severityDiff !== 0) return severityDiff;
152
+ return b.contextBudget - a.contextBudget;
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Generate summary of context analysis results
158
+ */
159
+ export function generateSummary(
160
+ results: ContextAnalysisResult[]
161
+ ): ContextSummary {
162
+ if (results.length === 0) {
163
+ return {
164
+ totalFiles: 0,
165
+ totalTokens: 0,
166
+ avgContextBudget: 0,
167
+ maxContextBudget: 0,
168
+ avgImportDepth: 0,
169
+ maxImportDepth: 0,
170
+ deepFiles: [],
171
+ avgFragmentation: 0,
172
+ fragmentedModules: [],
173
+ avgCohesion: 0,
174
+ lowCohesionFiles: [],
175
+ criticalIssues: 0,
176
+ majorIssues: 0,
177
+ minorIssues: 0,
178
+ totalPotentialSavings: 0,
179
+ topExpensiveFiles: [],
180
+ };
181
+ }
182
+
183
+ const totalFiles = results.length;
184
+ const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
185
+ const totalContextBudget = results.reduce(
186
+ (sum, r) => sum + r.contextBudget,
187
+ 0
188
+ );
189
+ const avgContextBudget = totalContextBudget / totalFiles;
190
+ const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
191
+
192
+ const avgImportDepth =
193
+ results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
194
+ const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
195
+
196
+ const deepFiles = results
197
+ .filter((r) => r.importDepth >= 5)
198
+ .map((r) => ({ file: r.file, depth: r.importDepth }))
199
+ .sort((a, b) => b.depth - a.depth)
200
+ .slice(0, 10);
201
+
202
+ const avgFragmentation =
203
+ results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
204
+
205
+ // Get unique module clusters
206
+ const moduleMap = new Map<string, ContextAnalysisResult[]>();
207
+ for (const result of results) {
208
+ for (const domain of result.domains) {
209
+ if (!moduleMap.has(domain)) {
210
+ moduleMap.set(domain, []);
211
+ }
212
+ moduleMap.get(domain)!.push(result);
213
+ }
214
+ }
215
+
216
+ const fragmentedModules: ModuleCluster[] = [];
217
+ for (const [domain, files] of moduleMap.entries()) {
218
+ if (files.length < 2) continue;
219
+
220
+ const fragmentationScore =
221
+ files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
222
+ if (fragmentationScore < 0.3) continue; // Skip well-organized modules
223
+
224
+ const totalTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
225
+ const avgCohesion =
226
+ files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
227
+ const targetFiles = Math.max(1, Math.ceil(files.length / 3));
228
+
229
+ fragmentedModules.push({
230
+ domain,
231
+ files: files.map((f) => f.file),
232
+ totalTokens,
233
+ fragmentationScore,
234
+ avgCohesion,
235
+ suggestedStructure: {
236
+ targetFiles,
237
+ consolidationPlan: [
238
+ `Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s)`,
239
+ `Current token cost: ${totalTokens.toLocaleString()}`,
240
+ `Estimated savings: ${Math.floor(totalTokens * 0.3).toLocaleString()} tokens (30%)`,
241
+ ],
242
+ },
243
+ });
244
+ }
245
+
246
+ fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
247
+
248
+ const avgCohesion =
249
+ results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
250
+
251
+ const lowCohesionFiles = results
252
+ .filter((r) => r.cohesionScore < 0.6)
253
+ .map((r) => ({ file: r.file, score: r.cohesionScore }))
254
+ .sort((a, b) => a.score - b.score)
255
+ .slice(0, 10);
256
+
257
+ const criticalIssues = results.filter((r) => r.severity === 'critical').length;
258
+ const majorIssues = results.filter((r) => r.severity === 'major').length;
259
+ const minorIssues = results.filter((r) => r.severity === 'minor').length;
260
+
261
+ const totalPotentialSavings = results.reduce(
262
+ (sum, r) => sum + r.potentialSavings,
263
+ 0
264
+ );
265
+
266
+ const topExpensiveFiles = results
267
+ .sort((a, b) => b.contextBudget - a.contextBudget)
268
+ .slice(0, 10)
269
+ .map((r) => ({
270
+ file: r.file,
271
+ contextBudget: r.contextBudget,
272
+ severity: r.severity,
273
+ }));
274
+
275
+ return {
276
+ totalFiles,
277
+ totalTokens,
278
+ avgContextBudget,
279
+ maxContextBudget,
280
+ avgImportDepth,
281
+ maxImportDepth,
282
+ deepFiles,
283
+ avgFragmentation,
284
+ fragmentedModules: fragmentedModules.slice(0, 10),
285
+ avgCohesion,
286
+ lowCohesionFiles,
287
+ criticalIssues,
288
+ majorIssues,
289
+ minorIssues,
290
+ totalPotentialSavings,
291
+ topExpensiveFiles,
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Analyze issues for a single file
297
+ */
298
+ function analyzeIssues(params: {
299
+ file: string;
300
+ importDepth: number;
301
+ contextBudget: number;
302
+ cohesionScore: number;
303
+ fragmentationScore: number;
304
+ maxDepth: number;
305
+ maxContextBudget: number;
306
+ minCohesion: number;
307
+ maxFragmentation: number;
308
+ circularDeps: string[][];
309
+ }): {
310
+ severity: ContextAnalysisResult['severity'];
311
+ issues: string[];
312
+ recommendations: string[];
313
+ potentialSavings: number;
314
+ } {
315
+ const {
316
+ file,
317
+ importDepth,
318
+ contextBudget,
319
+ cohesionScore,
320
+ fragmentationScore,
321
+ maxDepth,
322
+ maxContextBudget,
323
+ minCohesion,
324
+ maxFragmentation,
325
+ circularDeps,
326
+ } = params;
327
+
328
+ const issues: string[] = [];
329
+ const recommendations: string[] = [];
330
+ let severity: ContextAnalysisResult['severity'] = 'info';
331
+ let potentialSavings = 0;
332
+
333
+ // Check circular dependencies (CRITICAL)
334
+ if (circularDeps.length > 0) {
335
+ severity = 'critical';
336
+ issues.push(
337
+ `Part of ${circularDeps.length} circular dependency chain(s)`
338
+ );
339
+ recommendations.push('Break circular dependencies by extracting interfaces or using dependency injection');
340
+ potentialSavings += contextBudget * 0.2; // Estimate 20% savings
341
+ }
342
+
343
+ // Check import depth
344
+ if (importDepth > maxDepth * 1.5) {
345
+ severity = severity === 'critical' ? 'critical' : 'critical';
346
+ issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
347
+ recommendations.push('Flatten dependency tree or use facade pattern');
348
+ potentialSavings += contextBudget * 0.3; // Estimate 30% savings
349
+ } else if (importDepth > maxDepth) {
350
+ severity = severity === 'critical' ? 'critical' : 'major';
351
+ issues.push(`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`);
352
+ recommendations.push('Consider reducing dependency depth');
353
+ potentialSavings += contextBudget * 0.15;
354
+ }
355
+
356
+ // Check context budget
357
+ if (contextBudget > maxContextBudget * 1.5) {
358
+ severity = severity === 'critical' ? 'critical' : 'critical';
359
+ issues.push(`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`);
360
+ recommendations.push('Split into smaller modules or reduce dependency tree');
361
+ potentialSavings += contextBudget * 0.4; // Significant savings possible
362
+ } else if (contextBudget > maxContextBudget) {
363
+ severity = severity === 'critical' || severity === 'major' ? severity : 'major';
364
+ issues.push(`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`);
365
+ recommendations.push('Reduce file size or dependencies');
366
+ potentialSavings += contextBudget * 0.2;
367
+ }
368
+
369
+ // Check cohesion
370
+ if (cohesionScore < minCohesion * 0.5) {
371
+ severity = severity === 'critical' ? 'critical' : 'major';
372
+ issues.push(`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`);
373
+ recommendations.push('Split file by domain - separate unrelated functionality');
374
+ potentialSavings += contextBudget * 0.25;
375
+ } else if (cohesionScore < minCohesion) {
376
+ severity = severity === 'critical' || severity === 'major' ? severity : 'minor';
377
+ issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
378
+ recommendations.push('Consider grouping related exports together');
379
+ potentialSavings += contextBudget * 0.1;
380
+ }
381
+
382
+ // Check fragmentation
383
+ if (fragmentationScore > maxFragmentation) {
384
+ severity = severity === 'critical' || severity === 'major' ? severity : 'minor';
385
+ issues.push(`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`);
386
+ recommendations.push('Consolidate with related files in same domain');
387
+ potentialSavings += contextBudget * 0.3;
388
+ }
389
+
390
+ if (issues.length === 0) {
391
+ issues.push('No significant issues detected');
392
+ recommendations.push('File is well-structured for AI context usage');
393
+ }
394
+
395
+ return { severity, issues, recommendations, potentialSavings: Math.floor(potentialSavings) };
396
+ }
package/src/types.ts ADDED
@@ -0,0 +1,104 @@
1
+ import type { ScanOptions } from '@aiready/core';
2
+
3
+ export interface ContextAnalyzerOptions extends ScanOptions {
4
+ maxDepth?: number; // Maximum acceptable import depth, default 5
5
+ maxContextBudget?: number; // Maximum acceptable token budget, default 10000
6
+ minCohesion?: number; // Minimum acceptable cohesion score (0-1), default 0.6
7
+ maxFragmentation?: number; // Maximum acceptable fragmentation (0-1), default 0.5
8
+ focus?: 'fragmentation' | 'cohesion' | 'depth' | 'all'; // Analysis focus, default 'all'
9
+ includeNodeModules?: boolean; // Include node_modules in analysis, default false
10
+ }
11
+
12
+ export interface ContextAnalysisResult {
13
+ file: string;
14
+
15
+ // Basic metrics
16
+ tokenCost: number; // Total tokens in this file
17
+ linesOfCode: number;
18
+
19
+ // Dependency analysis
20
+ importDepth: number; // Max depth of import tree
21
+ dependencyCount: number; // Total transitive dependencies
22
+ dependencyList: string[]; // All files in dependency tree
23
+ circularDeps: string[][]; // Circular dependency chains if any
24
+
25
+ // Cohesion analysis
26
+ cohesionScore: number; // 0-1, how related are exports (1 = perfect cohesion)
27
+ domains: string[]; // Detected domain categories (e.g., ['user', 'auth'])
28
+ exportCount: number;
29
+
30
+ // AI context impact
31
+ contextBudget: number; // Total tokens to understand this file (includes all deps)
32
+ fragmentationScore: number; // 0-1, how scattered is this domain (0 = well-grouped)
33
+ relatedFiles: string[]; // Files that should be loaded together
34
+
35
+ // Recommendations
36
+ severity: 'critical' | 'major' | 'minor' | 'info';
37
+ issues: string[]; // List of specific problems
38
+ recommendations: string[]; // Actionable suggestions
39
+ potentialSavings: number; // Estimated token savings if fixed
40
+ }
41
+
42
+ export interface ModuleCluster {
43
+ domain: string; // e.g., "user-management", "auth"
44
+ files: string[];
45
+ totalTokens: number;
46
+ fragmentationScore: number; // 0-1, higher = more scattered
47
+ avgCohesion: number; // Average cohesion across files in cluster
48
+ suggestedStructure: {
49
+ targetFiles: number; // Recommended number of files
50
+ consolidationPlan: string[]; // Step-by-step suggestions
51
+ };
52
+ }
53
+
54
+ export interface ContextSummary {
55
+ totalFiles: number;
56
+ totalTokens: number;
57
+ avgContextBudget: number;
58
+ maxContextBudget: number;
59
+
60
+ // Depth metrics
61
+ avgImportDepth: number;
62
+ maxImportDepth: number;
63
+ deepFiles: Array<{ file: string; depth: number }>; // Files exceeding maxDepth
64
+
65
+ // Fragmentation metrics
66
+ avgFragmentation: number;
67
+ fragmentedModules: ModuleCluster[];
68
+
69
+ // Cohesion metrics
70
+ avgCohesion: number;
71
+ lowCohesionFiles: Array<{ file: string; score: number }>;
72
+
73
+ // Issues summary
74
+ criticalIssues: number;
75
+ majorIssues: number;
76
+ minorIssues: number;
77
+ totalPotentialSavings: number;
78
+
79
+ // Top offenders
80
+ topExpensiveFiles: Array<{
81
+ file: string;
82
+ contextBudget: number;
83
+ severity: string;
84
+ }>;
85
+ }
86
+
87
+ export interface DependencyGraph {
88
+ nodes: Map<string, DependencyNode>;
89
+ edges: Map<string, Set<string>>; // file -> dependencies
90
+ }
91
+
92
+ export interface DependencyNode {
93
+ file: string;
94
+ imports: string[]; // Direct imports
95
+ exports: ExportInfo[];
96
+ tokenCost: number;
97
+ linesOfCode: number;
98
+ }
99
+
100
+ export interface ExportInfo {
101
+ name: string;
102
+ type: 'function' | 'class' | 'const' | 'type' | 'interface' | 'default';
103
+ inferredDomain?: string; // Inferred from name/usage
104
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../core/tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }