@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
@@ -3,41 +3,30 @@ import type {
3
3
  CoUsageData,
4
4
  DomainAssignment,
5
5
  DomainSignals,
6
+ ExportInfo,
6
7
  } from './types';
7
8
 
8
9
  /**
9
10
  * Build co-usage matrix: track which files are imported together
10
- *
11
- * Files frequently imported together likely belong to the same semantic domain
12
11
  */
13
12
  export function buildCoUsageMatrix(
14
13
  graph: DependencyGraph
15
14
  ): Map<string, Map<string, number>> {
16
15
  const coUsageMatrix = new Map<string, Map<string, number>>();
17
16
 
18
- // For each file, track which other files are imported alongside it
19
- for (const [sourceFile, node] of graph.nodes) {
20
- void sourceFile;
17
+ for (const [, node] of graph.nodes) {
21
18
  const imports = node.imports;
22
19
 
23
- // For each pair of imports in this file, increment their co-usage count
24
20
  for (let i = 0; i < imports.length; i++) {
25
21
  const fileA = imports[i];
26
-
27
- if (!coUsageMatrix.has(fileA)) {
28
- coUsageMatrix.set(fileA, new Map());
29
- }
22
+ if (!coUsageMatrix.has(fileA)) coUsageMatrix.set(fileA, new Map());
30
23
 
31
24
  for (let j = i + 1; j < imports.length; j++) {
32
25
  const fileB = imports[j];
33
-
34
- // Increment bidirectional co-usage count
35
26
  const fileAUsage = coUsageMatrix.get(fileA)!;
36
27
  fileAUsage.set(fileB, (fileAUsage.get(fileB) || 0) + 1);
37
28
 
38
- if (!coUsageMatrix.has(fileB)) {
39
- coUsageMatrix.set(fileB, new Map());
40
- }
29
+ if (!coUsageMatrix.has(fileB)) coUsageMatrix.set(fileB, new Map());
41
30
  const fileBUsage = coUsageMatrix.get(fileB)!;
42
31
  fileBUsage.set(fileA, (fileBUsage.get(fileA) || 0) + 1);
43
32
  }
@@ -49,8 +38,6 @@ export function buildCoUsageMatrix(
49
38
 
50
39
  /**
51
40
  * Extract type dependencies from AST exports
52
- *
53
- * Files that share types are semantically related
54
41
  */
55
42
  export function buildTypeGraph(
56
43
  graph: DependencyGraph
@@ -61,9 +48,7 @@ export function buildTypeGraph(
61
48
  for (const exp of node.exports) {
62
49
  if (exp.typeReferences) {
63
50
  for (const typeRef of exp.typeReferences) {
64
- if (!typeGraph.has(typeRef)) {
65
- typeGraph.set(typeRef, new Set());
66
- }
51
+ if (!typeGraph.has(typeRef)) typeGraph.set(typeRef, new Set());
67
52
  typeGraph.get(typeRef)!.add(file);
68
53
  }
69
54
  }
@@ -75,8 +60,6 @@ export function buildTypeGraph(
75
60
 
76
61
  /**
77
62
  * Find semantic clusters using co-usage patterns
78
- *
79
- * Files with high co-usage counts belong in the same cluster
80
63
  */
81
64
  export function findSemanticClusters(
82
65
  coUsageMatrix: Map<string, Map<string, number>>,
@@ -85,14 +68,12 @@ export function findSemanticClusters(
85
68
  const clusters = new Map<string, string[]>();
86
69
  const visited = new Set<string>();
87
70
 
88
- // Simple clustering: group files with high co-usage
89
71
  for (const [file, coUsages] of coUsageMatrix) {
90
72
  if (visited.has(file)) continue;
91
73
 
92
74
  const cluster: string[] = [file];
93
75
  visited.add(file);
94
76
 
95
- // Find strongly related files (co-imported >= minCoUsage times)
96
77
  for (const [relatedFile, count] of coUsages) {
97
78
  if (count >= minCoUsage && !visited.has(relatedFile)) {
98
79
  cluster.push(relatedFile);
@@ -100,41 +81,14 @@ export function findSemanticClusters(
100
81
  }
101
82
  }
102
83
 
103
- if (cluster.length > 1) {
104
- // Use first file as cluster ID
105
- clusters.set(file, cluster);
106
- }
84
+ if (cluster.length > 1) clusters.set(file, cluster);
107
85
  }
108
86
 
109
87
  return clusters;
110
88
  }
111
89
 
112
- /**
113
- * Calculate confidence score for domain assignment based on multiple signals
114
- */
115
- export function calculateDomainConfidence(signals: DomainSignals): number {
116
- const weights = {
117
- coUsage: 0.35, // Strongest signal: actual usage patterns
118
- typeReference: 0.3, // Strong signal: shared types
119
- exportName: 0.15, // Medium signal: identifier semantics
120
- importPath: 0.1, // Weaker signal: path structure
121
- folderStructure: 0.1, // Weakest signal: organization convention
122
- };
123
-
124
- let confidence = 0;
125
- if (signals.coUsage) confidence += weights.coUsage;
126
- if (signals.typeReference) confidence += weights.typeReference;
127
- if (signals.exportName) confidence += weights.exportName;
128
- if (signals.importPath) confidence += weights.importPath;
129
- if (signals.folderStructure) confidence += weights.folderStructure;
130
-
131
- return confidence;
132
- }
133
-
134
90
  /**
135
91
  * Infer domain from semantic analysis (co-usage + types)
136
- *
137
- * This replaces the folder-based heuristic with actual code relationships
138
92
  */
139
93
  export function inferDomainFromSemantics(
140
94
  file: string,
@@ -144,16 +98,13 @@ export function inferDomainFromSemantics(
144
98
  typeGraph: Map<string, Set<string>>,
145
99
  exportTypeRefs?: string[]
146
100
  ): DomainAssignment[] {
147
- const assignments: DomainAssignment[] = [];
148
101
  const domainSignals = new Map<string, DomainSignals>();
149
102
 
150
- // 1. Check co-usage patterns
151
103
  const coUsages = coUsageMatrix.get(file) || new Map();
152
104
  const strongCoUsages = Array.from(coUsages.entries())
153
105
  .filter(([, count]) => count >= 3)
154
106
  .map(([coFile]) => coFile);
155
107
 
156
- // Extract domains from frequently co-imported files
157
108
  for (const coFile of strongCoUsages) {
158
109
  const coNode = graph.nodes.get(coFile);
159
110
  if (coNode) {
@@ -175,29 +126,27 @@ export function inferDomainFromSemantics(
175
126
  }
176
127
  }
177
128
 
178
- // 2. Check type references
179
129
  if (exportTypeRefs) {
180
130
  for (const typeRef of exportTypeRefs) {
181
131
  const filesWithType = typeGraph.get(typeRef);
182
132
  if (filesWithType) {
183
133
  for (const typeFile of filesWithType) {
184
- if (typeFile !== file) {
185
- const typeNode = graph.nodes.get(typeFile);
186
- if (typeNode) {
187
- for (const exp of typeNode.exports) {
188
- if (exp.inferredDomain && exp.inferredDomain !== 'unknown') {
189
- const domain = exp.inferredDomain;
190
- if (!domainSignals.has(domain)) {
191
- domainSignals.set(domain, {
192
- coUsage: false,
193
- typeReference: false,
194
- exportName: false,
195
- importPath: false,
196
- folderStructure: false,
197
- });
198
- }
199
- domainSignals.get(domain)!.typeReference = true;
134
+ if (typeFile === file) continue;
135
+ const typeNode = graph.nodes.get(typeFile);
136
+ if (typeNode) {
137
+ for (const exp of typeNode.exports) {
138
+ if (exp.inferredDomain && exp.inferredDomain !== 'unknown') {
139
+ const domain = exp.inferredDomain;
140
+ if (!domainSignals.has(domain)) {
141
+ domainSignals.set(domain, {
142
+ coUsage: false,
143
+ typeReference: false,
144
+ exportName: false,
145
+ importPath: false,
146
+ folderStructure: false,
147
+ });
200
148
  }
149
+ domainSignals.get(domain)!.typeReference = true;
201
150
  }
202
151
  }
203
152
  }
@@ -206,72 +155,203 @@ export function inferDomainFromSemantics(
206
155
  }
207
156
  }
208
157
 
209
- // 3. Build domain assignments with confidence scores
158
+ const assignments: DomainAssignment[] = [];
210
159
  for (const [domain, signals] of domainSignals) {
211
160
  const confidence = calculateDomainConfidence(signals);
212
- if (confidence >= 0.3) {
213
- // Minimum confidence threshold
214
- assignments.push({ domain, confidence, signals });
215
- }
161
+ if (confidence >= 0.3) assignments.push({ domain, confidence, signals });
216
162
  }
217
163
 
218
- // Sort by confidence (highest first)
219
164
  assignments.sort((a, b) => b.confidence - a.confidence);
220
-
221
165
  return assignments;
222
166
  }
223
167
 
168
+ export function calculateDomainConfidence(signals: DomainSignals): number {
169
+ const weights = {
170
+ coUsage: 0.35,
171
+ typeReference: 0.3,
172
+ exportName: 0.15,
173
+ importPath: 0.1,
174
+ folderStructure: 0.1,
175
+ };
176
+ let confidence = 0;
177
+ if (signals.coUsage) confidence += weights.coUsage;
178
+ if (signals.typeReference) confidence += weights.typeReference;
179
+ if (signals.exportName) confidence += weights.exportName;
180
+ if (signals.importPath) confidence += weights.importPath;
181
+ if (signals.folderStructure) confidence += weights.folderStructure;
182
+ return confidence;
183
+ }
184
+
224
185
  /**
225
- * Get co-usage data for a specific file
186
+ * Regex-based export extraction (legacy/fallback)
226
187
  */
188
+ export function extractExports(
189
+ content: string,
190
+ filePath?: string,
191
+ domainOptions?: { domainKeywords?: string[] },
192
+ fileImports?: string[]
193
+ ): ExportInfo[] {
194
+ const exports: ExportInfo[] = [];
195
+ const patterns = [
196
+ /export\s+function\s+(\w+)/g,
197
+ /export\s+class\s+(\w+)/g,
198
+ /export\s+const\s+(\w+)/g,
199
+ /export\s+type\s+(\w+)/g,
200
+ /export\s+interface\s+(\w+)/g,
201
+ /export\s+default/g,
202
+ ];
203
+
204
+ const types: ExportInfo['type'][] = [
205
+ 'function',
206
+ 'class',
207
+ 'const',
208
+ 'type',
209
+ 'interface',
210
+ 'default',
211
+ ];
212
+
213
+ patterns.forEach((pattern, index) => {
214
+ let match;
215
+ while ((match = pattern.exec(content)) !== null) {
216
+ const name = match[1] || 'default';
217
+ const type = types[index];
218
+ const inferredDomain = inferDomain(
219
+ name,
220
+ filePath,
221
+ domainOptions,
222
+ fileImports
223
+ );
224
+ exports.push({ name, type, inferredDomain });
225
+ }
226
+ });
227
+
228
+ return exports;
229
+ }
230
+
231
+ /**
232
+ * Infer domain from name, path, or imports
233
+ */
234
+ export function inferDomain(
235
+ name: string,
236
+ filePath?: string,
237
+ domainOptions?: { domainKeywords?: string[] },
238
+ fileImports?: string[]
239
+ ): string {
240
+ const lower = name.toLowerCase();
241
+ const tokens = Array.from(
242
+ new Set(
243
+ lower
244
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
245
+ .replace(/[^a-z0-9]+/gi, ' ')
246
+ .split(' ')
247
+ .filter(Boolean)
248
+ )
249
+ );
250
+
251
+ const defaultKeywords = [
252
+ 'authentication',
253
+ 'authorization',
254
+ 'payment',
255
+ 'invoice',
256
+ 'customer',
257
+ 'product',
258
+ 'order',
259
+ 'cart',
260
+ 'user',
261
+ 'admin',
262
+ 'repository',
263
+ 'controller',
264
+ 'service',
265
+ 'config',
266
+ 'model',
267
+ 'view',
268
+ 'auth',
269
+ ];
270
+
271
+ const domainKeywords = domainOptions?.domainKeywords?.length
272
+ ? [...domainOptions.domainKeywords, ...defaultKeywords]
273
+ : defaultKeywords;
274
+
275
+ for (const keyword of domainKeywords) {
276
+ if (tokens.includes(keyword)) return keyword;
277
+ }
278
+
279
+ for (const keyword of domainKeywords) {
280
+ if (lower.includes(keyword)) return keyword;
281
+ }
282
+
283
+ if (fileImports) {
284
+ for (const importPath of fileImports) {
285
+ const segments = importPath.split('/');
286
+ for (const segment of segments) {
287
+ const segLower = segment.toLowerCase();
288
+ const singularSegment = singularize(segLower);
289
+ for (const keyword of domainKeywords) {
290
+ if (
291
+ singularSegment === keyword ||
292
+ segLower === keyword ||
293
+ segLower.includes(keyword)
294
+ )
295
+ return keyword;
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ if (filePath) {
302
+ const segments = filePath.split('/');
303
+ for (const segment of segments) {
304
+ const segLower = segment.toLowerCase();
305
+ const singularSegment = singularize(segLower);
306
+ for (const keyword of domainKeywords) {
307
+ if (singularSegment === keyword || segLower === keyword) return keyword;
308
+ }
309
+ }
310
+ }
311
+
312
+ return 'unknown';
313
+ }
314
+
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
+
227
329
  export function getCoUsageData(
228
330
  file: string,
229
331
  coUsageMatrix: Map<string, Map<string, number>>
230
332
  ): CoUsageData {
231
- const coImportedWith = coUsageMatrix.get(file) || new Map();
232
-
233
- // Find files that import both this file and others
234
- const sharedImporters: string[] = [];
235
- // This would require inverse mapping from imports, simplified for now
236
-
237
333
  return {
238
334
  file,
239
- coImportedWith,
240
- sharedImporters,
335
+ coImportedWith: coUsageMatrix.get(file) || new Map(),
336
+ sharedImporters: [],
241
337
  };
242
338
  }
243
339
 
244
- /**
245
- * Find files that should be consolidated based on semantic similarity
246
- *
247
- * High co-usage + shared types = strong consolidation candidate
248
- */
249
340
  export function findConsolidationCandidates(
250
341
  graph: DependencyGraph,
251
342
  coUsageMatrix: Map<string, Map<string, number>>,
252
343
  typeGraph: Map<string, Set<string>>,
253
344
  minCoUsage: number = 5,
254
345
  minSharedTypes: number = 2
255
- ): Array<{ files: string[]; reason: string; strength: number }> {
256
- const candidates: Array<{
257
- files: string[];
258
- reason: string;
259
- strength: number;
260
- }> = [];
261
-
262
- // Find file pairs with both high co-usage AND shared types
346
+ ) {
347
+ const candidates: any[] = [];
263
348
  for (const [fileA, coUsages] of coUsageMatrix) {
264
349
  const nodeA = graph.nodes.get(fileA);
265
350
  if (!nodeA) continue;
266
-
267
- for (const [fileB, coUsageCount] of coUsages) {
268
- if (fileB <= fileA) continue; // Avoid duplicates
269
- if (coUsageCount < minCoUsage) continue;
270
-
351
+ for (const [fileB, count] of coUsages) {
352
+ if (fileB <= fileA || count < minCoUsage) continue;
271
353
  const nodeB = graph.nodes.get(fileB);
272
354
  if (!nodeB) continue;
273
-
274
- // Count shared types
275
355
  const typesA = new Set(
276
356
  nodeA.exports.flatMap((e) => e.typeReferences || [])
277
357
  );
@@ -279,28 +359,14 @@ export function findConsolidationCandidates(
279
359
  nodeB.exports.flatMap((e) => e.typeReferences || [])
280
360
  );
281
361
  const sharedTypes = Array.from(typesA).filter((t) => typesB.has(t));
282
-
283
- if (sharedTypes.length >= minSharedTypes) {
284
- const strength = coUsageCount / 10 + sharedTypes.length / 5;
362
+ if (sharedTypes.length >= minSharedTypes || count >= minCoUsage * 2) {
285
363
  candidates.push({
286
364
  files: [fileA, fileB],
287
- reason: `High co-usage (${coUsageCount}x) and ${sharedTypes.length} shared types`,
288
- strength,
289
- });
290
- } else if (coUsageCount >= minCoUsage * 2) {
291
- // Very high co-usage alone is enough
292
- const strength = coUsageCount / 10;
293
- candidates.push({
294
- files: [fileA, fileB],
295
- reason: `Very high co-usage (${coUsageCount}x)`,
296
- strength,
365
+ reason: `High co-usage (${count}x)`,
366
+ strength: count / 10,
297
367
  });
298
368
  }
299
369
  }
300
370
  }
301
-
302
- // Sort by strength (highest first)
303
- candidates.sort((a, b) => b.strength - a.strength);
304
-
305
- return candidates;
371
+ return candidates.sort((a, b) => b.strength - a.strength);
306
372
  }
package/src/summary.ts ADDED
@@ -0,0 +1,168 @@
1
+ import type {
2
+ ContextAnalysisResult,
3
+ ContextSummary,
4
+ ModuleCluster,
5
+ } from './types';
6
+ import { calculatePathEntropy, calculateDirectoryDistance } from './analyzer';
7
+
8
+ /**
9
+ * Generate summary of context analysis results
10
+ */
11
+ export function generateSummary(
12
+ results: ContextAnalysisResult[]
13
+ ): ContextSummary {
14
+ if (results.length === 0) {
15
+ return {
16
+ totalFiles: 0,
17
+ totalTokens: 0,
18
+ avgContextBudget: 0,
19
+ maxContextBudget: 0,
20
+ avgImportDepth: 0,
21
+ maxImportDepth: 0,
22
+ deepFiles: [],
23
+ avgFragmentation: 0,
24
+ fragmentedModules: [],
25
+ avgCohesion: 0,
26
+ lowCohesionFiles: [],
27
+ criticalIssues: 0,
28
+ majorIssues: 0,
29
+ minorIssues: 0,
30
+ totalPotentialSavings: 0,
31
+ topExpensiveFiles: [],
32
+ };
33
+ }
34
+
35
+ const totalFiles = results.length;
36
+ const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
37
+ const totalContextBudget = results.reduce(
38
+ (sum, r) => sum + r.contextBudget,
39
+ 0
40
+ );
41
+ const avgContextBudget = totalContextBudget / totalFiles;
42
+ const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
43
+
44
+ const avgImportDepth =
45
+ results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
46
+ const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
47
+
48
+ const deepFiles = results
49
+ .filter((r) => r.importDepth >= 5)
50
+ .map((r) => ({ file: r.file, depth: r.importDepth }))
51
+ .sort((a, b) => b.depth - a.depth)
52
+ .slice(0, 10);
53
+
54
+ const avgFragmentation =
55
+ results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
56
+
57
+ const moduleMap = new Map<string, ContextAnalysisResult[]>();
58
+ for (const result of results) {
59
+ for (const domain of result.domains) {
60
+ if (!moduleMap.has(domain)) moduleMap.set(domain, []);
61
+ moduleMap.get(domain)!.push(result);
62
+ }
63
+ }
64
+
65
+ const fragmentedModules: ModuleCluster[] = [];
66
+ for (const [domain, files] of moduleMap.entries()) {
67
+ if (files.length < 2) continue;
68
+ const fragmentationScore =
69
+ files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
70
+ if (fragmentationScore < 0.3) continue;
71
+
72
+ const totalTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
73
+ const avgCohesion =
74
+ files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
75
+ const targetFiles = Math.max(1, Math.ceil(files.length / 3));
76
+
77
+ const filePaths = files.map((f) => f.file);
78
+ const pathEntropy = calculatePathEntropy(filePaths);
79
+ const directoryDistance = calculateDirectoryDistance(filePaths);
80
+
81
+ function jaccard(a: string[], b: string[]) {
82
+ const s1 = new Set(a || []);
83
+ const s2 = new Set(b || []);
84
+ if (s1.size === 0 && s2.size === 0) return 0;
85
+ const inter = new Set([...s1].filter((x) => s2.has(x)));
86
+ const uni = new Set([...s1, ...s2]);
87
+ return uni.size === 0 ? 0 : inter.size / uni.size;
88
+ }
89
+
90
+ let importSimTotal = 0;
91
+ let importPairs = 0;
92
+ for (let i = 0; i < files.length; i++) {
93
+ for (let j = i + 1; j < files.length; j++) {
94
+ importSimTotal += jaccard(
95
+ files[i].dependencyList || [],
96
+ files[j].dependencyList || []
97
+ );
98
+ importPairs++;
99
+ }
100
+ }
101
+
102
+ const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
103
+
104
+ fragmentedModules.push({
105
+ domain,
106
+ files: files.map((f) => f.file),
107
+ totalTokens,
108
+ fragmentationScore,
109
+ avgCohesion,
110
+ importCohesion,
111
+ pathEntropy,
112
+ directoryDistance,
113
+ suggestedStructure: {
114
+ targetFiles,
115
+ consolidationPlan: [
116
+ `Consolidate ${files.length} files across ${new Set(files.map((f) => f.file.split('/').slice(0, -1).join('/'))).size} directories`,
117
+ `Target ~${targetFiles} core modules to reduce context switching`,
118
+ ],
119
+ },
120
+ });
121
+ }
122
+
123
+ const avgCohesion =
124
+ results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
125
+ const lowCohesionFiles = results
126
+ .filter((r) => r.cohesionScore < 0.4)
127
+ .map((r) => ({ file: r.file, score: r.cohesionScore }))
128
+ .sort((a, b) => a.score - b.score)
129
+ .slice(0, 10);
130
+
131
+ const criticalIssues = results.filter(
132
+ (r) => r.severity === 'critical'
133
+ ).length;
134
+ const majorIssues = results.filter((r) => r.severity === 'major').length;
135
+ const minorIssues = results.filter((r) => r.severity === 'minor').length;
136
+ const totalPotentialSavings = results.reduce(
137
+ (sum, r) => sum + r.potentialSavings,
138
+ 0
139
+ );
140
+
141
+ const topExpensiveFiles = results
142
+ .sort((a, b) => b.contextBudget - a.contextBudget)
143
+ .slice(0, 10)
144
+ .map((r) => ({
145
+ file: r.file,
146
+ contextBudget: r.contextBudget,
147
+ severity: r.severity,
148
+ }));
149
+
150
+ return {
151
+ totalFiles,
152
+ totalTokens,
153
+ avgContextBudget,
154
+ maxContextBudget,
155
+ avgImportDepth,
156
+ maxImportDepth,
157
+ deepFiles,
158
+ avgFragmentation,
159
+ fragmentedModules,
160
+ avgCohesion,
161
+ lowCohesionFiles,
162
+ criticalIssues,
163
+ majorIssues,
164
+ minorIssues,
165
+ totalPotentialSavings,
166
+ topExpensiveFiles,
167
+ };
168
+ }