@aiready/context-analyzer 0.5.1 → 0.6.0
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 +7 -7
- package/.turbo/turbo-test.log +10 -27
- package/COHESION-IMPROVEMENTS.md +202 -0
- package/dist/chunk-DD7UVNE3.mjs +678 -0
- package/dist/chunk-EX7HCWAO.mjs +625 -0
- package/dist/cli.js +100 -32
- package/dist/cli.mjs +1 -1
- package/dist/index.js +100 -32
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +24 -0
- package/src/__tests__/enhanced-cohesion.test.ts +126 -0
- package/src/analyzer.ts +178 -40
- package/src/index.ts +1 -1
- package/src/types.ts +3 -0
package/dist/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ function buildDependencyGraph(files) {
|
|
|
36
36
|
const edges = /* @__PURE__ */ new Map();
|
|
37
37
|
for (const { file, content } of files) {
|
|
38
38
|
const imports = extractImportsFromContent(content);
|
|
39
|
-
const exports2 =
|
|
39
|
+
const exports2 = extractExportsWithAST(content, file);
|
|
40
40
|
const tokenCost = (0, import_core.estimateTokens)(content);
|
|
41
41
|
const linesOfCode = content.split("\n").length;
|
|
42
42
|
nodes.set(file, {
|
|
@@ -150,24 +150,12 @@ function detectCircularDependencies(graph) {
|
|
|
150
150
|
}
|
|
151
151
|
return cycles;
|
|
152
152
|
}
|
|
153
|
-
function calculateCohesion(exports2) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
160
|
-
}
|
|
161
|
-
const total = domains.length;
|
|
162
|
-
let entropy = 0;
|
|
163
|
-
for (const count of domainCounts.values()) {
|
|
164
|
-
const p = count / total;
|
|
165
|
-
if (p > 0) {
|
|
166
|
-
entropy -= p * Math.log2(p);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const maxEntropy = Math.log2(total);
|
|
170
|
-
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
153
|
+
function calculateCohesion(exports2, filePath) {
|
|
154
|
+
return calculateEnhancedCohesion(exports2, filePath);
|
|
155
|
+
}
|
|
156
|
+
function isTestFile(filePath) {
|
|
157
|
+
const lower = filePath.toLowerCase();
|
|
158
|
+
return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
|
|
171
159
|
}
|
|
172
160
|
function calculateFragmentation(files, domain) {
|
|
173
161
|
if (files.length <= 1) return 0;
|
|
@@ -194,7 +182,7 @@ function detectModuleClusters(graph) {
|
|
|
194
182
|
const fragmentationScore = calculateFragmentation(files, domain);
|
|
195
183
|
const avgCohesion = files.reduce((sum, file) => {
|
|
196
184
|
const node = graph.nodes.get(file);
|
|
197
|
-
return sum + (node ? calculateCohesion(node.exports) : 0);
|
|
185
|
+
return sum + (node ? calculateCohesion(node.exports, file) : 0);
|
|
198
186
|
}, 0) / files.length;
|
|
199
187
|
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
200
188
|
const consolidationPlan = generateConsolidationPlan(
|
|
@@ -248,25 +236,33 @@ function extractExports(content) {
|
|
|
248
236
|
function inferDomain(name) {
|
|
249
237
|
const lower = name.toLowerCase();
|
|
250
238
|
const domainKeywords = [
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"order",
|
|
254
|
-
"product",
|
|
239
|
+
"authentication",
|
|
240
|
+
"authorization",
|
|
255
241
|
"payment",
|
|
256
|
-
"cart",
|
|
257
242
|
"invoice",
|
|
258
243
|
"customer",
|
|
244
|
+
"product",
|
|
245
|
+
"order",
|
|
246
|
+
"cart",
|
|
247
|
+
"user",
|
|
259
248
|
"admin",
|
|
260
|
-
"api",
|
|
261
|
-
"util",
|
|
262
|
-
"helper",
|
|
263
|
-
"config",
|
|
264
|
-
"service",
|
|
265
249
|
"repository",
|
|
266
250
|
"controller",
|
|
251
|
+
"service",
|
|
252
|
+
"config",
|
|
267
253
|
"model",
|
|
268
|
-
"view"
|
|
254
|
+
"view",
|
|
255
|
+
"auth",
|
|
256
|
+
"api",
|
|
257
|
+
"helper",
|
|
258
|
+
"util"
|
|
269
259
|
];
|
|
260
|
+
for (const keyword of domainKeywords) {
|
|
261
|
+
const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
|
|
262
|
+
if (wordBoundaryPattern.test(name)) {
|
|
263
|
+
return keyword;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
270
266
|
for (const keyword of domainKeywords) {
|
|
271
267
|
if (lower.includes(keyword)) {
|
|
272
268
|
return keyword;
|
|
@@ -300,6 +296,78 @@ function generateConsolidationPlan(domain, files, targetFiles) {
|
|
|
300
296
|
);
|
|
301
297
|
return plan;
|
|
302
298
|
}
|
|
299
|
+
function extractExportsWithAST(content, filePath) {
|
|
300
|
+
try {
|
|
301
|
+
const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
|
|
302
|
+
return astExports.map((exp) => ({
|
|
303
|
+
name: exp.name,
|
|
304
|
+
type: exp.type,
|
|
305
|
+
inferredDomain: inferDomain(exp.name),
|
|
306
|
+
imports: exp.imports,
|
|
307
|
+
dependencies: exp.dependencies
|
|
308
|
+
}));
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return extractExports(content);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function calculateEnhancedCohesion(exports2, filePath) {
|
|
314
|
+
if (exports2.length === 0) return 1;
|
|
315
|
+
if (exports2.length === 1) return 1;
|
|
316
|
+
if (filePath && isTestFile(filePath)) {
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
const domainCohesion = calculateDomainCohesion(exports2);
|
|
320
|
+
const hasImportData = exports2.some((e) => e.imports && e.imports.length > 0);
|
|
321
|
+
if (!hasImportData) {
|
|
322
|
+
return domainCohesion;
|
|
323
|
+
}
|
|
324
|
+
const importCohesion = calculateImportBasedCohesion(exports2);
|
|
325
|
+
return importCohesion * 0.6 + domainCohesion * 0.4;
|
|
326
|
+
}
|
|
327
|
+
function calculateImportBasedCohesion(exports2) {
|
|
328
|
+
const exportsWithImports = exports2.filter((e) => e.imports && e.imports.length > 0);
|
|
329
|
+
if (exportsWithImports.length < 2) {
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
let totalSimilarity = 0;
|
|
333
|
+
let comparisons = 0;
|
|
334
|
+
for (let i = 0; i < exportsWithImports.length; i++) {
|
|
335
|
+
for (let j = i + 1; j < exportsWithImports.length; j++) {
|
|
336
|
+
const exp1 = exportsWithImports[i];
|
|
337
|
+
const exp2 = exportsWithImports[j];
|
|
338
|
+
const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
|
|
339
|
+
totalSimilarity += similarity;
|
|
340
|
+
comparisons++;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return comparisons > 0 ? totalSimilarity / comparisons : 1;
|
|
344
|
+
}
|
|
345
|
+
function calculateJaccardSimilarity(arr1, arr2) {
|
|
346
|
+
if (arr1.length === 0 && arr2.length === 0) return 1;
|
|
347
|
+
if (arr1.length === 0 || arr2.length === 0) return 0;
|
|
348
|
+
const set1 = new Set(arr1);
|
|
349
|
+
const set2 = new Set(arr2);
|
|
350
|
+
const intersection = new Set([...set1].filter((x) => set2.has(x)));
|
|
351
|
+
const union = /* @__PURE__ */ new Set([...set1, ...set2]);
|
|
352
|
+
return intersection.size / union.size;
|
|
353
|
+
}
|
|
354
|
+
function calculateDomainCohesion(exports2) {
|
|
355
|
+
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
356
|
+
const domainCounts = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const domain of domains) {
|
|
358
|
+
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
359
|
+
}
|
|
360
|
+
const total = domains.length;
|
|
361
|
+
let entropy = 0;
|
|
362
|
+
for (const count of domainCounts.values()) {
|
|
363
|
+
const p = count / total;
|
|
364
|
+
if (p > 0) {
|
|
365
|
+
entropy -= p * Math.log2(p);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const maxEntropy = Math.log2(total);
|
|
369
|
+
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
370
|
+
}
|
|
303
371
|
|
|
304
372
|
// src/index.ts
|
|
305
373
|
async function analyzeContext(options) {
|
|
@@ -341,7 +409,7 @@ async function analyzeContext(options) {
|
|
|
341
409
|
const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
|
|
342
410
|
const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
|
|
343
411
|
const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
344
|
-
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports) : 1;
|
|
412
|
+
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
|
|
345
413
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
346
414
|
const relatedFiles = [];
|
|
347
415
|
for (const cluster of clusters) {
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ function buildDependencyGraph(files) {
|
|
|
34
34
|
const edges = /* @__PURE__ */ new Map();
|
|
35
35
|
for (const { file, content } of files) {
|
|
36
36
|
const imports = extractImportsFromContent(content);
|
|
37
|
-
const exports2 =
|
|
37
|
+
const exports2 = extractExportsWithAST(content, file);
|
|
38
38
|
const tokenCost = (0, import_core.estimateTokens)(content);
|
|
39
39
|
const linesOfCode = content.split("\n").length;
|
|
40
40
|
nodes.set(file, {
|
|
@@ -148,24 +148,12 @@ function detectCircularDependencies(graph) {
|
|
|
148
148
|
}
|
|
149
149
|
return cycles;
|
|
150
150
|
}
|
|
151
|
-
function calculateCohesion(exports2) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
158
|
-
}
|
|
159
|
-
const total = domains.length;
|
|
160
|
-
let entropy = 0;
|
|
161
|
-
for (const count of domainCounts.values()) {
|
|
162
|
-
const p = count / total;
|
|
163
|
-
if (p > 0) {
|
|
164
|
-
entropy -= p * Math.log2(p);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const maxEntropy = Math.log2(total);
|
|
168
|
-
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
151
|
+
function calculateCohesion(exports2, filePath) {
|
|
152
|
+
return calculateEnhancedCohesion(exports2, filePath);
|
|
153
|
+
}
|
|
154
|
+
function isTestFile(filePath) {
|
|
155
|
+
const lower = filePath.toLowerCase();
|
|
156
|
+
return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
|
|
169
157
|
}
|
|
170
158
|
function calculateFragmentation(files, domain) {
|
|
171
159
|
if (files.length <= 1) return 0;
|
|
@@ -192,7 +180,7 @@ function detectModuleClusters(graph) {
|
|
|
192
180
|
const fragmentationScore = calculateFragmentation(files, domain);
|
|
193
181
|
const avgCohesion = files.reduce((sum, file) => {
|
|
194
182
|
const node = graph.nodes.get(file);
|
|
195
|
-
return sum + (node ? calculateCohesion(node.exports) : 0);
|
|
183
|
+
return sum + (node ? calculateCohesion(node.exports, file) : 0);
|
|
196
184
|
}, 0) / files.length;
|
|
197
185
|
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
198
186
|
const consolidationPlan = generateConsolidationPlan(
|
|
@@ -246,25 +234,33 @@ function extractExports(content) {
|
|
|
246
234
|
function inferDomain(name) {
|
|
247
235
|
const lower = name.toLowerCase();
|
|
248
236
|
const domainKeywords = [
|
|
249
|
-
"
|
|
250
|
-
"
|
|
251
|
-
"order",
|
|
252
|
-
"product",
|
|
237
|
+
"authentication",
|
|
238
|
+
"authorization",
|
|
253
239
|
"payment",
|
|
254
|
-
"cart",
|
|
255
240
|
"invoice",
|
|
256
241
|
"customer",
|
|
242
|
+
"product",
|
|
243
|
+
"order",
|
|
244
|
+
"cart",
|
|
245
|
+
"user",
|
|
257
246
|
"admin",
|
|
258
|
-
"api",
|
|
259
|
-
"util",
|
|
260
|
-
"helper",
|
|
261
|
-
"config",
|
|
262
|
-
"service",
|
|
263
247
|
"repository",
|
|
264
248
|
"controller",
|
|
249
|
+
"service",
|
|
250
|
+
"config",
|
|
265
251
|
"model",
|
|
266
|
-
"view"
|
|
252
|
+
"view",
|
|
253
|
+
"auth",
|
|
254
|
+
"api",
|
|
255
|
+
"helper",
|
|
256
|
+
"util"
|
|
267
257
|
];
|
|
258
|
+
for (const keyword of domainKeywords) {
|
|
259
|
+
const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
|
|
260
|
+
if (wordBoundaryPattern.test(name)) {
|
|
261
|
+
return keyword;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
268
264
|
for (const keyword of domainKeywords) {
|
|
269
265
|
if (lower.includes(keyword)) {
|
|
270
266
|
return keyword;
|
|
@@ -298,6 +294,78 @@ function generateConsolidationPlan(domain, files, targetFiles) {
|
|
|
298
294
|
);
|
|
299
295
|
return plan;
|
|
300
296
|
}
|
|
297
|
+
function extractExportsWithAST(content, filePath) {
|
|
298
|
+
try {
|
|
299
|
+
const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
|
|
300
|
+
return astExports.map((exp) => ({
|
|
301
|
+
name: exp.name,
|
|
302
|
+
type: exp.type,
|
|
303
|
+
inferredDomain: inferDomain(exp.name),
|
|
304
|
+
imports: exp.imports,
|
|
305
|
+
dependencies: exp.dependencies
|
|
306
|
+
}));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return extractExports(content);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function calculateEnhancedCohesion(exports2, filePath) {
|
|
312
|
+
if (exports2.length === 0) return 1;
|
|
313
|
+
if (exports2.length === 1) return 1;
|
|
314
|
+
if (filePath && isTestFile(filePath)) {
|
|
315
|
+
return 1;
|
|
316
|
+
}
|
|
317
|
+
const domainCohesion = calculateDomainCohesion(exports2);
|
|
318
|
+
const hasImportData = exports2.some((e) => e.imports && e.imports.length > 0);
|
|
319
|
+
if (!hasImportData) {
|
|
320
|
+
return domainCohesion;
|
|
321
|
+
}
|
|
322
|
+
const importCohesion = calculateImportBasedCohesion(exports2);
|
|
323
|
+
return importCohesion * 0.6 + domainCohesion * 0.4;
|
|
324
|
+
}
|
|
325
|
+
function calculateImportBasedCohesion(exports2) {
|
|
326
|
+
const exportsWithImports = exports2.filter((e) => e.imports && e.imports.length > 0);
|
|
327
|
+
if (exportsWithImports.length < 2) {
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
let totalSimilarity = 0;
|
|
331
|
+
let comparisons = 0;
|
|
332
|
+
for (let i = 0; i < exportsWithImports.length; i++) {
|
|
333
|
+
for (let j = i + 1; j < exportsWithImports.length; j++) {
|
|
334
|
+
const exp1 = exportsWithImports[i];
|
|
335
|
+
const exp2 = exportsWithImports[j];
|
|
336
|
+
const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
|
|
337
|
+
totalSimilarity += similarity;
|
|
338
|
+
comparisons++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return comparisons > 0 ? totalSimilarity / comparisons : 1;
|
|
342
|
+
}
|
|
343
|
+
function calculateJaccardSimilarity(arr1, arr2) {
|
|
344
|
+
if (arr1.length === 0 && arr2.length === 0) return 1;
|
|
345
|
+
if (arr1.length === 0 || arr2.length === 0) return 0;
|
|
346
|
+
const set1 = new Set(arr1);
|
|
347
|
+
const set2 = new Set(arr2);
|
|
348
|
+
const intersection = new Set([...set1].filter((x) => set2.has(x)));
|
|
349
|
+
const union = /* @__PURE__ */ new Set([...set1, ...set2]);
|
|
350
|
+
return intersection.size / union.size;
|
|
351
|
+
}
|
|
352
|
+
function calculateDomainCohesion(exports2) {
|
|
353
|
+
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
354
|
+
const domainCounts = /* @__PURE__ */ new Map();
|
|
355
|
+
for (const domain of domains) {
|
|
356
|
+
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
357
|
+
}
|
|
358
|
+
const total = domains.length;
|
|
359
|
+
let entropy = 0;
|
|
360
|
+
for (const count of domainCounts.values()) {
|
|
361
|
+
const p = count / total;
|
|
362
|
+
if (p > 0) {
|
|
363
|
+
entropy -= p * Math.log2(p);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const maxEntropy = Math.log2(total);
|
|
367
|
+
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
368
|
+
}
|
|
301
369
|
|
|
302
370
|
// src/index.ts
|
|
303
371
|
async function getSmartDefaults(directory, userOptions) {
|
|
@@ -383,7 +451,7 @@ async function analyzeContext(options) {
|
|
|
383
451
|
const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
|
|
384
452
|
const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
|
|
385
453
|
const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
386
|
-
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports) : 1;
|
|
454
|
+
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
|
|
387
455
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
388
456
|
const relatedFiles = [];
|
|
389
457
|
for (const cluster of clusters) {
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/context-analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "AI context window cost analysis - detect fragmented code, deep import chains, and expensive context budgets",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"commander": "^12.1.0",
|
|
51
51
|
"chalk": "^5.3.0",
|
|
52
52
|
"prompts": "^2.4.2",
|
|
53
|
-
"@aiready/core": "0.
|
|
53
|
+
"@aiready/core": "0.6.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^22.10.2",
|
|
@@ -148,6 +148,30 @@ describe('calculateCohesion', () => {
|
|
|
148
148
|
const cohesion = calculateCohesion(exports);
|
|
149
149
|
expect(cohesion).toBeLessThan(0.5);
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
it('should return 1 for test files even with mixed domains', () => {
|
|
153
|
+
const exports = [
|
|
154
|
+
{ name: 'mockUser', type: 'function' as const, inferredDomain: 'user' },
|
|
155
|
+
{ name: 'mockOrder', type: 'function' as const, inferredDomain: 'order' },
|
|
156
|
+
{ name: 'setupTestDb', type: 'function' as const, inferredDomain: 'helper' },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// Test file - should return 1 despite mixed domains
|
|
160
|
+
const cohesionTestFile = calculateCohesion(exports, 'src/__tests__/helpers.test.ts');
|
|
161
|
+
expect(cohesionTestFile).toBe(1);
|
|
162
|
+
|
|
163
|
+
// Mock file - should return 1 despite mixed domains
|
|
164
|
+
const cohesionMockFile = calculateCohesion(exports, 'src/test-utils/mocks.ts');
|
|
165
|
+
expect(cohesionMockFile).toBe(1);
|
|
166
|
+
|
|
167
|
+
// Fixture file - should return 1 despite mixed domains
|
|
168
|
+
const cohesionFixtureFile = calculateCohesion(exports, 'src/fixtures/data.ts');
|
|
169
|
+
expect(cohesionFixtureFile).toBe(1);
|
|
170
|
+
|
|
171
|
+
// Regular file - should have low cohesion
|
|
172
|
+
const cohesionRegularFile = calculateCohesion(exports, 'src/utils/helpers.ts');
|
|
173
|
+
expect(cohesionRegularFile).toBeLessThan(0.5);
|
|
174
|
+
});
|
|
151
175
|
});
|
|
152
176
|
|
|
153
177
|
describe('calculateFragmentation', () => {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { calculateCohesion } from '../analyzer';
|
|
3
|
+
import type { ExportInfo } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('Enhanced Cohesion Calculation', () => {
|
|
6
|
+
it('should use domain-based cohesion when no import data available', () => {
|
|
7
|
+
const exports: ExportInfo[] = [
|
|
8
|
+
{ name: 'getUserData', type: 'function', inferredDomain: 'user' },
|
|
9
|
+
{ name: 'getProductData', type: 'function', inferredDomain: 'product' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const cohesion = calculateCohesion(exports);
|
|
13
|
+
|
|
14
|
+
// With mixed domains (user, product) and no import data, should use domain-based calculation
|
|
15
|
+
// Domain entropy for 2 different domains = low cohesion
|
|
16
|
+
expect(cohesion).toBeLessThan(0.5);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should use import-based cohesion when import data available', () => {
|
|
20
|
+
const exports: ExportInfo[] = [
|
|
21
|
+
{
|
|
22
|
+
name: 'getUserData',
|
|
23
|
+
type: 'function',
|
|
24
|
+
inferredDomain: 'user',
|
|
25
|
+
imports: ['react', 'axios', 'lodash'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'getProductData',
|
|
29
|
+
type: 'function',
|
|
30
|
+
inferredDomain: 'product',
|
|
31
|
+
imports: ['react', 'axios', 'lodash'], // Same imports!
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const cohesion = calculateCohesion(exports);
|
|
36
|
+
|
|
37
|
+
// Even though domains differ, imports are identical (Jaccard = 1.0)
|
|
38
|
+
// Enhanced cohesion = 0.6 * 1.0 + 0.4 * 0.0 (different domains) = 0.6
|
|
39
|
+
// Should be >= 0.6 (import-based weight)
|
|
40
|
+
expect(cohesion).toBeGreaterThanOrEqual(0.6);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should weight import-based similarity higher than domain-based', () => {
|
|
44
|
+
const exportsWithSharedImports: ExportInfo[] = [
|
|
45
|
+
{
|
|
46
|
+
name: 'getUserData',
|
|
47
|
+
type: 'function',
|
|
48
|
+
inferredDomain: 'user',
|
|
49
|
+
imports: ['react', 'axios'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'getProductData',
|
|
53
|
+
type: 'function',
|
|
54
|
+
inferredDomain: 'product',
|
|
55
|
+
imports: ['react', 'axios'],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const exportsWithoutSharedImports: ExportInfo[] = [
|
|
60
|
+
{
|
|
61
|
+
name: 'getUserData',
|
|
62
|
+
type: 'function',
|
|
63
|
+
inferredDomain: 'user',
|
|
64
|
+
imports: ['react', 'axios'],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'getProductData',
|
|
68
|
+
type: 'function',
|
|
69
|
+
inferredDomain: 'product',
|
|
70
|
+
imports: ['lodash', 'moment'],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const cohesionWithShared = calculateCohesion(exportsWithSharedImports);
|
|
75
|
+
const cohesionWithoutShared = calculateCohesion(exportsWithoutSharedImports);
|
|
76
|
+
|
|
77
|
+
// Shared imports should result in higher cohesion
|
|
78
|
+
expect(cohesionWithShared).toBeGreaterThan(cohesionWithoutShared);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle mixed case: some exports with imports, some without', () => {
|
|
82
|
+
const exports: ExportInfo[] = [
|
|
83
|
+
{
|
|
84
|
+
name: 'getUserData',
|
|
85
|
+
type: 'function',
|
|
86
|
+
inferredDomain: 'user',
|
|
87
|
+
imports: ['react', 'axios'],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'getProductData',
|
|
91
|
+
type: 'function',
|
|
92
|
+
inferredDomain: 'product',
|
|
93
|
+
// No imports field
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const cohesion = calculateCohesion(exports);
|
|
98
|
+
|
|
99
|
+
// Should fall back to domain-based when not all exports have import data
|
|
100
|
+
expect(cohesion).toBeGreaterThan(0);
|
|
101
|
+
expect(cohesion).toBeLessThan(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return 1 for single export', () => {
|
|
105
|
+
const exports: ExportInfo[] = [
|
|
106
|
+
{
|
|
107
|
+
name: 'getUserData',
|
|
108
|
+
type: 'function',
|
|
109
|
+
inferredDomain: 'user',
|
|
110
|
+
imports: ['react'],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
expect(calculateCohesion(exports)).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return 1 for test files regardless of domains or imports', () => {
|
|
118
|
+
const exports: ExportInfo[] = [
|
|
119
|
+
{ name: 'testUserLogin', type: 'function', inferredDomain: 'user', imports: ['react'] },
|
|
120
|
+
{ name: 'testProductView', type: 'function', inferredDomain: 'product', imports: [] },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const cohesion = calculateCohesion(exports, 'src/utils/test-helpers.ts');
|
|
124
|
+
expect(cohesion).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
});
|