@aiready/context-analyzer 0.9.40 → 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.
- package/.turbo/turbo-build.log +11 -11
- package/.turbo/turbo-test.log +20 -23
- package/dist/chunk-4SYIJ7CU.mjs +1538 -0
- package/dist/chunk-4XQVYYPC.mjs +1470 -0
- package/dist/chunk-5CLU3HYU.mjs +1475 -0
- package/dist/chunk-5K73Q3OQ.mjs +1520 -0
- package/dist/chunk-6AVS4KTM.mjs +1536 -0
- package/dist/chunk-6I4552YB.mjs +1467 -0
- package/dist/chunk-6LPITDKG.mjs +1539 -0
- package/dist/chunk-AECWO7NQ.mjs +1539 -0
- package/dist/chunk-AJC3FR6G.mjs +1509 -0
- package/dist/chunk-CVGIDSMN.mjs +1522 -0
- package/dist/chunk-DXG5NIYL.mjs +1527 -0
- package/dist/chunk-G3CCJCBI.mjs +1521 -0
- package/dist/chunk-GFADGYXZ.mjs +1752 -0
- package/dist/chunk-GTRIBVS6.mjs +1467 -0
- package/dist/chunk-H4HWBQU6.mjs +1530 -0
- package/dist/chunk-JH535NPP.mjs +1619 -0
- package/dist/chunk-KGFWKSGJ.mjs +1442 -0
- package/dist/chunk-N2GQWNFG.mjs +1527 -0
- package/dist/chunk-NQA3F2HJ.mjs +1532 -0
- package/dist/chunk-NXXQ2U73.mjs +1467 -0
- package/dist/chunk-QDGPR3L6.mjs +1518 -0
- package/dist/chunk-SAVOSPM3.mjs +1522 -0
- package/dist/chunk-SIX4KMF2.mjs +1468 -0
- package/dist/chunk-SPAM2YJE.mjs +1537 -0
- package/dist/chunk-UG7OPVHB.mjs +1521 -0
- package/dist/chunk-VIJTZPBI.mjs +1470 -0
- package/dist/chunk-W37E7MW5.mjs +1403 -0
- package/dist/chunk-W76FEISE.mjs +1538 -0
- package/dist/chunk-WCFQYXQA.mjs +1532 -0
- package/dist/chunk-XY77XABG.mjs +1545 -0
- package/dist/chunk-YCGDIGOG.mjs +1467 -0
- package/dist/cli.js +768 -1160
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +196 -64
- package/dist/index.d.ts +196 -64
- package/dist/index.js +937 -1209
- package/dist/index.mjs +65 -3
- package/package.json +2 -2
- package/src/analyzer.ts +143 -2177
- package/src/ast-utils.ts +94 -0
- package/src/classifier.ts +497 -0
- package/src/cluster-detector.ts +100 -0
- package/src/defaults.ts +59 -0
- package/src/graph-builder.ts +272 -0
- package/src/index.ts +30 -519
- package/src/metrics.ts +231 -0
- package/src/remediation.ts +139 -0
- package/src/scoring.ts +12 -34
- package/src/semantic-analysis.ts +192 -126
- package/src/summary.ts +168 -0
package/src/semantic-analysis.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
)
|
|
256
|
-
const candidates:
|
|
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
|
-
|
|
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 (${
|
|
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
|
+
}
|