@aiready/context-analyzer 0.9.41 → 0.9.42

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 (52) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/.turbo/turbo-test.log +19 -19
  3. package/dist/chunk-4SYIJ7CU.mjs +1538 -0
  4. package/dist/chunk-4XQVYYPC.mjs +1470 -0
  5. package/dist/chunk-5CLU3HYU.mjs +1475 -0
  6. package/dist/chunk-5K73Q3OQ.mjs +1520 -0
  7. package/dist/chunk-6AVS4KTM.mjs +1536 -0
  8. package/dist/chunk-6I4552YB.mjs +1467 -0
  9. package/dist/chunk-6LPITDKG.mjs +1539 -0
  10. package/dist/chunk-AECWO7NQ.mjs +1539 -0
  11. package/dist/chunk-AJC3FR6G.mjs +1509 -0
  12. package/dist/chunk-CVGIDSMN.mjs +1522 -0
  13. package/dist/chunk-DXG5NIYL.mjs +1527 -0
  14. package/dist/chunk-G3CCJCBI.mjs +1521 -0
  15. package/dist/chunk-GFADGYXZ.mjs +1752 -0
  16. package/dist/chunk-GTRIBVS6.mjs +1467 -0
  17. package/dist/chunk-H4HWBQU6.mjs +1530 -0
  18. package/dist/chunk-JH535NPP.mjs +1619 -0
  19. package/dist/chunk-KGFWKSGJ.mjs +1442 -0
  20. package/dist/chunk-N2GQWNFG.mjs +1527 -0
  21. package/dist/chunk-NQA3F2HJ.mjs +1532 -0
  22. package/dist/chunk-NXXQ2U73.mjs +1467 -0
  23. package/dist/chunk-QDGPR3L6.mjs +1518 -0
  24. package/dist/chunk-SAVOSPM3.mjs +1522 -0
  25. package/dist/chunk-SIX4KMF2.mjs +1468 -0
  26. package/dist/chunk-SPAM2YJE.mjs +1537 -0
  27. package/dist/chunk-UG7OPVHB.mjs +1521 -0
  28. package/dist/chunk-VIJTZPBI.mjs +1470 -0
  29. package/dist/chunk-W37E7MW5.mjs +1403 -0
  30. package/dist/chunk-W76FEISE.mjs +1538 -0
  31. package/dist/chunk-WCFQYXQA.mjs +1532 -0
  32. package/dist/chunk-XY77XABG.mjs +1545 -0
  33. package/dist/chunk-YCGDIGOG.mjs +1467 -0
  34. package/dist/cli.js +768 -1160
  35. package/dist/cli.mjs +1 -1
  36. package/dist/index.d.mts +196 -64
  37. package/dist/index.d.ts +196 -64
  38. package/dist/index.js +937 -1209
  39. package/dist/index.mjs +65 -3
  40. package/package.json +2 -2
  41. package/src/analyzer.ts +143 -2177
  42. package/src/ast-utils.ts +94 -0
  43. package/src/classifier.ts +497 -0
  44. package/src/cluster-detector.ts +100 -0
  45. package/src/defaults.ts +59 -0
  46. package/src/graph-builder.ts +272 -0
  47. package/src/index.ts +30 -519
  48. package/src/metrics.ts +231 -0
  49. package/src/remediation.ts +139 -0
  50. package/src/scoring.ts +12 -34
  51. package/src/semantic-analysis.ts +192 -126
  52. package/src/summary.ts +168 -0
package/src/index.ts CHANGED
@@ -6,126 +6,28 @@ import {
6
6
  calculateContextBudget,
7
7
  detectCircularDependencies,
8
8
  calculateCohesion,
9
- calculateFragmentation,
10
9
  detectModuleClusters,
11
- calculatePathEntropy,
12
- calculateDirectoryDistance,
13
10
  classifyFile,
14
11
  adjustCohesionForClassification,
15
12
  adjustFragmentationForClassification,
16
13
  getClassificationRecommendations,
14
+ analyzeIssues,
17
15
  } from './analyzer';
18
16
  import { calculateContextScore } from './scoring';
17
+ import { getSmartDefaults } from './defaults';
18
+ import { generateSummary } from './summary';
19
19
  import type {
20
20
  ContextAnalyzerOptions,
21
21
  ContextAnalysisResult,
22
22
  ContextSummary,
23
- ModuleCluster,
24
- DomainAssignment,
25
- DomainSignals,
26
- CoUsageData,
27
- TypeDependency,
28
- FileClassification,
29
23
  } from './types';
30
- import {
31
- buildCoUsageMatrix,
32
- buildTypeGraph,
33
- findSemanticClusters,
34
- calculateDomainConfidence,
35
- inferDomainFromSemantics,
36
- getCoUsageData,
37
- findConsolidationCandidates,
38
- } from './semantic-analysis';
39
- // Reference optional imports used by future heuristics to avoid lint warnings
40
- void calculateFragmentation;
41
-
42
- export type {
43
- ContextAnalyzerOptions,
44
- ContextAnalysisResult,
45
- ContextSummary,
46
- ModuleCluster,
47
- DomainAssignment,
48
- DomainSignals,
49
- CoUsageData,
50
- TypeDependency,
51
- FileClassification,
52
- };
53
-
54
- export { classifyFile, adjustFragmentationForClassification };
55
24
 
56
- export {
57
- buildCoUsageMatrix,
58
- buildTypeGraph,
59
- findSemanticClusters,
60
- calculateDomainConfidence,
61
- inferDomainFromSemantics,
62
- getCoUsageData,
63
- findConsolidationCandidates,
64
- };
65
-
66
- /**
67
- * Generate smart defaults for context analysis based on repository size
68
- * Automatically tunes thresholds to target ~10 most serious issues
69
- */
70
- async function getSmartDefaults(
71
- directory: string,
72
- userOptions: Partial<ContextAnalyzerOptions>
73
- ): Promise<ContextAnalyzerOptions> {
74
- // Estimate repository size by scanning files
75
- const files = await scanFiles({
76
- rootDir: directory,
77
- include: userOptions.include,
78
- exclude: userOptions.exclude,
79
- });
80
-
81
- const estimatedBlocks = files.length;
82
-
83
- // Smart defaults based on repository size
84
- // Adjusted to be stricter (higher thresholds) to catch only serious issues
85
- // This targets ~10 most critical issues instead of showing all files
86
- let maxDepth: number;
87
- let maxContextBudget: number;
88
- let minCohesion: number;
89
- let maxFragmentation: number;
90
-
91
- if (estimatedBlocks < 100) {
92
- // Small project - be more lenient
93
- maxDepth = 4;
94
- maxContextBudget = 8000;
95
- minCohesion = 0.5;
96
- maxFragmentation = 0.5;
97
- } else if (estimatedBlocks < 500) {
98
- // Medium project - moderate strictness
99
- maxDepth = 5;
100
- maxContextBudget = 15000;
101
- minCohesion = 0.45;
102
- maxFragmentation = 0.6;
103
- } else if (estimatedBlocks < 2000) {
104
- // Large project - stricter to focus on worst issues
105
- maxDepth = 7;
106
- maxContextBudget = 25000;
107
- minCohesion = 0.4;
108
- maxFragmentation = 0.7;
109
- } else {
110
- // Enterprise project - very strict to show only critical issues
111
- maxDepth = 10;
112
- maxContextBudget = 40000;
113
- minCohesion = 0.35;
114
- maxFragmentation = 0.8;
115
- }
116
-
117
- return {
118
- maxDepth,
119
- maxContextBudget,
120
- minCohesion,
121
- maxFragmentation,
122
- focus: 'all',
123
- includeNodeModules: false,
124
- rootDir: userOptions.rootDir || directory,
125
- include: userOptions.include,
126
- exclude: userOptions.exclude,
127
- };
128
- }
25
+ export * from './analyzer';
26
+ export * from './scoring';
27
+ export * from './defaults';
28
+ export * from './summary';
29
+ export * from './types';
30
+ export * from './semantic-analysis';
129
31
 
130
32
  /**
131
33
  * Analyze AI context window cost for a codebase
@@ -143,13 +45,8 @@ export async function analyzeContext(
143
45
  ...scanOptions
144
46
  } = options;
145
47
 
146
- // Scan files (supports .ts, .js, .tsx, .jsx, .py)
147
- // Note: scanFiles now automatically merges user excludes with DEFAULT_EXCLUDE
148
48
  const files = await scanFiles({
149
49
  ...scanOptions,
150
- // Only add node_modules to exclude if includeNodeModules is false
151
- // The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
152
- // if user overrides the default exclude list
153
50
  exclude:
154
51
  includeNodeModules && scanOptions.exclude
155
52
  ? scanOptions.exclude.filter(
@@ -158,12 +55,7 @@ export async function analyzeContext(
158
55
  : scanOptions.exclude,
159
56
  });
160
57
 
161
- // Separate files by language
162
58
  const pythonFiles = files.filter((f) => f.toLowerCase().endsWith('.py'));
163
- const tsJsFiles = files.filter((f) => !f.toLowerCase().endsWith('.py'));
164
- void tsJsFiles;
165
-
166
- // Read all file contents
167
59
  const fileContents = await Promise.all(
168
60
  files.map(async (file) => ({
169
61
  file,
@@ -171,12 +63,10 @@ export async function analyzeContext(
171
63
  }))
172
64
  );
173
65
 
174
- // Build dependency graph (TS/JS)
175
66
  const graph = buildDependencyGraph(
176
67
  fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
177
68
  );
178
69
 
179
- // Analyze Python files separately (if any)
180
70
  let pythonResults: ContextAnalysisResult[] = [];
181
71
  if (pythonFiles.length > 0) {
182
72
  const { analyzePythonContext } = await import('./analyzers/python-context');
@@ -185,7 +75,6 @@ export async function analyzeContext(
185
75
  scanOptions.rootDir || options.rootDir || '.'
186
76
  );
187
77
 
188
- // Convert Python metrics to ContextAnalysisResult format
189
78
  pythonResults = pythonMetrics.map((metric) => {
190
79
  const { severity, issues, recommendations, potentialSavings } =
191
80
  analyzeIssues({
@@ -193,7 +82,7 @@ export async function analyzeContext(
193
82
  importDepth: metric.importDepth,
194
83
  contextBudget: metric.contextBudget,
195
84
  cohesionScore: metric.cohesion,
196
- fragmentationScore: 0, // Python analyzer doesn't calculate fragmentation yet
85
+ fragmentationScore: 0,
197
86
  maxDepth,
198
87
  maxContextBudget,
199
88
  minCohesion,
@@ -207,7 +96,7 @@ export async function analyzeContext(
207
96
  file: metric.file,
208
97
  tokenCost: Math.floor(
209
98
  metric.contextBudget / (1 + metric.imports.length || 1)
210
- ), // Estimate
99
+ ),
211
100
  linesOfCode: metric.metrics.linesOfCode,
212
101
  importDepth: metric.importDepth,
213
102
  dependencyCount: metric.imports.length,
@@ -218,12 +107,12 @@ export async function analyzeContext(
218
107
  cycle.split(' → ')
219
108
  ),
220
109
  cohesionScore: metric.cohesion,
221
- domains: ['python'], // Generic for now
110
+ domains: ['python'],
222
111
  exportCount: metric.exports.length,
223
112
  contextBudget: metric.contextBudget,
224
113
  fragmentationScore: 0,
225
114
  relatedFiles: [],
226
- fileClassification: 'unknown' as const, // Python files not yet classified
115
+ fileClassification: 'unknown' as const,
227
116
  severity,
228
117
  issues,
229
118
  recommendations,
@@ -232,12 +121,8 @@ export async function analyzeContext(
232
121
  });
233
122
  }
234
123
 
235
- // Detect circular dependencies (TS/JS)
236
124
  const circularDeps = detectCircularDependencies(graph);
237
-
238
- // Detect module clusters for fragmentation analysis
239
- // Enable log-scaling for fragmentation by default on medium+ repos
240
- const useLogScale = files.length >= 500; // medium and larger projects
125
+ const useLogScale = files.length >= 500;
241
126
  const clusters = detectModuleClusters(graph, { useLogScale });
242
127
  const fragmentationMap = new Map<string, number>();
243
128
  for (const cluster of clusters) {
@@ -246,27 +131,22 @@ export async function analyzeContext(
246
131
  }
247
132
  }
248
133
 
249
- // Analyze each file
250
134
  const results: ContextAnalysisResult[] = [];
251
135
 
252
136
  for (const { file } of fileContents) {
253
137
  const node = graph.nodes.get(file);
254
138
  if (!node) continue;
255
139
 
256
- // Calculate metrics based on focus
257
140
  const importDepth =
258
141
  focus === 'depth' || focus === 'all'
259
142
  ? calculateImportDepth(file, graph)
260
143
  : 0;
261
-
262
144
  const dependencyList =
263
145
  focus === 'depth' || focus === 'all'
264
146
  ? getTransitiveDependencies(file, graph)
265
147
  : [];
266
-
267
148
  const contextBudget =
268
149
  focus === 'all' ? calculateContextBudget(file, graph) : node.tokenCost;
269
-
270
150
  const cohesionScore =
271
151
  focus === 'cohesion' || focus === 'all'
272
152
  ? calculateCohesion(node.exports, file, {
@@ -275,8 +155,6 @@ export async function analyzeContext(
275
155
  : 1;
276
156
 
277
157
  const fragmentationScore = fragmentationMap.get(file) || 0;
278
-
279
- // Find related files (files in same domain cluster)
280
158
  const relatedFiles: string[] = [];
281
159
  for (const cluster of clusters) {
282
160
  if (cluster.files.includes(file)) {
@@ -285,55 +163,38 @@ export async function analyzeContext(
285
163
  }
286
164
  }
287
165
 
288
- // Determine severity and generate issues/recommendations
289
- const { severity, issues, recommendations, potentialSavings } =
290
- analyzeIssues({
291
- file,
292
- importDepth,
293
- contextBudget,
294
- cohesionScore,
295
- fragmentationScore,
296
- maxDepth,
297
- maxContextBudget,
298
- minCohesion,
299
- maxFragmentation,
300
- circularDeps,
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;
166
+ const { issues } = analyzeIssues({
167
+ file,
168
+ importDepth,
169
+ contextBudget,
170
+ cohesionScore,
171
+ fragmentationScore,
172
+ maxDepth,
173
+ maxContextBudget,
174
+ minCohesion,
175
+ maxFragmentation,
176
+ circularDeps,
177
+ });
307
178
 
308
- // Get domains from exports
309
179
  const domains = [
310
180
  ...new Set(node.exports.map((e) => e.inferredDomain || 'unknown')),
311
181
  ];
312
-
313
- // Classify the file to help distinguish real issues from false positives
314
- const fileClassification = classifyFile(node, cohesionScore, domains);
315
-
316
- // Adjust cohesion based on classification (utility/service/handler files get boosted)
182
+ const fileClassification = classifyFile(node);
317
183
  const adjustedCohesionScore = adjustCohesionForClassification(
318
184
  cohesionScore,
319
185
  fileClassification,
320
186
  node
321
187
  );
322
-
323
- // Adjust fragmentation based on classification
324
188
  const adjustedFragmentationScore = adjustFragmentationForClassification(
325
189
  fragmentationScore,
326
190
  fileClassification
327
191
  );
328
-
329
- // Get classification-specific recommendations
330
192
  const classificationRecommendations = getClassificationRecommendations(
331
193
  fileClassification,
332
194
  file,
333
195
  issues
334
196
  );
335
197
 
336
- // Re-analyze issues with adjusted cohesion and fragmentation
337
198
  const {
338
199
  severity: adjustedSeverity,
339
200
  issues: adjustedIssues,
@@ -343,7 +204,7 @@ export async function analyzeContext(
343
204
  file,
344
205
  importDepth,
345
206
  contextBudget,
346
- cohesionScore: adjustedCohesionScore, // Use adjusted cohesion
207
+ cohesionScore: adjustedCohesionScore,
347
208
  fragmentationScore: adjustedFragmentationScore,
348
209
  maxDepth,
349
210
  maxContextBudget,
@@ -360,7 +221,7 @@ export async function analyzeContext(
360
221
  dependencyCount: dependencyList.length,
361
222
  dependencyList,
362
223
  circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
363
- cohesionScore: adjustedCohesionScore, // Report adjusted cohesion
224
+ cohesionScore: adjustedCohesionScore,
364
225
  domains,
365
226
  exportCount: node.exports.length,
366
227
  contextBudget,
@@ -377,361 +238,11 @@ export async function analyzeContext(
377
238
  });
378
239
  }
379
240
 
380
- // Merge Python and TS/JS results
381
241
  const allResults = [...results, ...pythonResults];
382
-
383
- // Keep ALL results including info severity for visualization purposes
384
- // The visualizer needs to show all files as nodes
385
- const sorted = allResults.sort((a, b) => {
242
+ return allResults.sort((a, b) => {
386
243
  const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
387
244
  const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
388
245
  if (severityDiff !== 0) return severityDiff;
389
246
  return b.contextBudget - a.contextBudget;
390
247
  });
391
-
392
- return sorted;
393
- }
394
-
395
- /**
396
- * Generate summary of context analysis results
397
- */
398
- export function generateSummary(
399
- results: ContextAnalysisResult[]
400
- ): ContextSummary {
401
- if (results.length === 0) {
402
- return {
403
- totalFiles: 0,
404
- totalTokens: 0,
405
- avgContextBudget: 0,
406
- maxContextBudget: 0,
407
- avgImportDepth: 0,
408
- maxImportDepth: 0,
409
- deepFiles: [],
410
- avgFragmentation: 0,
411
- fragmentedModules: [],
412
- avgCohesion: 0,
413
- lowCohesionFiles: [],
414
- criticalIssues: 0,
415
- majorIssues: 0,
416
- minorIssues: 0,
417
- totalPotentialSavings: 0,
418
- topExpensiveFiles: [],
419
- };
420
- }
421
-
422
- const totalFiles = results.length;
423
- const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
424
- const totalContextBudget = results.reduce(
425
- (sum, r) => sum + r.contextBudget,
426
- 0
427
- );
428
- const avgContextBudget = totalContextBudget / totalFiles;
429
- const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
430
-
431
- const avgImportDepth =
432
- results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
433
- const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
434
-
435
- const deepFiles = results
436
- .filter((r) => r.importDepth >= 5)
437
- .map((r) => ({ file: r.file, depth: r.importDepth }))
438
- .sort((a, b) => b.depth - a.depth)
439
- .slice(0, 10);
440
-
441
- const avgFragmentation =
442
- results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
443
-
444
- // Get unique module clusters
445
- const moduleMap = new Map<string, ContextAnalysisResult[]>();
446
- for (const result of results) {
447
- for (const domain of result.domains) {
448
- if (!moduleMap.has(domain)) {
449
- moduleMap.set(domain, []);
450
- }
451
- moduleMap.get(domain)!.push(result);
452
- }
453
- }
454
-
455
- const fragmentedModules: ModuleCluster[] = [];
456
- for (const [domain, files] of moduleMap.entries()) {
457
- if (files.length < 2) continue;
458
- const fragmentationScore =
459
- files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
460
- if (fragmentationScore < 0.3) continue; // Skip well-organized modules
461
-
462
- const totalTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
463
- const avgCohesion =
464
- files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
465
- const targetFiles = Math.max(1, Math.ceil(files.length / 3));
466
-
467
- // Compute path entropy and directory distance for reporting
468
- const filePaths = files.map((f) => f.file);
469
- const pathEntropy = calculatePathEntropy(filePaths);
470
- const directoryDistance = calculateDirectoryDistance(filePaths);
471
-
472
- // Compute import cohesion based on dependency lists (Jaccard similarity)
473
- function jaccard(a: string[], b: string[]) {
474
- const s1 = new Set(a || []);
475
- const s2 = new Set(b || []);
476
- if (s1.size === 0 && s2.size === 0) return 0;
477
- const inter = new Set([...s1].filter((x) => s2.has(x)));
478
- const uni = new Set([...s1, ...s2]);
479
- return uni.size === 0 ? 0 : inter.size / uni.size;
480
- }
481
-
482
- let importSimTotal = 0;
483
- let importPairs = 0;
484
- for (let i = 0; i < files.length; i++) {
485
- for (let j = i + 1; j < files.length; j++) {
486
- importSimTotal += jaccard(
487
- files[i].dependencyList || [],
488
- files[j].dependencyList || []
489
- );
490
- importPairs++;
491
- }
492
- }
493
-
494
- const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
495
-
496
- fragmentedModules.push({
497
- domain,
498
- files: files.map((f) => f.file),
499
- totalTokens,
500
- fragmentationScore,
501
- pathEntropy,
502
- directoryDistance,
503
- importCohesion,
504
- avgCohesion,
505
- suggestedStructure: {
506
- targetFiles,
507
- consolidationPlan: [
508
- `Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s)`,
509
- `Current token cost: ${totalTokens.toLocaleString()}`,
510
- `Estimated savings: ${Math.floor(totalTokens * 0.3).toLocaleString()} tokens (30%)`,
511
- ],
512
- },
513
- });
514
- }
515
-
516
- fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
517
-
518
- const avgCohesion =
519
- results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
520
-
521
- const lowCohesionFiles = results
522
- .filter((r) => r.cohesionScore < 0.6)
523
- .map((r) => ({ file: r.file, score: r.cohesionScore }))
524
- .sort((a, b) => a.score - b.score)
525
- .slice(0, 10);
526
-
527
- const criticalIssues = results.filter(
528
- (r) => r.severity === 'critical'
529
- ).length;
530
- const majorIssues = results.filter((r) => r.severity === 'major').length;
531
- const minorIssues = results.filter((r) => r.severity === 'minor').length;
532
-
533
- const totalPotentialSavings = results.reduce(
534
- (sum, r) => sum + r.potentialSavings,
535
- 0
536
- );
537
-
538
- const topExpensiveFiles = results
539
- .sort((a, b) => b.contextBudget - a.contextBudget)
540
- .slice(0, 10)
541
- .map((r) => ({
542
- file: r.file,
543
- contextBudget: r.contextBudget,
544
- severity: r.severity,
545
- }));
546
-
547
- return {
548
- totalFiles,
549
- totalTokens,
550
- avgContextBudget,
551
- maxContextBudget,
552
- avgImportDepth,
553
- maxImportDepth,
554
- deepFiles,
555
- avgFragmentation,
556
- fragmentedModules: fragmentedModules.slice(0, 10),
557
- avgCohesion,
558
- lowCohesionFiles,
559
- criticalIssues,
560
- majorIssues,
561
- minorIssues,
562
- totalPotentialSavings,
563
- topExpensiveFiles,
564
- };
565
- }
566
-
567
- /**
568
- * Analyze issues for a single file
569
- */
570
- function analyzeIssues(params: {
571
- file: string;
572
- importDepth: number;
573
- contextBudget: number;
574
- cohesionScore: number;
575
- fragmentationScore: number;
576
- maxDepth: number;
577
- maxContextBudget: number;
578
- minCohesion: number;
579
- maxFragmentation: number;
580
- circularDeps: string[][];
581
- }): {
582
- severity: ContextAnalysisResult['severity'];
583
- issues: string[];
584
- recommendations: string[];
585
- potentialSavings: number;
586
- } {
587
- const {
588
- file,
589
- importDepth,
590
- contextBudget,
591
- cohesionScore,
592
- fragmentationScore,
593
- maxDepth,
594
- maxContextBudget,
595
- minCohesion,
596
- maxFragmentation,
597
- circularDeps,
598
- } = params;
599
-
600
- const issues: string[] = [];
601
- const recommendations: string[] = [];
602
- let severity: ContextAnalysisResult['severity'] = 'info';
603
- let potentialSavings = 0;
604
-
605
- // Check circular dependencies (CRITICAL)
606
- if (circularDeps.length > 0) {
607
- severity = 'critical';
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'
611
- );
612
- potentialSavings += contextBudget * 0.2; // Estimate 20% savings
613
- }
614
-
615
- // Check import depth
616
- if (importDepth > maxDepth * 1.5) {
617
- severity = severity === 'critical' ? 'critical' : 'critical';
618
- issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
619
- recommendations.push('Flatten dependency tree or use facade pattern');
620
- potentialSavings += contextBudget * 0.3; // Estimate 30% savings
621
- } else if (importDepth > maxDepth) {
622
- severity = severity === 'critical' ? 'critical' : 'major';
623
- issues.push(
624
- `Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
625
- );
626
- recommendations.push('Consider reducing dependency depth');
627
- potentialSavings += contextBudget * 0.15;
628
- }
629
-
630
- // Check context budget
631
- if (contextBudget > maxContextBudget * 1.5) {
632
- severity = severity === 'critical' ? 'critical' : 'critical';
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
- );
639
- potentialSavings += contextBudget * 0.4; // Significant savings possible
640
- } else if (contextBudget > maxContextBudget) {
641
- severity =
642
- severity === 'critical' || severity === 'major' ? severity : 'major';
643
- issues.push(
644
- `Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
645
- );
646
- recommendations.push('Reduce file size or dependencies');
647
- potentialSavings += contextBudget * 0.2;
648
- }
649
-
650
- // Check cohesion
651
- if (cohesionScore < minCohesion * 0.5) {
652
- severity = severity === 'critical' ? 'critical' : 'major';
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
- );
659
- potentialSavings += contextBudget * 0.25;
660
- } else if (cohesionScore < minCohesion) {
661
- severity =
662
- severity === 'critical' || severity === 'major' ? severity : 'minor';
663
- issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
664
- recommendations.push('Consider grouping related exports together');
665
- potentialSavings += contextBudget * 0.1;
666
- }
667
-
668
- // Check fragmentation
669
- if (fragmentationScore > maxFragmentation) {
670
- severity =
671
- severity === 'critical' || severity === 'major' ? severity : 'minor';
672
- issues.push(
673
- `High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
674
- );
675
- recommendations.push('Consolidate with related files in same domain');
676
- potentialSavings += contextBudget * 0.3;
677
- }
678
-
679
- if (issues.length === 0) {
680
- issues.push('No significant issues detected');
681
- recommendations.push('File is well-structured for AI context usage');
682
- }
683
-
684
- // Detect build artifacts and downgrade severity to reduce noise
685
- if (isBuildArtifact(file)) {
686
- issues.push('Detected build artifact (bundled/output file)');
687
- recommendations.push(
688
- 'Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis'
689
- );
690
- severity = downgradeSeverity(severity);
691
- // Build artifacts do not represent actionable savings
692
- potentialSavings = 0;
693
- }
694
-
695
- return {
696
- severity,
697
- issues,
698
- recommendations,
699
- potentialSavings: Math.floor(potentialSavings),
700
- };
701
- }
702
-
703
- export { getSmartDefaults };
704
-
705
- /**
706
- * Heuristic: identify common build artifact paths
707
- */
708
- function isBuildArtifact(filePath: string): boolean {
709
- const lower = filePath.toLowerCase();
710
- return (
711
- lower.includes('/node_modules/') ||
712
- lower.includes('/dist/') ||
713
- lower.includes('/build/') ||
714
- lower.includes('/out/') ||
715
- lower.includes('/output/') ||
716
- lower.includes('/cdk.out/') ||
717
- lower.includes('/.next/') ||
718
- /\/asset\.[^/]+\//.test(lower) // e.g., cdk.out/asset.*
719
- );
720
248
  }
721
-
722
- function downgradeSeverity(
723
- s: ContextAnalysisResult['severity']
724
- ): ContextAnalysisResult['severity'] {
725
- switch (s) {
726
- case 'critical':
727
- return 'minor';
728
- case 'major':
729
- return 'minor';
730
- case 'minor':
731
- return 'info';
732
- default:
733
- return 'info';
734
- }
735
- }
736
-
737
- export { calculateContextScore };