@aiready/context-analyzer 0.21.5 → 0.21.6

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 (149) hide show
  1. package/.aiready/aiready-report-20260314-222254.json +39216 -0
  2. package/.aiready/aiready-report-20260314-223947.json +3413 -0
  3. package/.aiready/aiready-report-20260314-224112.json +3413 -0
  4. package/.aiready/aiready-report-20260314-224302.json +2973 -0
  5. package/.aiready/aiready-report-20260314-224939.json +3092 -0
  6. package/.aiready/aiready-report-20260314-225154.json +3092 -0
  7. package/.turbo/turbo-build.log +26 -24
  8. package/.turbo/turbo-test.log +41 -119
  9. package/dist/__tests__/analyzer.test.js +55 -14
  10. package/dist/__tests__/analyzer.test.js.map +1 -1
  11. package/dist/__tests__/cluster-detector.test.d.ts +2 -0
  12. package/dist/__tests__/cluster-detector.test.d.ts.map +1 -0
  13. package/dist/__tests__/cluster-detector.test.js +121 -0
  14. package/dist/__tests__/cluster-detector.test.js.map +1 -0
  15. package/dist/__tests__/contract.test.d.ts +2 -0
  16. package/dist/__tests__/contract.test.d.ts.map +1 -0
  17. package/dist/__tests__/contract.test.js +59 -0
  18. package/dist/__tests__/contract.test.js.map +1 -0
  19. package/dist/__tests__/enhanced-cohesion.test.js +12 -2
  20. package/dist/__tests__/enhanced-cohesion.test.js.map +1 -1
  21. package/dist/__tests__/file-classification.test.d.ts +2 -0
  22. package/dist/__tests__/file-classification.test.d.ts.map +1 -0
  23. package/dist/__tests__/file-classification.test.js +749 -0
  24. package/dist/__tests__/file-classification.test.js.map +1 -0
  25. package/dist/__tests__/fragmentation-advanced.test.js +2 -8
  26. package/dist/__tests__/fragmentation-advanced.test.js.map +1 -1
  27. package/dist/__tests__/fragmentation-coupling.test.js +2 -2
  28. package/dist/__tests__/fragmentation-coupling.test.js.map +1 -1
  29. package/dist/__tests__/fragmentation-log.test.js +3 -7
  30. package/dist/__tests__/fragmentation-log.test.js.map +1 -1
  31. package/dist/__tests__/provider.test.d.ts +2 -0
  32. package/dist/__tests__/provider.test.d.ts.map +1 -0
  33. package/dist/__tests__/provider.test.js +72 -0
  34. package/dist/__tests__/provider.test.js.map +1 -0
  35. package/dist/__tests__/remediation.test.d.ts +2 -0
  36. package/dist/__tests__/remediation.test.d.ts.map +1 -0
  37. package/dist/__tests__/remediation.test.js +61 -0
  38. package/dist/__tests__/remediation.test.js.map +1 -0
  39. package/dist/__tests__/scoring.test.js +196 -16
  40. package/dist/__tests__/scoring.test.js.map +1 -1
  41. package/dist/__tests__/structural-cohesion.test.js +8 -2
  42. package/dist/__tests__/structural-cohesion.test.js.map +1 -1
  43. package/dist/analyzer.d.ts +31 -94
  44. package/dist/analyzer.d.ts.map +1 -1
  45. package/dist/analyzer.js +260 -678
  46. package/dist/analyzer.js.map +1 -1
  47. package/dist/analyzers/python-context.d.ts.map +1 -1
  48. package/dist/analyzers/python-context.js +10 -8
  49. package/dist/analyzers/python-context.js.map +1 -1
  50. package/dist/ast-utils.d.ts +16 -0
  51. package/dist/ast-utils.d.ts.map +1 -0
  52. package/dist/ast-utils.js +81 -0
  53. package/dist/ast-utils.js.map +1 -0
  54. package/dist/chunk-64U3PNO3.mjs +94 -0
  55. package/dist/chunk-CDIVYADN.mjs +2110 -0
  56. package/dist/chunk-D3SIHB2V.mjs +2118 -0
  57. package/dist/chunk-FNPSK3CG.mjs +1760 -0
  58. package/dist/chunk-GXTGOLZT.mjs +92 -0
  59. package/dist/chunk-LERPI33Y.mjs +2060 -0
  60. package/dist/chunk-MZP3G7TF.mjs +2118 -0
  61. package/dist/chunk-NOHK5DLU.mjs +2173 -0
  62. package/dist/chunk-ORLC5Y4J.mjs +1787 -0
  63. package/dist/chunk-OTCQL7DY.mjs +2045 -0
  64. package/dist/chunk-SFK6XTJE.mjs +2110 -0
  65. package/dist/chunk-U5R2FTCR.mjs +1803 -0
  66. package/dist/chunk-UU4HZ7ZT.mjs +1849 -0
  67. package/dist/chunk-WKOZOHOU.mjs +2060 -0
  68. package/dist/chunk-XIXAWCMS.mjs +1760 -0
  69. package/dist/classifier.d.ts +114 -0
  70. package/dist/classifier.d.ts.map +1 -0
  71. package/dist/classifier.js +439 -0
  72. package/dist/classifier.js.map +1 -0
  73. package/dist/cli.js +590 -1071
  74. package/dist/cli.js.map +1 -1
  75. package/dist/cli.mjs +63 -533
  76. package/dist/cluster-detector.d.ts +8 -0
  77. package/dist/cluster-detector.d.ts.map +1 -0
  78. package/dist/cluster-detector.js +70 -0
  79. package/dist/cluster-detector.js.map +1 -0
  80. package/dist/defaults.d.ts +7 -0
  81. package/dist/defaults.d.ts.map +1 -0
  82. package/dist/defaults.js +54 -0
  83. package/dist/defaults.js.map +1 -0
  84. package/dist/graph-builder.d.ts +33 -0
  85. package/dist/graph-builder.d.ts.map +1 -0
  86. package/dist/graph-builder.js +225 -0
  87. package/dist/graph-builder.js.map +1 -0
  88. package/dist/index.d.mts +24 -31
  89. package/dist/index.d.ts +24 -31
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +788 -569
  92. package/dist/index.js.map +1 -1
  93. package/dist/index.mjs +265 -8
  94. package/dist/metrics.d.ts +34 -0
  95. package/dist/metrics.d.ts.map +1 -0
  96. package/dist/metrics.js +170 -0
  97. package/dist/metrics.js.map +1 -0
  98. package/dist/provider.d.ts +6 -0
  99. package/dist/provider.d.ts.map +1 -0
  100. package/dist/provider.js +48 -0
  101. package/dist/provider.js.map +1 -0
  102. package/dist/python-context-3GZKN3LR.mjs +162 -0
  103. package/dist/python-context-O2EN3M6Z.mjs +162 -0
  104. package/dist/remediation.d.ts +25 -0
  105. package/dist/remediation.d.ts.map +1 -0
  106. package/dist/remediation.js +98 -0
  107. package/dist/remediation.js.map +1 -0
  108. package/dist/scoring.d.ts +3 -7
  109. package/dist/scoring.d.ts.map +1 -1
  110. package/dist/scoring.js +57 -48
  111. package/dist/scoring.js.map +1 -1
  112. package/dist/semantic-analysis.d.ts +12 -23
  113. package/dist/semantic-analysis.d.ts.map +1 -1
  114. package/dist/semantic-analysis.js +172 -110
  115. package/dist/semantic-analysis.js.map +1 -1
  116. package/dist/summary.d.ts +6 -0
  117. package/dist/summary.d.ts.map +1 -0
  118. package/dist/summary.js +92 -0
  119. package/dist/summary.js.map +1 -0
  120. package/dist/types.d.ts +9 -2
  121. package/dist/types.d.ts.map +1 -1
  122. package/dist/utils/output-formatter.d.ts +14 -0
  123. package/dist/utils/output-formatter.d.ts.map +1 -0
  124. package/dist/utils/output-formatter.js +338 -0
  125. package/dist/utils/output-formatter.js.map +1 -0
  126. package/package.json +2 -2
  127. package/src/__tests__/analyzer.test.ts +1 -1
  128. package/src/__tests__/auto-detection.test.ts +1 -1
  129. package/src/__tests__/contract.test.ts +1 -1
  130. package/src/__tests__/enhanced-cohesion.test.ts +1 -1
  131. package/src/__tests__/file-classification.test.ts +1 -1
  132. package/src/__tests__/fragmentation-advanced.test.ts +1 -1
  133. package/src/__tests__/fragmentation-coupling.test.ts +1 -1
  134. package/src/__tests__/fragmentation-log.test.ts +1 -1
  135. package/src/__tests__/provider.test.ts +1 -1
  136. package/src/__tests__/structural-cohesion.test.ts +1 -1
  137. package/src/analyzer.ts +96 -309
  138. package/src/analyzers/python-context.ts +7 -76
  139. package/src/cli-action.ts +103 -0
  140. package/src/cli.ts +12 -693
  141. package/src/cluster-detector.ts +1 -1
  142. package/src/graph-builder.ts +9 -85
  143. package/src/index.ts +6 -0
  144. package/src/issue-analyzer.ts +143 -0
  145. package/src/semantic-analysis.ts +1 -14
  146. package/src/summary.ts +62 -106
  147. package/src/utils/dependency-graph-utils.ts +126 -0
  148. package/src/utils/output-formatter.ts +411 -0
  149. package/src/utils/string-utils.ts +17 -0
@@ -35,7 +35,7 @@ export function detectModuleClusters(
35
35
  (f) => new Set(graph.nodes.get(f)?.imports || [])
36
36
  );
37
37
  let intersection = new Set(allImportSets[0]);
38
- let union = new Set(allImportSets[0]);
38
+ const union = new Set(allImportSets[0]);
39
39
 
40
40
  for (let i = 1; i < allImportSets.length; i++) {
41
41
  const nextSet = allImportSets[i];
@@ -1,4 +1,10 @@
1
1
  import { estimateTokens, parseFileExports } from '@aiready/core';
2
+ import { singularize } from './utils/string-utils';
3
+ import {
4
+ calculateImportDepthFromEdges,
5
+ detectGraphCycles,
6
+ getTransitiveDependenciesFromEdges,
7
+ } from './utils/dependency-graph-utils';
2
8
  import type { DependencyGraph, DependencyNode } from './types';
3
9
  import {
4
10
  buildCoUsageMatrix,
@@ -94,25 +100,6 @@ export function extractDomainKeywordsFromPaths(files: FileContent[]): string[] {
94
100
  return Array.from(folderNames);
95
101
  }
96
102
 
97
- /**
98
- * Simple singularization for common English plurals
99
- */
100
- function singularize(word: string): string {
101
- const irregulars: Record<string, string> = {
102
- people: 'person',
103
- children: 'child',
104
- men: 'man',
105
- women: 'woman',
106
- };
107
-
108
- if (irregulars[word]) return irregulars[word];
109
- if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
110
- if (word.endsWith('ses')) return word.slice(0, -2);
111
- if (word.endsWith('s') && word.length > 3) return word.slice(0, -1);
112
-
113
- return word;
114
- }
115
-
116
103
  /**
117
104
  * Build a dependency graph from file contents
118
105
  */
@@ -196,23 +183,7 @@ export function calculateImportDepth(
196
183
  visited = new Set<string>(),
197
184
  depth = 0
198
185
  ): number {
199
- if (visited.has(file)) return depth;
200
-
201
- const dependencies = graph.edges.get(file);
202
- if (!dependencies || dependencies.size === 0) return depth;
203
-
204
- visited.add(file);
205
- let maxDepth = depth;
206
-
207
- for (const dep of dependencies) {
208
- maxDepth = Math.max(
209
- maxDepth,
210
- calculateImportDepth(dep, graph, visited, depth + 1)
211
- );
212
- }
213
-
214
- visited.delete(file);
215
- return maxDepth;
186
+ return calculateImportDepthFromEdges(file, graph.edges, visited, depth);
216
187
  }
217
188
 
218
189
  /**
@@ -223,19 +194,7 @@ export function getTransitiveDependencies(
223
194
  graph: DependencyGraph,
224
195
  visited = new Set<string>()
225
196
  ): string[] {
226
- if (visited.has(file)) return [];
227
-
228
- visited.add(file);
229
- const dependencies = graph.edges.get(file);
230
- if (!dependencies || dependencies.size === 0) return [];
231
-
232
- const allDeps: string[] = [];
233
- for (const dep of dependencies) {
234
- allDeps.push(dep);
235
- allDeps.push(...getTransitiveDependencies(dep, graph, visited));
236
- }
237
-
238
- return [...new Set(allDeps)];
197
+ return getTransitiveDependenciesFromEdges(file, graph.edges, visited);
239
198
  }
240
199
 
241
200
  /**
@@ -265,40 +224,5 @@ export function calculateContextBudget(
265
224
  * Detect circular dependencies
266
225
  */
267
226
  export function detectCircularDependencies(graph: DependencyGraph): string[][] {
268
- const cycles: string[][] = [];
269
- const visited = new Set<string>();
270
- const recursionStack = new Set<string>();
271
-
272
- function dfs(file: string, path: string[]): void {
273
- if (recursionStack.has(file)) {
274
- const cycleStart = path.indexOf(file);
275
- if (cycleStart !== -1) {
276
- cycles.push([...path.slice(cycleStart), file]);
277
- }
278
- return;
279
- }
280
-
281
- if (visited.has(file)) return;
282
-
283
- visited.add(file);
284
- recursionStack.add(file);
285
- path.push(file);
286
-
287
- const dependencies = graph.edges.get(file);
288
- if (dependencies) {
289
- for (const dep of dependencies) {
290
- dfs(dep, [...path]);
291
- }
292
- }
293
-
294
- recursionStack.delete(file);
295
- }
296
-
297
- for (const file of graph.nodes.keys()) {
298
- if (!visited.has(file)) {
299
- dfs(file, []);
300
- }
301
- }
302
-
303
- return cycles;
227
+ return detectGraphCycles(graph.edges);
304
228
  }
package/src/index.ts CHANGED
@@ -5,9 +5,15 @@ import { ContextAnalyzerProvider } from './provider';
5
5
  ToolRegistry.register(ContextAnalyzerProvider);
6
6
 
7
7
  export * from './analyzer';
8
+ export * from './graph-builder';
9
+ export * from './metrics';
10
+ export * from './classifier';
11
+ export * from './cluster-detector';
12
+ export * from './remediation';
8
13
  export * from './scoring';
9
14
  export * from './defaults';
10
15
  export * from './summary';
11
16
  export * from './types';
12
17
  export * from './semantic-analysis';
13
18
  export { ContextAnalyzerProvider };
19
+ export * from './utils/output-formatter';
@@ -0,0 +1,143 @@
1
+ import { Severity } from '@aiready/core';
2
+
3
+ /**
4
+ * Internal issue analysis logic
5
+ */
6
+ export function analyzeIssues(params: {
7
+ file: string;
8
+ importDepth: number;
9
+ contextBudget: number;
10
+ cohesionScore: number;
11
+ fragmentationScore: number;
12
+ maxDepth: number;
13
+ maxContextBudget: number;
14
+ minCohesion: number;
15
+ maxFragmentation: number;
16
+ circularDeps: string[][];
17
+ }): {
18
+ severity: Severity;
19
+ issues: string[];
20
+ recommendations: string[];
21
+ potentialSavings: number;
22
+ } {
23
+ const {
24
+ file,
25
+ importDepth,
26
+ contextBudget,
27
+ cohesionScore,
28
+ fragmentationScore,
29
+ maxDepth,
30
+ maxContextBudget,
31
+ minCohesion,
32
+ maxFragmentation,
33
+ circularDeps,
34
+ } = params;
35
+
36
+ const issues: string[] = [];
37
+ const recommendations: string[] = [];
38
+ let severity: Severity = Severity.Info;
39
+ let potentialSavings = 0;
40
+
41
+ // Check circular dependencies (CRITICAL)
42
+ if (circularDeps.length > 0) {
43
+ severity = Severity.Critical;
44
+ issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
45
+ recommendations.push(
46
+ 'Break circular dependencies by extracting interfaces or using dependency injection'
47
+ );
48
+ potentialSavings += contextBudget * 0.2;
49
+ }
50
+
51
+ // Check import depth
52
+ if (importDepth > maxDepth * 1.5) {
53
+ severity = Severity.Critical;
54
+ issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
55
+ recommendations.push('Flatten dependency tree or use facade pattern');
56
+ potentialSavings += contextBudget * 0.3;
57
+ } else if (importDepth > maxDepth) {
58
+ if (severity !== Severity.Critical) severity = Severity.Major;
59
+ issues.push(
60
+ `Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
61
+ );
62
+ recommendations.push('Consider reducing dependency depth');
63
+ potentialSavings += contextBudget * 0.15;
64
+ }
65
+
66
+ // Check context budget
67
+ if (contextBudget > maxContextBudget * 1.5) {
68
+ severity = Severity.Critical;
69
+ issues.push(
70
+ `Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
71
+ );
72
+ recommendations.push(
73
+ 'Split into smaller modules or reduce dependency tree'
74
+ );
75
+ potentialSavings += contextBudget * 0.4;
76
+ } else if (contextBudget > maxContextBudget) {
77
+ if (severity !== Severity.Critical) severity = Severity.Major;
78
+ issues.push(
79
+ `Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
80
+ );
81
+ recommendations.push('Reduce file size or dependencies');
82
+ potentialSavings += contextBudget * 0.2;
83
+ }
84
+
85
+ // Check cohesion
86
+ if (cohesionScore < minCohesion * 0.5) {
87
+ if (severity !== Severity.Critical) severity = Severity.Major;
88
+ issues.push(
89
+ `Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
90
+ );
91
+ recommendations.push(
92
+ 'Split file by domain - separate unrelated functionality'
93
+ );
94
+ potentialSavings += contextBudget * 0.25;
95
+ } else if (cohesionScore < minCohesion) {
96
+ if (severity === Severity.Info) severity = Severity.Minor;
97
+ issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
98
+ recommendations.push('Consider grouping related exports together');
99
+ potentialSavings += contextBudget * 0.1;
100
+ }
101
+
102
+ // Check fragmentation
103
+ if (fragmentationScore > maxFragmentation) {
104
+ if (severity === Severity.Info || severity === Severity.Minor)
105
+ severity = Severity.Minor;
106
+ issues.push(
107
+ `High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
108
+ );
109
+ recommendations.push('Consolidate with related files in same domain');
110
+ potentialSavings += contextBudget * 0.3;
111
+ }
112
+
113
+ if (issues.length === 0) {
114
+ issues.push('No significant issues detected');
115
+ recommendations.push('File is well-structured for AI context usage');
116
+ }
117
+
118
+ // Detect build artifacts
119
+ if (isBuildArtifact(file)) {
120
+ issues.push('Detected build artifact (bundled/output file)');
121
+ recommendations.push('Exclude build outputs from analysis');
122
+ severity = Severity.Info;
123
+ potentialSavings = 0;
124
+ }
125
+
126
+ return {
127
+ severity,
128
+ issues,
129
+ recommendations,
130
+ potentialSavings: Math.floor(potentialSavings),
131
+ };
132
+ }
133
+
134
+ export function isBuildArtifact(filePath: string): boolean {
135
+ const lower = filePath.toLowerCase();
136
+ return (
137
+ lower.includes('/node_modules/') ||
138
+ lower.includes('/dist/') ||
139
+ lower.includes('/build/') ||
140
+ lower.includes('/out/') ||
141
+ lower.includes('/.next/')
142
+ );
143
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  DomainSignals,
6
6
  ExportInfo,
7
7
  } from './types';
8
+ import { singularize } from './utils/string-utils';
8
9
 
9
10
  /**
10
11
  * Build co-usage matrix: track which files are imported together
@@ -312,20 +313,6 @@ export function inferDomain(
312
313
  return 'unknown';
313
314
  }
314
315
 
315
- function singularize(word: string): string {
316
- const irregulars: Record<string, string> = {
317
- people: 'person',
318
- children: 'child',
319
- men: 'man',
320
- women: 'woman',
321
- };
322
- if (irregulars[word]) return irregulars[word];
323
- if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
324
- if (word.endsWith('ses')) return word.slice(0, -2);
325
- if (word.endsWith('s') && word.length > 3) return word.slice(0, -1);
326
- return word;
327
- }
328
-
329
316
  export function getCoUsageData(
330
317
  file: string,
331
318
  coUsageMatrix: Map<string, Map<string, number>>
package/src/summary.ts CHANGED
@@ -3,7 +3,7 @@ import type {
3
3
  ContextSummary,
4
4
  ModuleCluster,
5
5
  } from './types';
6
- import { calculatePathEntropy, calculateDirectoryDistance } from './analyzer';
6
+ import { calculatePathEntropy, calculateDirectoryDistance } from './metrics';
7
7
  import { GLOBAL_SCAN_OPTIONS } from '@aiready/core';
8
8
 
9
9
  /**
@@ -11,7 +11,7 @@ import { GLOBAL_SCAN_OPTIONS } from '@aiready/core';
11
11
  */
12
12
  export function generateSummary(
13
13
  results: ContextAnalysisResult[],
14
- options?: any
14
+ options: any = {}
15
15
  ): ContextSummary {
16
16
  const config = options
17
17
  ? Object.fromEntries(
@@ -19,137 +19,92 @@ export function generateSummary(
19
19
  ([key]) => !GLOBAL_SCAN_OPTIONS.includes(key) || key === 'rootDir'
20
20
  )
21
21
  )
22
- : undefined;
23
-
24
- if (results.length === 0) {
25
- return {
26
- totalFiles: 0,
27
- totalTokens: 0,
28
- avgContextBudget: 0,
29
- maxContextBudget: 0,
30
- avgImportDepth: 0,
31
- maxImportDepth: 0,
32
- deepFiles: [],
33
- avgFragmentation: 0,
34
- fragmentedModules: [],
35
- avgCohesion: 0,
36
- lowCohesionFiles: [],
37
- criticalIssues: 0,
38
- majorIssues: 0,
39
- minorIssues: 0,
40
- totalPotentialSavings: 0,
41
- topExpensiveFiles: [],
42
- config,
43
- };
44
- }
22
+ : {};
45
23
 
46
24
  const totalFiles = results.length;
47
25
  const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
48
- const totalContextBudget = results.reduce(
49
- (sum, r) => sum + r.contextBudget,
50
- 0
51
- );
52
- const avgContextBudget = totalContextBudget / totalFiles;
53
- const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
54
-
55
- const avgImportDepth =
56
- results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
57
- const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
26
+ const avgContextBudget =
27
+ totalFiles > 0
28
+ ? results.reduce((sum, r) => sum + r.contextBudget, 0) / totalFiles
29
+ : 0;
58
30
 
31
+ // Find deep files
59
32
  const deepFiles = results
60
- .filter((r) => r.importDepth >= 5)
61
- .map((r) => ({ file: r.file, depth: r.importDepth }))
62
- .sort((a, b) => b.depth - a.depth)
63
- .slice(0, 10);
33
+ .filter((r) => r.importDepth > 5)
34
+ .map((r) => ({ file: r.file, depth: r.importDepth }));
64
35
 
65
- const avgFragmentation =
66
- results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
36
+ const maxImportDepth = Math.max(0, ...results.map((r) => r.importDepth));
67
37
 
38
+ // Find fragmented modules (clusters)
68
39
  const moduleMap = new Map<string, ContextAnalysisResult[]>();
69
- for (const result of results) {
70
- for (const domain of result.domains) {
71
- if (!moduleMap.has(domain)) moduleMap.set(domain, []);
72
- moduleMap.get(domain)!.push(result);
40
+ results.forEach((r) => {
41
+ const parts = r.file.split('/');
42
+ // Try to identify domain/module (e.g., packages/core, src/utils)
43
+ let domain = 'root';
44
+ if (parts.length > 2) {
45
+ domain = parts.slice(0, 2).join('/');
73
46
  }
74
- }
47
+ if (!moduleMap.has(domain)) moduleMap.set(domain, []);
48
+ moduleMap.get(domain)!.push(r);
49
+ });
75
50
 
76
51
  const fragmentedModules: ModuleCluster[] = [];
77
- for (const [domain, files] of moduleMap.entries()) {
78
- if (files.length < 2) continue;
79
- const fragmentationScore =
80
- files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
81
- if (fragmentationScore < 0.3) continue;
82
-
83
- const totalTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
84
- const avgCohesion =
85
- files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
86
- const targetFiles = Math.max(1, Math.ceil(files.length / 3));
87
-
52
+ moduleMap.forEach((files, domain) => {
53
+ const clusterTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
88
54
  const filePaths = files.map((f) => f.file);
89
- const pathEntropy = calculatePathEntropy(filePaths);
90
- const directoryDistance = calculateDirectoryDistance(filePaths);
91
-
92
- function jaccard(a: string[], b: string[]) {
93
- const s1 = new Set(a || []);
94
- const s2 = new Set(b || []);
95
- if (s1.size === 0 && s2.size === 0) return 0;
96
- const inter = new Set([...s1].filter((x) => s2.has(x)));
97
- const uni = new Set([...s1, ...s2]);
98
- return uni.size === 0 ? 0 : inter.size / uni.size;
55
+ const avgEntropy = calculatePathEntropy(filePaths);
56
+
57
+ // A module is fragmented if it has many files with high directory distance
58
+ // and relatively low cohesion
59
+ const fragmentationScore = Math.min(1, avgEntropy * (files.length / 10));
60
+
61
+ if (fragmentationScore > 0.4) {
62
+ fragmentedModules.push({
63
+ domain,
64
+ files: filePaths,
65
+ fragmentationScore,
66
+ totalTokens: clusterTokens,
67
+ avgCohesion:
68
+ files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length,
69
+ suggestedStructure: {
70
+ targetFiles: Math.ceil(files.length / 2),
71
+ consolidationPlan: [
72
+ `Consolidate ${files.length} files in ${domain} into fewer modules`,
73
+ ],
74
+ },
75
+ });
99
76
  }
77
+ });
100
78
 
101
- let importSimTotal = 0;
102
- let importPairs = 0;
103
- for (let i = 0; i < files.length; i++) {
104
- for (let j = i + 1; j < files.length; j++) {
105
- importSimTotal += jaccard(
106
- files[i].dependencyList || [],
107
- files[j].dependencyList || []
108
- );
109
- importPairs++;
110
- }
111
- }
79
+ fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
112
80
 
113
- const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
114
-
115
- fragmentedModules.push({
116
- domain,
117
- files: files.map((f) => f.file),
118
- totalTokens,
119
- fragmentationScore,
120
- avgCohesion,
121
- importCohesion,
122
- pathEntropy,
123
- directoryDistance,
124
- suggestedStructure: {
125
- targetFiles,
126
- consolidationPlan: [
127
- `Consolidate ${files.length} files across ${new Set(files.map((f) => f.file.split('/').slice(0, -1).join('/'))).size} directories`,
128
- `Target ~${targetFiles} core modules to reduce context switching`,
129
- ],
130
- },
131
- });
132
- }
81
+ const avgFragmentation =
82
+ fragmentedModules.length > 0
83
+ ? fragmentedModules.reduce((sum, m) => sum + m.fragmentationScore, 0) /
84
+ fragmentedModules.length
85
+ : 0;
133
86
 
87
+ // Cohesion
134
88
  const avgCohesion =
135
- results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
89
+ results.reduce((sum, r) => sum + r.cohesionScore, 0) / (totalFiles || 1);
90
+
136
91
  const lowCohesionFiles = results
137
92
  .filter((r) => r.cohesionScore < 0.4)
138
- .map((r) => ({ file: r.file, score: r.cohesionScore }))
139
- .sort((a, b) => a.score - b.score)
140
- .slice(0, 10);
93
+ .map((r) => ({ file: r.file, score: r.cohesionScore }));
141
94
 
95
+ // Issues
142
96
  const criticalIssues = results.filter(
143
97
  (r) => r.severity === 'critical'
144
98
  ).length;
145
99
  const majorIssues = results.filter((r) => r.severity === 'major').length;
146
100
  const minorIssues = results.filter((r) => r.severity === 'minor').length;
101
+
147
102
  const totalPotentialSavings = results.reduce(
148
- (sum, r) => sum + r.potentialSavings,
103
+ (sum, r) => sum + (r.potentialSavings || 0),
149
104
  0
150
105
  );
151
106
 
152
- const topExpensiveFiles = results
107
+ const topExpensiveFiles = [...results]
153
108
  .sort((a, b) => b.contextBudget - a.contextBudget)
154
109
  .slice(0, 10)
155
110
  .map((r) => ({
@@ -162,8 +117,9 @@ export function generateSummary(
162
117
  totalFiles,
163
118
  totalTokens,
164
119
  avgContextBudget,
165
- maxContextBudget,
166
- avgImportDepth,
120
+ maxContextBudget: Math.max(0, ...results.map((r) => r.contextBudget)),
121
+ avgImportDepth:
122
+ results.reduce((sum, r) => sum + r.importDepth, 0) / (totalFiles || 1),
167
123
  maxImportDepth,
168
124
  deepFiles,
169
125
  avgFragmentation,
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Shared dependency graph utilities used by context analyzers.
3
+ */
4
+
5
+ export function calculateImportDepthFromEdges(
6
+ file: string,
7
+ edges: Map<string, Set<string>>,
8
+ visited = new Set<string>(),
9
+ depth = 0
10
+ ): number {
11
+ if (visited.has(file)) return depth;
12
+
13
+ const dependencies = edges.get(file);
14
+ if (!dependencies || dependencies.size === 0) return depth;
15
+
16
+ const nextVisited = new Set(visited);
17
+ nextVisited.add(file);
18
+
19
+ let maxDepth = depth;
20
+ for (const dep of dependencies) {
21
+ maxDepth = Math.max(
22
+ maxDepth,
23
+ calculateImportDepthFromEdges(dep, edges, nextVisited, depth + 1)
24
+ );
25
+ }
26
+
27
+ return maxDepth;
28
+ }
29
+
30
+ export function getTransitiveDependenciesFromEdges(
31
+ file: string,
32
+ edges: Map<string, Set<string>>,
33
+ visited = new Set<string>()
34
+ ): string[] {
35
+ if (visited.has(file)) return [];
36
+
37
+ const nextVisited = new Set(visited);
38
+ nextVisited.add(file);
39
+
40
+ const dependencies = edges.get(file);
41
+ if (!dependencies || dependencies.size === 0) return [];
42
+
43
+ const allDeps: string[] = [];
44
+ for (const dep of dependencies) {
45
+ allDeps.push(dep);
46
+ allDeps.push(
47
+ ...getTransitiveDependenciesFromEdges(dep, edges, nextVisited)
48
+ );
49
+ }
50
+
51
+ return [...new Set(allDeps)];
52
+ }
53
+
54
+ export function detectGraphCycles(edges: Map<string, Set<string>>): string[][] {
55
+ const cycles: string[][] = [];
56
+ const visited = new Set<string>();
57
+ const recursionStack = new Set<string>();
58
+
59
+ function dfs(file: string, path: string[]): void {
60
+ if (recursionStack.has(file)) {
61
+ const cycleStart = path.indexOf(file);
62
+ if (cycleStart !== -1) {
63
+ cycles.push([...path.slice(cycleStart), file]);
64
+ }
65
+ return;
66
+ }
67
+
68
+ if (visited.has(file)) return;
69
+
70
+ visited.add(file);
71
+ recursionStack.add(file);
72
+
73
+ const dependencies = edges.get(file);
74
+ if (dependencies) {
75
+ for (const dep of dependencies) {
76
+ dfs(dep, [...path, file]);
77
+ }
78
+ }
79
+
80
+ recursionStack.delete(file);
81
+ }
82
+
83
+ for (const file of edges.keys()) {
84
+ if (!visited.has(file)) {
85
+ dfs(file, []);
86
+ }
87
+ }
88
+
89
+ return cycles;
90
+ }
91
+
92
+ export function detectGraphCyclesFromFile(
93
+ file: string,
94
+ edges: Map<string, Set<string>>
95
+ ): string[][] {
96
+ const cycles: string[][] = [];
97
+ const visited = new Set<string>();
98
+ const recursionStack = new Set<string>();
99
+
100
+ function dfs(current: string, path: string[]): void {
101
+ if (recursionStack.has(current)) {
102
+ const cycleStart = path.indexOf(current);
103
+ if (cycleStart !== -1) {
104
+ cycles.push([...path.slice(cycleStart), current]);
105
+ }
106
+ return;
107
+ }
108
+
109
+ if (visited.has(current)) return;
110
+
111
+ visited.add(current);
112
+ recursionStack.add(current);
113
+
114
+ const dependencies = edges.get(current);
115
+ if (dependencies) {
116
+ for (const dep of dependencies) {
117
+ dfs(dep, [...path, current]);
118
+ }
119
+ }
120
+
121
+ recursionStack.delete(current);
122
+ }
123
+
124
+ dfs(file, []);
125
+ return cycles;
126
+ }