@aiready/context-analyzer 0.9.34 → 0.9.36

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 CHANGED
@@ -1,5 +1,4 @@
1
1
  import { scanFiles, readFileContent } from '@aiready/core';
2
- import type { ScanOptions } from '@aiready/core';
3
2
  import {
4
3
  buildDependencyGraph,
5
4
  calculateImportDepth,
@@ -37,11 +36,13 @@ import {
37
36
  getCoUsageData,
38
37
  findConsolidationCandidates,
39
38
  } from './semantic-analysis';
39
+ // Reference optional imports used by future heuristics to avoid lint warnings
40
+ void calculateFragmentation;
40
41
 
41
- export type {
42
- ContextAnalyzerOptions,
43
- ContextAnalysisResult,
44
- ContextSummary,
42
+ export type {
43
+ ContextAnalyzerOptions,
44
+ ContextAnalysisResult,
45
+ ContextSummary,
45
46
  ModuleCluster,
46
47
  DomainAssignment,
47
48
  DomainSignals,
@@ -50,10 +51,7 @@ export type {
50
51
  FileClassification,
51
52
  };
52
53
 
53
- export {
54
- classifyFile,
55
- adjustFragmentationForClassification,
56
- };
54
+ export { classifyFile, adjustFragmentationForClassification };
57
55
 
58
56
  export {
59
57
  buildCoUsageMatrix,
@@ -152,14 +150,18 @@ export async function analyzeContext(
152
150
  // Only add node_modules to exclude if includeNodeModules is false
153
151
  // The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
154
152
  // if user overrides the default exclude list
155
- exclude: includeNodeModules && scanOptions.exclude
156
- ? scanOptions.exclude.filter(pattern => pattern !== '**/node_modules/**')
157
- : scanOptions.exclude,
153
+ exclude:
154
+ includeNodeModules && scanOptions.exclude
155
+ ? scanOptions.exclude.filter(
156
+ (pattern) => pattern !== '**/node_modules/**'
157
+ )
158
+ : scanOptions.exclude,
158
159
  });
159
160
 
160
161
  // Separate files by language
161
- const pythonFiles = files.filter(f => f.toLowerCase().endsWith('.py'));
162
- const tsJsFiles = files.filter(f => !f.toLowerCase().endsWith('.py'));
162
+ const pythonFiles = files.filter((f) => f.toLowerCase().endsWith('.py'));
163
+ const tsJsFiles = files.filter((f) => !f.toLowerCase().endsWith('.py'));
164
+ void tsJsFiles;
163
165
 
164
166
  // Read all file contents
165
167
  const fileContents = await Promise.all(
@@ -170,37 +172,51 @@ export async function analyzeContext(
170
172
  );
171
173
 
172
174
  // Build dependency graph (TS/JS)
173
- const graph = buildDependencyGraph(fileContents.filter(f => !f.file.toLowerCase().endsWith('.py')));
175
+ const graph = buildDependencyGraph(
176
+ fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
177
+ );
174
178
 
175
179
  // Analyze Python files separately (if any)
176
180
  let pythonResults: ContextAnalysisResult[] = [];
177
181
  if (pythonFiles.length > 0) {
178
182
  const { analyzePythonContext } = await import('./analyzers/python-context');
179
- const pythonMetrics = await analyzePythonContext(pythonFiles, scanOptions.rootDir || options.rootDir || '.');
180
-
183
+ const pythonMetrics = await analyzePythonContext(
184
+ pythonFiles,
185
+ scanOptions.rootDir || options.rootDir || '.'
186
+ );
187
+
181
188
  // Convert Python metrics to ContextAnalysisResult format
182
- pythonResults = pythonMetrics.map(metric => {
183
- const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
184
- file: metric.file,
185
- importDepth: metric.importDepth,
186
- contextBudget: metric.contextBudget,
187
- cohesionScore: metric.cohesion,
188
- fragmentationScore: 0, // Python analyzer doesn't calculate fragmentation yet
189
- maxDepth,
190
- maxContextBudget,
191
- minCohesion,
192
- maxFragmentation,
193
- circularDeps: metric.metrics.circularDependencies.map(cycle => cycle.split(' → ')),
194
- });
189
+ pythonResults = pythonMetrics.map((metric) => {
190
+ const { severity, issues, recommendations, potentialSavings } =
191
+ analyzeIssues({
192
+ file: metric.file,
193
+ importDepth: metric.importDepth,
194
+ contextBudget: metric.contextBudget,
195
+ cohesionScore: metric.cohesion,
196
+ fragmentationScore: 0, // Python analyzer doesn't calculate fragmentation yet
197
+ maxDepth,
198
+ maxContextBudget,
199
+ minCohesion,
200
+ maxFragmentation,
201
+ circularDeps: metric.metrics.circularDependencies.map((cycle) =>
202
+ cycle.split(' → ')
203
+ ),
204
+ });
195
205
 
196
206
  return {
197
207
  file: metric.file,
198
- tokenCost: Math.floor(metric.contextBudget / (1 + metric.imports.length || 1)), // Estimate
208
+ tokenCost: Math.floor(
209
+ metric.contextBudget / (1 + metric.imports.length || 1)
210
+ ), // Estimate
199
211
  linesOfCode: metric.metrics.linesOfCode,
200
212
  importDepth: metric.importDepth,
201
213
  dependencyCount: metric.imports.length,
202
- dependencyList: metric.imports.map(imp => imp.resolvedPath || imp.source),
203
- circularDeps: metric.metrics.circularDependencies.map(cycle => cycle.split(' ')),
214
+ dependencyList: metric.imports.map(
215
+ (imp) => imp.resolvedPath || imp.source
216
+ ),
217
+ circularDeps: metric.metrics.circularDependencies.map((cycle) =>
218
+ cycle.split(' → ')
219
+ ),
204
220
  cohesionScore: metric.cohesion,
205
221
  domains: ['python'], // Generic for now
206
222
  exportCount: metric.exports.length,
@@ -253,7 +269,9 @@ export async function analyzeContext(
253
269
 
254
270
  const cohesionScore =
255
271
  focus === 'cohesion' || focus === 'all'
256
- ? calculateCohesion(node.exports, file, { coUsageMatrix: graph.coUsageMatrix })
272
+ ? calculateCohesion(node.exports, file, {
273
+ coUsageMatrix: graph.coUsageMatrix,
274
+ })
257
275
  : 1;
258
276
 
259
277
  const fragmentationScore = fragmentationMap.get(file) || 0;
@@ -281,6 +299,11 @@ export async function analyzeContext(
281
299
  maxFragmentation,
282
300
  circularDeps,
283
301
  });
302
+ // Some returned fields are not needed for the Python mapping here
303
+ void severity;
304
+ void issues;
305
+ void recommendations;
306
+ void potentialSavings;
284
307
 
285
308
  // Get domains from exports
286
309
  const domains = [
@@ -289,14 +312,14 @@ export async function analyzeContext(
289
312
 
290
313
  // Classify the file to help distinguish real issues from false positives
291
314
  const fileClassification = classifyFile(node, cohesionScore, domains);
292
-
315
+
293
316
  // Adjust cohesion based on classification (utility/service/handler files get boosted)
294
317
  const adjustedCohesionScore = adjustCohesionForClassification(
295
318
  cohesionScore,
296
319
  fileClassification,
297
320
  node
298
321
  );
299
-
322
+
300
323
  // Adjust fragmentation based on classification
301
324
  const adjustedFragmentationScore = adjustFragmentationForClassification(
302
325
  fragmentationScore,
@@ -346,7 +369,10 @@ export async function analyzeContext(
346
369
  fileClassification,
347
370
  severity: adjustedSeverity,
348
371
  issues: adjustedIssues,
349
- recommendations: [...finalRecommendations, ...classificationRecommendations.slice(0, 1)],
372
+ recommendations: [
373
+ ...finalRecommendations,
374
+ ...classificationRecommendations.slice(0, 1),
375
+ ],
350
376
  potentialSavings: adjustedSavings,
351
377
  });
352
378
  }
@@ -457,7 +483,10 @@ export function generateSummary(
457
483
  let importPairs = 0;
458
484
  for (let i = 0; i < files.length; i++) {
459
485
  for (let j = i + 1; j < files.length; j++) {
460
- importSimTotal += jaccard(files[i].dependencyList || [], files[j].dependencyList || []);
486
+ importSimTotal += jaccard(
487
+ files[i].dependencyList || [],
488
+ files[j].dependencyList || []
489
+ );
461
490
  importPairs++;
462
491
  }
463
492
  }
@@ -495,7 +524,9 @@ export function generateSummary(
495
524
  .sort((a, b) => a.score - b.score)
496
525
  .slice(0, 10);
497
526
 
498
- const criticalIssues = results.filter((r) => r.severity === 'critical').length;
527
+ const criticalIssues = results.filter(
528
+ (r) => r.severity === 'critical'
529
+ ).length;
499
530
  const majorIssues = results.filter((r) => r.severity === 'major').length;
500
531
  const minorIssues = results.filter((r) => r.severity === 'minor').length;
501
532
 
@@ -574,10 +605,10 @@ function analyzeIssues(params: {
574
605
  // Check circular dependencies (CRITICAL)
575
606
  if (circularDeps.length > 0) {
576
607
  severity = 'critical';
577
- issues.push(
578
- `Part of ${circularDeps.length} circular dependency chain(s)`
608
+ issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
609
+ recommendations.push(
610
+ 'Break circular dependencies by extracting interfaces or using dependency injection'
579
611
  );
580
- recommendations.push('Break circular dependencies by extracting interfaces or using dependency injection');
581
612
  potentialSavings += contextBudget * 0.2; // Estimate 20% savings
582
613
  }
583
614
 
@@ -589,7 +620,9 @@ function analyzeIssues(params: {
589
620
  potentialSavings += contextBudget * 0.3; // Estimate 30% savings
590
621
  } else if (importDepth > maxDepth) {
591
622
  severity = severity === 'critical' ? 'critical' : 'major';
592
- issues.push(`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`);
623
+ issues.push(
624
+ `Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
625
+ );
593
626
  recommendations.push('Consider reducing dependency depth');
594
627
  potentialSavings += contextBudget * 0.15;
595
628
  }
@@ -597,12 +630,19 @@ function analyzeIssues(params: {
597
630
  // Check context budget
598
631
  if (contextBudget > maxContextBudget * 1.5) {
599
632
  severity = severity === 'critical' ? 'critical' : 'critical';
600
- issues.push(`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`);
601
- recommendations.push('Split into smaller modules or reduce dependency tree');
633
+ issues.push(
634
+ `Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
635
+ );
636
+ recommendations.push(
637
+ 'Split into smaller modules or reduce dependency tree'
638
+ );
602
639
  potentialSavings += contextBudget * 0.4; // Significant savings possible
603
640
  } else if (contextBudget > maxContextBudget) {
604
- severity = severity === 'critical' || severity === 'major' ? severity : 'major';
605
- issues.push(`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`);
641
+ severity =
642
+ severity === 'critical' || severity === 'major' ? severity : 'major';
643
+ issues.push(
644
+ `Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
645
+ );
606
646
  recommendations.push('Reduce file size or dependencies');
607
647
  potentialSavings += contextBudget * 0.2;
608
648
  }
@@ -610,11 +650,16 @@ function analyzeIssues(params: {
610
650
  // Check cohesion
611
651
  if (cohesionScore < minCohesion * 0.5) {
612
652
  severity = severity === 'critical' ? 'critical' : 'major';
613
- issues.push(`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`);
614
- recommendations.push('Split file by domain - separate unrelated functionality');
653
+ issues.push(
654
+ `Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
655
+ );
656
+ recommendations.push(
657
+ 'Split file by domain - separate unrelated functionality'
658
+ );
615
659
  potentialSavings += contextBudget * 0.25;
616
660
  } else if (cohesionScore < minCohesion) {
617
- severity = severity === 'critical' || severity === 'major' ? severity : 'minor';
661
+ severity =
662
+ severity === 'critical' || severity === 'major' ? severity : 'minor';
618
663
  issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
619
664
  recommendations.push('Consider grouping related exports together');
620
665
  potentialSavings += contextBudget * 0.1;
@@ -622,8 +667,11 @@ function analyzeIssues(params: {
622
667
 
623
668
  // Check fragmentation
624
669
  if (fragmentationScore > maxFragmentation) {
625
- severity = severity === 'critical' || severity === 'major' ? severity : 'minor';
626
- issues.push(`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`);
670
+ severity =
671
+ severity === 'critical' || severity === 'major' ? severity : 'minor';
672
+ issues.push(
673
+ `High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
674
+ );
627
675
  recommendations.push('Consolidate with related files in same domain');
628
676
  potentialSavings += contextBudget * 0.3;
629
677
  }
@@ -636,13 +684,20 @@ function analyzeIssues(params: {
636
684
  // Detect build artifacts and downgrade severity to reduce noise
637
685
  if (isBuildArtifact(file)) {
638
686
  issues.push('Detected build artifact (bundled/output file)');
639
- recommendations.push('Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis');
687
+ recommendations.push(
688
+ 'Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis'
689
+ );
640
690
  severity = downgradeSeverity(severity);
641
691
  // Build artifacts do not represent actionable savings
642
692
  potentialSavings = 0;
643
693
  }
644
694
 
645
- return { severity, issues, recommendations, potentialSavings: Math.floor(potentialSavings) };
695
+ return {
696
+ severity,
697
+ issues,
698
+ recommendations,
699
+ potentialSavings: Math.floor(potentialSavings),
700
+ };
646
701
  }
647
702
 
648
703
  export { getSmartDefaults };
@@ -664,7 +719,9 @@ function isBuildArtifact(filePath: string): boolean {
664
719
  );
665
720
  }
666
721
 
667
- function downgradeSeverity(s: ContextAnalysisResult['severity']): ContextAnalysisResult['severity'] {
722
+ function downgradeSeverity(
723
+ s: ContextAnalysisResult['severity']
724
+ ): ContextAnalysisResult['severity'] {
668
725
  switch (s) {
669
726
  case 'critical':
670
727
  return 'minor';
package/src/scoring.ts CHANGED
@@ -1,21 +1,21 @@
1
- import {
2
- calculateMonthlyCost,
1
+ import {
2
+ calculateMonthlyCost,
3
3
  calculateProductivityImpact,
4
4
  DEFAULT_COST_CONFIG,
5
- type CostConfig
5
+ type CostConfig,
6
6
  } from '@aiready/core';
7
7
  import type { ToolScoringOutput } from '@aiready/core';
8
8
  import type { ContextSummary } from './types';
9
9
 
10
10
  /**
11
11
  * Calculate AI Readiness Score for context efficiency (0-100)
12
- *
12
+ *
13
13
  * Based on:
14
14
  * - Average context budget (tokens needed to understand files)
15
15
  * - Import depth (dependency chain length)
16
16
  * - Fragmentation score (code organization)
17
17
  * - Critical/major issues
18
- *
18
+ *
19
19
  * Includes business value metrics:
20
20
  * - Estimated monthly cost of context waste
21
21
  * - Estimated developer hours to fix
@@ -33,66 +33,70 @@ export function calculateContextScore(
33
33
  criticalIssues,
34
34
  majorIssues,
35
35
  } = summary;
36
-
36
+
37
37
  // Context budget scoring (40% weight in final score)
38
38
  // Ideal: <5000 tokens avg = 100
39
39
  // Acceptable: 5000-10000 = 90-70
40
40
  // High: 10000-20000 = 70-40
41
41
  // Critical: >20000 = <40
42
- const budgetScore = avgContextBudget < 5000
43
- ? 100
44
- : Math.max(0, 100 - (avgContextBudget - 5000) / 150);
45
-
42
+ const budgetScore =
43
+ avgContextBudget < 5000
44
+ ? 100
45
+ : Math.max(0, 100 - (avgContextBudget - 5000) / 150);
46
+
46
47
  // Import depth scoring (30% weight)
47
48
  // Ideal: <5 avg = 100
48
49
  // Acceptable: 5-8 = 80-60
49
50
  // Deep: >8 = <60
50
- const depthScore = avgImportDepth < 5
51
- ? 100
52
- : Math.max(0, 100 - (avgImportDepth - 5) * 10);
53
-
51
+ const depthScore =
52
+ avgImportDepth < 5 ? 100 : Math.max(0, 100 - (avgImportDepth - 5) * 10);
53
+
54
54
  // Fragmentation scoring (30% weight)
55
55
  // Well-organized: <0.3 = 100
56
56
  // Moderate: 0.3-0.5 = 80-60
57
57
  // Fragmented: >0.5 = <60
58
- const fragmentationScore = avgFragmentation < 0.3
59
- ? 100
60
- : Math.max(0, 100 - (avgFragmentation - 0.3) * 200);
61
-
58
+ const fragmentationScore =
59
+ avgFragmentation < 0.3
60
+ ? 100
61
+ : Math.max(0, 100 - (avgFragmentation - 0.3) * 200);
62
+
62
63
  // Issue penalties
63
64
  const criticalPenalty = criticalIssues * 10;
64
65
  const majorPenalty = majorIssues * 3;
65
-
66
+
66
67
  // Max budget penalty (if any single file is extreme)
67
- const maxBudgetPenalty = maxContextBudget > 15000
68
- ? Math.min(20, (maxContextBudget - 15000) / 500)
69
- : 0;
70
-
68
+ const maxBudgetPenalty =
69
+ maxContextBudget > 15000
70
+ ? Math.min(20, (maxContextBudget - 15000) / 500)
71
+ : 0;
72
+
71
73
  // Weighted average of subscores
72
- const rawScore = (budgetScore * 0.4) + (depthScore * 0.3) + (fragmentationScore * 0.3);
73
- const finalScore = rawScore - criticalPenalty - majorPenalty - maxBudgetPenalty;
74
-
74
+ const rawScore =
75
+ budgetScore * 0.4 + depthScore * 0.3 + fragmentationScore * 0.3;
76
+ const finalScore =
77
+ rawScore - criticalPenalty - majorPenalty - maxBudgetPenalty;
78
+
75
79
  const score = Math.max(0, Math.min(100, Math.round(finalScore)));
76
-
80
+
77
81
  // Build factors array
78
82
  const factors = [
79
83
  {
80
84
  name: 'Context Budget',
81
- impact: Math.round((budgetScore * 0.4) - 40),
85
+ impact: Math.round(budgetScore * 0.4 - 40),
82
86
  description: `Avg ${Math.round(avgContextBudget)} tokens per file ${avgContextBudget < 5000 ? '(excellent)' : avgContextBudget < 10000 ? '(acceptable)' : '(high)'}`,
83
87
  },
84
88
  {
85
89
  name: 'Import Depth',
86
- impact: Math.round((depthScore * 0.3) - 30),
90
+ impact: Math.round(depthScore * 0.3 - 30),
87
91
  description: `Avg ${avgImportDepth.toFixed(1)} levels ${avgImportDepth < 5 ? '(excellent)' : avgImportDepth < 8 ? '(acceptable)' : '(deep)'}`,
88
92
  },
89
93
  {
90
94
  name: 'Fragmentation',
91
- impact: Math.round((fragmentationScore * 0.3) - 30),
95
+ impact: Math.round(fragmentationScore * 0.3 - 30),
92
96
  description: `${(avgFragmentation * 100).toFixed(0)}% fragmentation ${avgFragmentation < 0.3 ? '(well-organized)' : avgFragmentation < 0.5 ? '(moderate)' : '(high)'}`,
93
97
  },
94
98
  ];
95
-
99
+
96
100
  if (criticalIssues > 0) {
97
101
  factors.push({
98
102
  name: 'Critical Issues',
@@ -100,7 +104,7 @@ export function calculateContextScore(
100
104
  description: `${criticalIssues} critical context issue${criticalIssues > 1 ? 's' : ''}`,
101
105
  });
102
106
  }
103
-
107
+
104
108
  if (majorIssues > 0) {
105
109
  factors.push({
106
110
  name: 'Major Issues',
@@ -108,7 +112,7 @@ export function calculateContextScore(
108
112
  description: `${majorIssues} major context issue${majorIssues > 1 ? 's' : ''}`,
109
113
  });
110
114
  }
111
-
115
+
112
116
  if (maxBudgetPenalty > 0) {
113
117
  factors.push({
114
118
  name: 'Extreme File Detected',
@@ -116,19 +120,22 @@ export function calculateContextScore(
116
120
  description: `One file requires ${Math.round(maxContextBudget)} tokens (very high)`,
117
121
  });
118
122
  }
119
-
123
+
120
124
  // Generate recommendations
121
125
  const recommendations: ToolScoringOutput['recommendations'] = [];
122
-
126
+
123
127
  if (avgContextBudget > 10000) {
124
- const estimatedImpact = Math.min(15, Math.round((avgContextBudget - 10000) / 1000));
128
+ const estimatedImpact = Math.min(
129
+ 15,
130
+ Math.round((avgContextBudget - 10000) / 1000)
131
+ );
125
132
  recommendations.push({
126
133
  action: 'Reduce file dependencies to lower context requirements',
127
134
  estimatedImpact,
128
135
  priority: 'high',
129
136
  });
130
137
  }
131
-
138
+
132
139
  if (avgImportDepth > 8) {
133
140
  const estimatedImpact = Math.min(10, Math.round((avgImportDepth - 8) * 2));
134
141
  recommendations.push({
@@ -137,16 +144,19 @@ export function calculateContextScore(
137
144
  priority: avgImportDepth > 10 ? 'high' : 'medium',
138
145
  });
139
146
  }
140
-
147
+
141
148
  if (avgFragmentation > 0.5) {
142
- const estimatedImpact = Math.min(12, Math.round((avgFragmentation - 0.5) * 40));
149
+ const estimatedImpact = Math.min(
150
+ 12,
151
+ Math.round((avgFragmentation - 0.5) * 40)
152
+ );
143
153
  recommendations.push({
144
154
  action: 'Consolidate related code into cohesive modules',
145
155
  estimatedImpact,
146
156
  priority: 'medium',
147
157
  });
148
158
  }
149
-
159
+
150
160
  if (maxContextBudget > 20000) {
151
161
  recommendations.push({
152
162
  action: `Split large file (${Math.round(maxContextBudget)} tokens) into smaller modules`,
@@ -154,20 +164,20 @@ export function calculateContextScore(
154
164
  priority: 'high',
155
165
  });
156
166
  }
157
-
167
+
158
168
  // Calculate business value metrics
159
169
  const cfg = { ...DEFAULT_COST_CONFIG, ...costConfig };
160
170
  // Total context budget across all files
161
171
  const totalContextBudget = avgContextBudget * summary.totalFiles;
162
172
  const estimatedMonthlyCost = calculateMonthlyCost(totalContextBudget, cfg);
163
-
173
+
164
174
  // Convert issues to format for productivity calculation
165
175
  const issues = [
166
176
  ...Array(criticalIssues).fill({ severity: 'critical' as const }),
167
177
  ...Array(majorIssues).fill({ severity: 'major' as const }),
168
178
  ];
169
179
  const productivityImpact = calculateProductivityImpact(issues);
170
-
180
+
171
181
  return {
172
182
  toolName: 'context-analyzer',
173
183
  score,