@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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/.turbo/turbo-lint.log +2 -22
  3. package/.turbo/turbo-test.log +25 -25
  4. package/coverage/analyzer.ts.html +1369 -0
  5. package/coverage/ast-utils.ts.html +382 -0
  6. package/coverage/base.css +224 -0
  7. package/coverage/block-navigation.js +87 -0
  8. package/coverage/classifier.ts.html +1771 -0
  9. package/coverage/clover.xml +1245 -0
  10. package/coverage/cluster-detector.ts.html +385 -0
  11. package/coverage/coverage-final.json +15 -0
  12. package/coverage/defaults.ts.html +262 -0
  13. package/coverage/favicon.png +0 -0
  14. package/coverage/graph-builder.ts.html +859 -0
  15. package/coverage/index.html +311 -0
  16. package/coverage/index.ts.html +124 -0
  17. package/coverage/metrics.ts.html +748 -0
  18. package/coverage/prettify.css +1 -0
  19. package/coverage/prettify.js +2 -0
  20. package/coverage/provider.ts.html +283 -0
  21. package/coverage/remediation.ts.html +502 -0
  22. package/coverage/scoring.ts.html +619 -0
  23. package/coverage/semantic-analysis.ts.html +1201 -0
  24. package/coverage/sort-arrow-sprite.png +0 -0
  25. package/coverage/sorter.js +210 -0
  26. package/coverage/summary.ts.html +625 -0
  27. package/coverage/types.ts.html +571 -0
  28. package/dist/chunk-736QSHJP.mjs +1807 -0
  29. package/dist/chunk-CCBNKQYB.mjs +1812 -0
  30. package/dist/chunk-JUHHOSHG.mjs +1808 -0
  31. package/dist/cli.js +393 -379
  32. package/dist/cli.mjs +1 -1
  33. package/dist/index.d.mts +65 -6
  34. package/dist/index.d.ts +65 -6
  35. package/dist/index.js +396 -380
  36. package/dist/index.mjs +3 -1
  37. package/package.json +2 -2
  38. package/src/__tests__/cluster-detector.test.ts +138 -0
  39. package/src/__tests__/provider.test.ts +78 -0
  40. package/src/__tests__/remediation.test.ts +94 -0
  41. package/src/analyzer.ts +244 -1
  42. package/src/classifier.ts +100 -35
  43. package/src/index.ts +1 -242
  44. 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-PVVMK56C.mjs";
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.18",
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.18"
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 { estimateTokens, Severity } from '@aiready/core';
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
+ }