@aiready/context-analyzer 0.19.17 → 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 +23 -23
  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/src/classifier.ts CHANGED
@@ -1,7 +1,29 @@
1
1
  import type { DependencyNode, FileClassification } from './types';
2
2
 
3
+ /**
4
+ * Constants for file classifications to avoid magic strings
5
+ */
6
+ export const Classification = {
7
+ BARREL: 'barrel-export' as const,
8
+ TYPE_DEFINITION: 'type-definition' as const,
9
+ NEXTJS_PAGE: 'nextjs-page' as const,
10
+ LAMBDA_HANDLER: 'lambda-handler' as const,
11
+ SERVICE: 'service-file' as const,
12
+ EMAIL_TEMPLATE: 'email-template' as const,
13
+ PARSER: 'parser-file' as const,
14
+ COHESIVE_MODULE: 'cohesive-module' as const,
15
+ UTILITY_MODULE: 'utility-module' as const,
16
+ MIXED_CONCERNS: 'mixed-concerns' as const,
17
+ UNKNOWN: 'unknown' as const,
18
+ };
19
+
3
20
  /**
4
21
  * Classify a file into a specific type for better analysis context
22
+ *
23
+ * @param node The dependency node representing the file
24
+ * @param cohesionScore The calculated cohesion score for the file
25
+ * @param domains The detected domains/concerns for the file
26
+ * @returns The determined file classification
5
27
  */
6
28
  export function classifyFile(
7
29
  node: DependencyNode,
@@ -10,74 +32,78 @@ export function classifyFile(
10
32
  ): FileClassification {
11
33
  // 1. Detect barrel exports (primarily re-exports)
12
34
  if (isBarrelExport(node)) {
13
- return 'barrel-export';
35
+ return Classification.BARREL;
14
36
  }
15
37
 
16
38
  // 2. Detect type definition files
17
39
  if (isTypeDefinition(node)) {
18
- return 'type-definition';
40
+ return Classification.TYPE_DEFINITION;
19
41
  }
20
42
 
21
43
  // 3. Detect Next.js App Router pages
22
44
  if (isNextJsPage(node)) {
23
- return 'nextjs-page';
45
+ return Classification.NEXTJS_PAGE;
24
46
  }
25
47
 
26
48
  // 4. Detect Lambda handlers
27
49
  if (isLambdaHandler(node)) {
28
- return 'lambda-handler';
50
+ return Classification.LAMBDA_HANDLER;
29
51
  }
30
52
 
31
53
  // 5. Detect Service files
32
54
  if (isServiceFile(node)) {
33
- return 'service-file';
55
+ return Classification.SERVICE;
34
56
  }
35
57
 
36
58
  // 6. Detect Email templates
37
59
  if (isEmailTemplate(node)) {
38
- return 'email-template';
60
+ return Classification.EMAIL_TEMPLATE;
39
61
  }
40
62
 
41
63
  // 7. Detect Parser/Transformer files
42
64
  if (isParserFile(node)) {
43
- return 'parser-file';
65
+ return Classification.PARSER;
44
66
  }
45
67
 
46
68
  // 8. Detect Session/State management files
47
69
  if (isSessionFile(node)) {
48
70
  // If it has high cohesion, it's a cohesive module
49
- if (cohesionScore >= 0.25 && domains.length <= 1) return 'cohesive-module';
50
- return 'utility-module'; // Group with utility for now
71
+ if (cohesionScore >= 0.25 && domains.length <= 1)
72
+ return Classification.COHESIVE_MODULE;
73
+ return Classification.UTILITY_MODULE; // Group with utility for now
51
74
  }
52
75
 
53
76
  // 9. Detect Utility modules (multi-domain but functional purpose)
54
77
  if (isUtilityModule(node)) {
55
- return 'utility-module';
78
+ return Classification.UTILITY_MODULE;
56
79
  }
57
80
 
58
81
  // 10. Detect Config/Schema files
59
82
  if (isConfigFile(node)) {
60
- return 'cohesive-module';
83
+ return Classification.COHESIVE_MODULE;
61
84
  }
62
85
 
63
86
  // Cohesion and Domain heuristics
64
87
  if (domains.length <= 1 && domains[0] !== 'unknown') {
65
- return 'cohesive-module';
88
+ return Classification.COHESIVE_MODULE;
66
89
  }
67
90
 
68
91
  if (domains.length > 1 && cohesionScore < 0.4) {
69
- return 'mixed-concerns';
92
+ return Classification.MIXED_CONCERNS;
70
93
  }
71
94
 
72
95
  if (cohesionScore >= 0.7) {
73
- return 'cohesive-module';
96
+ return Classification.COHESIVE_MODULE;
74
97
  }
75
98
 
76
- return 'unknown';
99
+ return Classification.UNKNOWN;
77
100
  }
78
101
 
79
102
  /**
80
103
  * Detect if a file is a barrel export (index.ts)
104
+ *
105
+ * @param node The dependency node to check
106
+ * @returns True if the file appears to be a barrel export
81
107
  */
82
108
  export function isBarrelExport(node: DependencyNode): boolean {
83
109
  const { file, exports } = node;
@@ -106,6 +132,9 @@ export function isBarrelExport(node: DependencyNode): boolean {
106
132
 
107
133
  /**
108
134
  * Detect if a file is primarily type definitions
135
+ *
136
+ * @param node The dependency node to check
137
+ * @returns True if the file appears to be primarily types
109
138
  */
110
139
  export function isTypeDefinition(node: DependencyNode): boolean {
111
140
  const { file } = node;
@@ -132,6 +161,9 @@ export function isTypeDefinition(node: DependencyNode): boolean {
132
161
 
133
162
  /**
134
163
  * Detect if a file is a utility module
164
+ *
165
+ * @param node The dependency node to check
166
+ * @returns True if the file appears to be a utility module
135
167
  */
136
168
  export function isUtilityModule(node: DependencyNode): boolean {
137
169
  const { file } = node;
@@ -155,6 +187,9 @@ export function isUtilityModule(node: DependencyNode): boolean {
155
187
 
156
188
  /**
157
189
  * Detect if a file is a Lambda/API handler
190
+ *
191
+ * @param node The dependency node to check
192
+ * @returns True if the file appears to be a Lambda handler
158
193
  */
159
194
  export function isLambdaHandler(node: DependencyNode): boolean {
160
195
  const { file, exports } = node;
@@ -191,6 +226,9 @@ export function isLambdaHandler(node: DependencyNode): boolean {
191
226
 
192
227
  /**
193
228
  * Detect if a file is a service file
229
+ *
230
+ * @param node The dependency node to check
231
+ * @returns True if the file appears to be a service file
194
232
  */
195
233
  export function isServiceFile(node: DependencyNode): boolean {
196
234
  const { file, exports } = node;
@@ -217,6 +255,9 @@ export function isServiceFile(node: DependencyNode): boolean {
217
255
 
218
256
  /**
219
257
  * Detect if a file is an email template/layout
258
+ *
259
+ * @param node The dependency node to check
260
+ * @returns True if the file appears to be an email template
220
261
  */
221
262
  export function isEmailTemplate(node: DependencyNode): boolean {
222
263
  const { file, exports } = node;
@@ -254,6 +295,9 @@ export function isEmailTemplate(node: DependencyNode): boolean {
254
295
 
255
296
  /**
256
297
  * Detect if a file is a parser/transformer
298
+ *
299
+ * @param node The dependency node to check
300
+ * @returns True if the file appears to be a parser
257
301
  */
258
302
  export function isParserFile(node: DependencyNode): boolean {
259
303
  const { file, exports } = node;
@@ -290,6 +334,9 @@ export function isParserFile(node: DependencyNode): boolean {
290
334
 
291
335
  /**
292
336
  * Detect if a file is a session/state management file
337
+ *
338
+ * @param node The dependency node to check
339
+ * @returns True if the file appears to be a session/state file
293
340
  */
294
341
  export function isSessionFile(node: DependencyNode): boolean {
295
342
  const { file, exports } = node;
@@ -315,6 +362,9 @@ export function isSessionFile(node: DependencyNode): boolean {
315
362
 
316
363
  /**
317
364
  * Detect if a file is a configuration or schema file
365
+ *
366
+ * @param node The dependency node to check
367
+ * @returns True if the file appears to be a config file
318
368
  */
319
369
  export function isConfigFile(node: DependencyNode): boolean {
320
370
  const { file, exports } = node;
@@ -348,6 +398,9 @@ export function isConfigFile(node: DependencyNode): boolean {
348
398
 
349
399
  /**
350
400
  * Detect if a file is a Next.js App Router page
401
+ *
402
+ * @param node The dependency node to check
403
+ * @returns True if the file appears to be a Next.js page
351
404
  */
352
405
  export function isNextJsPage(node: DependencyNode): boolean {
353
406
  const { file, exports } = node;
@@ -377,6 +430,11 @@ export function isNextJsPage(node: DependencyNode): boolean {
377
430
 
378
431
  /**
379
432
  * Adjust cohesion score based on file classification
433
+ *
434
+ * @param baseCohesion The initial cohesion score
435
+ * @param classification The file classification
436
+ * @param node Optional dependency node for further context
437
+ * @returns The adjusted cohesion score
380
438
  */
381
439
  export function adjustCohesionForClassification(
382
440
  baseCohesion: number,
@@ -384,13 +442,13 @@ export function adjustCohesionForClassification(
384
442
  node?: DependencyNode
385
443
  ): number {
386
444
  switch (classification) {
387
- case 'barrel-export':
445
+ case Classification.BARREL:
388
446
  return 1;
389
- case 'type-definition':
447
+ case Classification.TYPE_DEFINITION:
390
448
  return 1;
391
- case 'nextjs-page':
449
+ case Classification.NEXTJS_PAGE:
392
450
  return 1;
393
- case 'utility-module': {
451
+ case Classification.UTILITY_MODULE: {
394
452
  if (
395
453
  node &&
396
454
  hasRelatedExportNames(
@@ -401,17 +459,17 @@ export function adjustCohesionForClassification(
401
459
  }
402
460
  return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
403
461
  }
404
- case 'service-file':
462
+ case Classification.SERVICE:
405
463
  return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
406
- case 'lambda-handler':
464
+ case Classification.LAMBDA_HANDLER:
407
465
  return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
408
- case 'email-template':
466
+ case Classification.EMAIL_TEMPLATE:
409
467
  return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
410
- case 'parser-file':
468
+ case Classification.PARSER:
411
469
  return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
412
- case 'cohesive-module':
470
+ case Classification.COHESIVE_MODULE:
413
471
  return Math.max(baseCohesion, 0.7);
414
- case 'mixed-concerns':
472
+ case Classification.MIXED_CONCERNS:
415
473
  return baseCohesion;
416
474
  default:
417
475
  return Math.min(1, baseCohesion + 0.1);
@@ -420,6 +478,9 @@ export function adjustCohesionForClassification(
420
478
 
421
479
  /**
422
480
  * Check if export names suggest related functionality
481
+ *
482
+ * @param exportNames List of exported names
483
+ * @returns True if names appear related
423
484
  */
424
485
  function hasRelatedExportNames(exportNames: string[]): boolean {
425
486
  if (exportNames.length < 2) return true;
@@ -470,26 +531,30 @@ function hasRelatedExportNames(exportNames: string[]): boolean {
470
531
 
471
532
  /**
472
533
  * Adjust fragmentation score based on file classification
534
+ *
535
+ * @param baseFragmentation The initial fragmentation score
536
+ * @param classification The file classification
537
+ * @returns The adjusted fragmentation score
473
538
  */
474
539
  export function adjustFragmentationForClassification(
475
540
  baseFragmentation: number,
476
541
  classification: FileClassification
477
542
  ): number {
478
543
  switch (classification) {
479
- case 'barrel-export':
544
+ case Classification.BARREL:
480
545
  return 0;
481
- case 'type-definition':
546
+ case Classification.TYPE_DEFINITION:
482
547
  return 0;
483
- case 'utility-module':
484
- case 'service-file':
485
- case 'lambda-handler':
486
- case 'email-template':
487
- case 'parser-file':
488
- case 'nextjs-page':
548
+ case Classification.UTILITY_MODULE:
549
+ case Classification.SERVICE:
550
+ case Classification.LAMBDA_HANDLER:
551
+ case Classification.EMAIL_TEMPLATE:
552
+ case Classification.PARSER:
553
+ case Classification.NEXTJS_PAGE:
489
554
  return baseFragmentation * 0.2;
490
- case 'cohesive-module':
555
+ case Classification.COHESIVE_MODULE:
491
556
  return baseFragmentation * 0.3;
492
- case 'mixed-concerns':
557
+ case Classification.MIXED_CONCERNS:
493
558
  return baseFragmentation;
494
559
  default:
495
560
  return baseFragmentation * 0.7;
package/src/index.ts CHANGED
@@ -1,27 +1,5 @@
1
- import { scanFiles, readFileContent, ToolRegistry } from '@aiready/core';
2
- import {
3
- buildDependencyGraph,
4
- calculateImportDepth,
5
- getTransitiveDependencies,
6
- calculateContextBudget,
7
- detectCircularDependencies,
8
- calculateCohesion,
9
- detectModuleClusters,
10
- classifyFile,
11
- adjustCohesionForClassification,
12
- adjustFragmentationForClassification,
13
- getClassificationRecommendations,
14
- analyzeIssues,
15
- } from './analyzer';
16
- import { calculateContextScore } from './scoring';
17
- import { getSmartDefaults } from './defaults';
18
- import { generateSummary } from './summary';
1
+ import { ToolRegistry } from '@aiready/core';
19
2
  import { ContextAnalyzerProvider } from './provider';
20
- import type {
21
- ContextAnalyzerOptions,
22
- ContextAnalysisResult,
23
- ContextSummary,
24
- } from './types';
25
3
 
26
4
  // Register with global registry
27
5
  ToolRegistry.register(ContextAnalyzerProvider);
@@ -33,222 +11,3 @@ export * from './summary';
33
11
  export * from './types';
34
12
  export * from './semantic-analysis';
35
13
  export { ContextAnalyzerProvider };
36
-
37
- /**
38
- * Analyze AI context window cost for a codebase
39
- */
40
- export async function analyzeContext(
41
- options: ContextAnalyzerOptions
42
- ): Promise<ContextAnalysisResult[]> {
43
- const {
44
- maxDepth = 5,
45
- maxContextBudget = 10000,
46
- minCohesion = 0.6,
47
- maxFragmentation = 0.5,
48
- focus = 'all',
49
- includeNodeModules = false,
50
- ...scanOptions
51
- } = options;
52
-
53
- const files = await scanFiles({
54
- ...scanOptions,
55
- exclude:
56
- includeNodeModules && scanOptions.exclude
57
- ? scanOptions.exclude.filter(
58
- (pattern) => pattern !== '**/node_modules/**'
59
- )
60
- : scanOptions.exclude,
61
- });
62
-
63
- const pythonFiles = files.filter((f) => f.toLowerCase().endsWith('.py'));
64
- const fileContents = await Promise.all(
65
- files.map(async (file) => ({
66
- file,
67
- content: await readFileContent(file),
68
- }))
69
- );
70
-
71
- const graph = buildDependencyGraph(
72
- fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
73
- );
74
-
75
- let pythonResults: ContextAnalysisResult[] = [];
76
- if (pythonFiles.length > 0) {
77
- const { analyzePythonContext } = await import('./analyzers/python-context');
78
- const pythonMetrics = await analyzePythonContext(
79
- pythonFiles,
80
- scanOptions.rootDir || options.rootDir || '.'
81
- );
82
-
83
- pythonResults = pythonMetrics.map((metric) => {
84
- const { severity, issues, recommendations, potentialSavings } =
85
- analyzeIssues({
86
- file: metric.file,
87
- importDepth: metric.importDepth,
88
- contextBudget: metric.contextBudget,
89
- cohesionScore: metric.cohesion,
90
- fragmentationScore: 0,
91
- maxDepth,
92
- maxContextBudget,
93
- minCohesion,
94
- maxFragmentation,
95
- circularDeps: metric.metrics.circularDependencies.map((cycle) =>
96
- cycle.split(' → ')
97
- ),
98
- });
99
-
100
- return {
101
- file: metric.file,
102
- tokenCost: Math.floor(
103
- metric.contextBudget / (1 + metric.imports.length || 1)
104
- ),
105
- linesOfCode: metric.metrics.linesOfCode,
106
- importDepth: metric.importDepth,
107
- dependencyCount: metric.imports.length,
108
- dependencyList: metric.imports.map(
109
- (imp) => imp.resolvedPath || imp.source
110
- ),
111
- circularDeps: metric.metrics.circularDependencies.map((cycle) =>
112
- cycle.split(' → ')
113
- ),
114
- cohesionScore: metric.cohesion,
115
- domains: ['python'],
116
- exportCount: metric.exports.length,
117
- contextBudget: metric.contextBudget,
118
- fragmentationScore: 0,
119
- relatedFiles: [],
120
- fileClassification: 'unknown' as const,
121
- severity,
122
- issues,
123
- recommendations,
124
- potentialSavings,
125
- };
126
- });
127
- }
128
-
129
- const circularDeps = detectCircularDependencies(graph);
130
- const useLogScale = files.length >= 500;
131
- const clusters = detectModuleClusters(graph, { useLogScale });
132
- const fragmentationMap = new Map<string, number>();
133
- for (const cluster of clusters) {
134
- for (const file of cluster.files) {
135
- fragmentationMap.set(file, cluster.fragmentationScore);
136
- }
137
- }
138
-
139
- const results: ContextAnalysisResult[] = [];
140
-
141
- for (const { file } of fileContents) {
142
- const node = graph.nodes.get(file);
143
- if (!node) continue;
144
-
145
- const importDepth =
146
- focus === 'depth' || focus === 'all'
147
- ? calculateImportDepth(file, graph)
148
- : 0;
149
- const dependencyList =
150
- focus === 'depth' || focus === 'all'
151
- ? getTransitiveDependencies(file, graph)
152
- : [];
153
- const contextBudget =
154
- focus === 'all' ? calculateContextBudget(file, graph) : node.tokenCost;
155
- const cohesionScore =
156
- focus === 'cohesion' || focus === 'all'
157
- ? calculateCohesion(node.exports, file, {
158
- coUsageMatrix: graph.coUsageMatrix,
159
- })
160
- : 1;
161
-
162
- const fragmentationScore = fragmentationMap.get(file) || 0;
163
- const relatedFiles: string[] = [];
164
- for (const cluster of clusters) {
165
- if (cluster.files.includes(file)) {
166
- relatedFiles.push(...cluster.files.filter((f) => f !== file));
167
- break;
168
- }
169
- }
170
-
171
- const { issues } = analyzeIssues({
172
- file,
173
- importDepth,
174
- contextBudget,
175
- cohesionScore,
176
- fragmentationScore,
177
- maxDepth,
178
- maxContextBudget,
179
- minCohesion,
180
- maxFragmentation,
181
- circularDeps,
182
- });
183
-
184
- const domains = [
185
- ...new Set(node.exports.map((e) => e.inferredDomain || 'unknown')),
186
- ];
187
- const fileClassification = classifyFile(node);
188
- const adjustedCohesionScore = adjustCohesionForClassification(
189
- cohesionScore,
190
- fileClassification,
191
- node
192
- );
193
- const adjustedFragmentationScore = adjustFragmentationForClassification(
194
- fragmentationScore,
195
- fileClassification
196
- );
197
- const classificationRecommendations = getClassificationRecommendations(
198
- fileClassification,
199
- file,
200
- issues
201
- );
202
-
203
- const {
204
- severity: adjustedSeverity,
205
- issues: adjustedIssues,
206
- recommendations: finalRecommendations,
207
- potentialSavings: adjustedSavings,
208
- } = analyzeIssues({
209
- file,
210
- importDepth,
211
- contextBudget,
212
- cohesionScore: adjustedCohesionScore,
213
- fragmentationScore: adjustedFragmentationScore,
214
- maxDepth,
215
- maxContextBudget,
216
- minCohesion,
217
- maxFragmentation,
218
- circularDeps,
219
- });
220
-
221
- results.push({
222
- file,
223
- tokenCost: node.tokenCost,
224
- linesOfCode: node.linesOfCode,
225
- importDepth,
226
- dependencyCount: dependencyList.length,
227
- dependencyList,
228
- circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
229
- cohesionScore: adjustedCohesionScore,
230
- domains,
231
- exportCount: node.exports.length,
232
- contextBudget,
233
- fragmentationScore: adjustedFragmentationScore,
234
- relatedFiles,
235
- fileClassification,
236
- severity: adjustedSeverity,
237
- issues: adjustedIssues,
238
- recommendations: [
239
- ...finalRecommendations,
240
- ...classificationRecommendations.slice(0, 1),
241
- ],
242
- potentialSavings: adjustedSavings,
243
- });
244
- }
245
-
246
- const allResults = [...results, ...pythonResults];
247
- const finalSummary = generateSummary(allResults, options);
248
- return allResults.sort((a, b) => {
249
- const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
250
- const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
251
- if (severityDiff !== 0) return severityDiff;
252
- return b.contextBudget - a.contextBudget;
253
- });
254
- }
package/src/provider.ts CHANGED
@@ -9,7 +9,8 @@ import {
9
9
  IssueType,
10
10
  SpokeOutputSchema,
11
11
  } from '@aiready/core';
12
- import { analyzeContext, generateSummary } from './index';
12
+ import { analyzeContext } from './analyzer';
13
+ import { generateSummary } from './summary';
13
14
  import { calculateContextScore } from './scoring';
14
15
  import { ContextAnalyzerOptions, ContextAnalysisResult } from './types';
15
16