@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/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ __export(python_context_exports, {
|
|
|
37
37
|
});
|
|
38
38
|
async function analyzePythonContext(files, rootDir) {
|
|
39
39
|
const results = [];
|
|
40
|
-
const parser = (0,
|
|
40
|
+
const parser = (0, import_core6.getParser)("dummy.py");
|
|
41
41
|
if (!parser) {
|
|
42
42
|
console.warn("Python parser not available");
|
|
43
43
|
return results;
|
|
@@ -101,7 +101,7 @@ async function analyzePythonContext(files, rootDir) {
|
|
|
101
101
|
}
|
|
102
102
|
async function buildPythonDependencyGraph(files, rootDir) {
|
|
103
103
|
const graph = /* @__PURE__ */ new Map();
|
|
104
|
-
const parser = (0,
|
|
104
|
+
const parser = (0, import_core6.getParser)("dummy.py");
|
|
105
105
|
if (!parser) return graph;
|
|
106
106
|
for (const file of files) {
|
|
107
107
|
try {
|
|
@@ -181,7 +181,7 @@ async function calculatePythonImportDepth(file, dependencyGraph, visited, depth
|
|
|
181
181
|
}
|
|
182
182
|
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
183
183
|
void dependencyGraph;
|
|
184
|
-
let budget = (0,
|
|
184
|
+
let budget = (0, import_core6.estimateTokens)(code);
|
|
185
185
|
const avgTokensPerDep = 500;
|
|
186
186
|
budget += imports.length * avgTokensPerDep;
|
|
187
187
|
return budget;
|
|
@@ -231,11 +231,11 @@ function detectCircularDependencies2(file, dependencyGraph) {
|
|
|
231
231
|
dfs(file, []);
|
|
232
232
|
return [...new Set(circular)];
|
|
233
233
|
}
|
|
234
|
-
var
|
|
234
|
+
var import_core6, import_path, import_fs;
|
|
235
235
|
var init_python_context = __esm({
|
|
236
236
|
"src/analyzers/python-context.ts"() {
|
|
237
237
|
"use strict";
|
|
238
|
-
|
|
238
|
+
import_core6 = require("@aiready/core");
|
|
239
239
|
import_path = require("path");
|
|
240
240
|
import_fs = __toESM(require("fs"));
|
|
241
241
|
}
|
|
@@ -244,44 +244,73 @@ var init_python_context = __esm({
|
|
|
244
244
|
// src/index.ts
|
|
245
245
|
var index_exports = {};
|
|
246
246
|
__export(index_exports, {
|
|
247
|
+
adjustCohesionForClassification: () => adjustCohesionForClassification,
|
|
247
248
|
adjustFragmentationForClassification: () => adjustFragmentationForClassification,
|
|
248
249
|
analyzeContext: () => analyzeContext,
|
|
250
|
+
analyzeIssues: () => analyzeIssues,
|
|
249
251
|
buildCoUsageMatrix: () => buildCoUsageMatrix,
|
|
252
|
+
buildDependencyGraph: () => buildDependencyGraph,
|
|
250
253
|
buildTypeGraph: () => buildTypeGraph,
|
|
254
|
+
calculateCohesion: () => calculateCohesion,
|
|
255
|
+
calculateContextBudget: () => calculateContextBudget,
|
|
251
256
|
calculateContextScore: () => calculateContextScore,
|
|
257
|
+
calculateDirectoryDistance: () => calculateDirectoryDistance,
|
|
252
258
|
calculateDomainConfidence: () => calculateDomainConfidence,
|
|
259
|
+
calculateEnhancedCohesion: () => calculateEnhancedCohesion,
|
|
260
|
+
calculateFragmentation: () => calculateFragmentation,
|
|
261
|
+
calculateImportDepth: () => calculateImportDepth,
|
|
262
|
+
calculatePathEntropy: () => calculatePathEntropy,
|
|
263
|
+
calculateStructuralCohesionFromCoUsage: () => calculateStructuralCohesionFromCoUsage,
|
|
253
264
|
classifyFile: () => classifyFile,
|
|
265
|
+
detectCircularDependencies: () => detectCircularDependencies,
|
|
266
|
+
detectModuleClusters: () => detectModuleClusters,
|
|
267
|
+
extractDomainKeywordsFromPaths: () => extractDomainKeywordsFromPaths,
|
|
268
|
+
extractExports: () => extractExports,
|
|
269
|
+
extractImportsFromContent: () => extractImportsFromContent,
|
|
254
270
|
findConsolidationCandidates: () => findConsolidationCandidates,
|
|
255
271
|
findSemanticClusters: () => findSemanticClusters,
|
|
256
272
|
generateSummary: () => generateSummary,
|
|
273
|
+
getClassificationRecommendations: () => getClassificationRecommendations,
|
|
257
274
|
getCoUsageData: () => getCoUsageData,
|
|
275
|
+
getGeneralRecommendations: () => getGeneralRecommendations,
|
|
258
276
|
getSmartDefaults: () => getSmartDefaults,
|
|
259
|
-
|
|
277
|
+
getTransitiveDependencies: () => getTransitiveDependencies,
|
|
278
|
+
inferDomain: () => inferDomain,
|
|
279
|
+
inferDomainFromSemantics: () => inferDomainFromSemantics,
|
|
280
|
+
isBarrelExport: () => isBarrelExport,
|
|
281
|
+
isConfigFile: () => isConfigFile,
|
|
282
|
+
isEmailTemplate: () => isEmailTemplate,
|
|
283
|
+
isLambdaHandler: () => isLambdaHandler,
|
|
284
|
+
isNextJsPage: () => isNextJsPage,
|
|
285
|
+
isParserFile: () => isParserFile,
|
|
286
|
+
isServiceFile: () => isServiceFile,
|
|
287
|
+
isSessionFile: () => isSessionFile,
|
|
288
|
+
isTypeDefinition: () => isTypeDefinition,
|
|
289
|
+
isUtilityModule: () => isUtilityModule,
|
|
290
|
+
mapScoreToRating: () => mapScoreToRating
|
|
260
291
|
});
|
|
261
292
|
module.exports = __toCommonJS(index_exports);
|
|
262
|
-
var
|
|
293
|
+
var import_core7 = require("@aiready/core");
|
|
263
294
|
|
|
264
|
-
// src/
|
|
295
|
+
// src/metrics.ts
|
|
296
|
+
var import_core2 = require("@aiready/core");
|
|
297
|
+
|
|
298
|
+
// src/ast-utils.ts
|
|
265
299
|
var import_core = require("@aiready/core");
|
|
266
300
|
|
|
267
301
|
// src/semantic-analysis.ts
|
|
268
302
|
function buildCoUsageMatrix(graph) {
|
|
269
303
|
const coUsageMatrix = /* @__PURE__ */ new Map();
|
|
270
|
-
for (const [
|
|
271
|
-
void sourceFile;
|
|
304
|
+
for (const [, node] of graph.nodes) {
|
|
272
305
|
const imports = node.imports;
|
|
273
306
|
for (let i = 0; i < imports.length; i++) {
|
|
274
307
|
const fileA = imports[i];
|
|
275
|
-
if (!coUsageMatrix.has(fileA))
|
|
276
|
-
coUsageMatrix.set(fileA, /* @__PURE__ */ new Map());
|
|
277
|
-
}
|
|
308
|
+
if (!coUsageMatrix.has(fileA)) coUsageMatrix.set(fileA, /* @__PURE__ */ new Map());
|
|
278
309
|
for (let j = i + 1; j < imports.length; j++) {
|
|
279
310
|
const fileB = imports[j];
|
|
280
311
|
const fileAUsage = coUsageMatrix.get(fileA);
|
|
281
312
|
fileAUsage.set(fileB, (fileAUsage.get(fileB) || 0) + 1);
|
|
282
|
-
if (!coUsageMatrix.has(fileB))
|
|
283
|
-
coUsageMatrix.set(fileB, /* @__PURE__ */ new Map());
|
|
284
|
-
}
|
|
313
|
+
if (!coUsageMatrix.has(fileB)) coUsageMatrix.set(fileB, /* @__PURE__ */ new Map());
|
|
285
314
|
const fileBUsage = coUsageMatrix.get(fileB);
|
|
286
315
|
fileBUsage.set(fileA, (fileBUsage.get(fileA) || 0) + 1);
|
|
287
316
|
}
|
|
@@ -295,9 +324,7 @@ function buildTypeGraph(graph) {
|
|
|
295
324
|
for (const exp of node.exports) {
|
|
296
325
|
if (exp.typeReferences) {
|
|
297
326
|
for (const typeRef of exp.typeReferences) {
|
|
298
|
-
if (!typeGraph.has(typeRef))
|
|
299
|
-
typeGraph.set(typeRef, /* @__PURE__ */ new Set());
|
|
300
|
-
}
|
|
327
|
+
if (!typeGraph.has(typeRef)) typeGraph.set(typeRef, /* @__PURE__ */ new Set());
|
|
301
328
|
typeGraph.get(typeRef).add(file);
|
|
302
329
|
}
|
|
303
330
|
}
|
|
@@ -318,35 +345,11 @@ function findSemanticClusters(coUsageMatrix, minCoUsage = 3) {
|
|
|
318
345
|
visited.add(relatedFile);
|
|
319
346
|
}
|
|
320
347
|
}
|
|
321
|
-
if (cluster.length > 1)
|
|
322
|
-
clusters.set(file, cluster);
|
|
323
|
-
}
|
|
348
|
+
if (cluster.length > 1) clusters.set(file, cluster);
|
|
324
349
|
}
|
|
325
350
|
return clusters;
|
|
326
351
|
}
|
|
327
|
-
function calculateDomainConfidence(signals) {
|
|
328
|
-
const weights = {
|
|
329
|
-
coUsage: 0.35,
|
|
330
|
-
// Strongest signal: actual usage patterns
|
|
331
|
-
typeReference: 0.3,
|
|
332
|
-
// Strong signal: shared types
|
|
333
|
-
exportName: 0.15,
|
|
334
|
-
// Medium signal: identifier semantics
|
|
335
|
-
importPath: 0.1,
|
|
336
|
-
// Weaker signal: path structure
|
|
337
|
-
folderStructure: 0.1
|
|
338
|
-
// Weakest signal: organization convention
|
|
339
|
-
};
|
|
340
|
-
let confidence = 0;
|
|
341
|
-
if (signals.coUsage) confidence += weights.coUsage;
|
|
342
|
-
if (signals.typeReference) confidence += weights.typeReference;
|
|
343
|
-
if (signals.exportName) confidence += weights.exportName;
|
|
344
|
-
if (signals.importPath) confidence += weights.importPath;
|
|
345
|
-
if (signals.folderStructure) confidence += weights.folderStructure;
|
|
346
|
-
return confidence;
|
|
347
|
-
}
|
|
348
352
|
function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGraph, exportTypeRefs) {
|
|
349
|
-
const assignments = [];
|
|
350
353
|
const domainSignals = /* @__PURE__ */ new Map();
|
|
351
354
|
const coUsages = coUsageMatrix.get(file) || /* @__PURE__ */ new Map();
|
|
352
355
|
const strongCoUsages = Array.from(coUsages.entries()).filter(([, count]) => count >= 3).map(([coFile]) => coFile);
|
|
@@ -375,23 +378,22 @@ function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGr
|
|
|
375
378
|
const filesWithType = typeGraph.get(typeRef);
|
|
376
379
|
if (filesWithType) {
|
|
377
380
|
for (const typeFile of filesWithType) {
|
|
378
|
-
if (typeFile
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
domainSignals.get(domain).typeReference = true;
|
|
381
|
+
if (typeFile === file) continue;
|
|
382
|
+
const typeNode = graph.nodes.get(typeFile);
|
|
383
|
+
if (typeNode) {
|
|
384
|
+
for (const exp of typeNode.exports) {
|
|
385
|
+
if (exp.inferredDomain && exp.inferredDomain !== "unknown") {
|
|
386
|
+
const domain = exp.inferredDomain;
|
|
387
|
+
if (!domainSignals.has(domain)) {
|
|
388
|
+
domainSignals.set(domain, {
|
|
389
|
+
coUsage: false,
|
|
390
|
+
typeReference: false,
|
|
391
|
+
exportName: false,
|
|
392
|
+
importPath: false,
|
|
393
|
+
folderStructure: false
|
|
394
|
+
});
|
|
394
395
|
}
|
|
396
|
+
domainSignals.get(domain).typeReference = true;
|
|
395
397
|
}
|
|
396
398
|
}
|
|
397
399
|
}
|
|
@@ -399,22 +401,140 @@ function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGr
|
|
|
399
401
|
}
|
|
400
402
|
}
|
|
401
403
|
}
|
|
404
|
+
const assignments = [];
|
|
402
405
|
for (const [domain, signals] of domainSignals) {
|
|
403
406
|
const confidence = calculateDomainConfidence(signals);
|
|
404
|
-
if (confidence >= 0.3) {
|
|
405
|
-
assignments.push({ domain, confidence, signals });
|
|
406
|
-
}
|
|
407
|
+
if (confidence >= 0.3) assignments.push({ domain, confidence, signals });
|
|
407
408
|
}
|
|
408
409
|
assignments.sort((a, b) => b.confidence - a.confidence);
|
|
409
410
|
return assignments;
|
|
410
411
|
}
|
|
412
|
+
function calculateDomainConfidence(signals) {
|
|
413
|
+
const weights = {
|
|
414
|
+
coUsage: 0.35,
|
|
415
|
+
typeReference: 0.3,
|
|
416
|
+
exportName: 0.15,
|
|
417
|
+
importPath: 0.1,
|
|
418
|
+
folderStructure: 0.1
|
|
419
|
+
};
|
|
420
|
+
let confidence = 0;
|
|
421
|
+
if (signals.coUsage) confidence += weights.coUsage;
|
|
422
|
+
if (signals.typeReference) confidence += weights.typeReference;
|
|
423
|
+
if (signals.exportName) confidence += weights.exportName;
|
|
424
|
+
if (signals.importPath) confidence += weights.importPath;
|
|
425
|
+
if (signals.folderStructure) confidence += weights.folderStructure;
|
|
426
|
+
return confidence;
|
|
427
|
+
}
|
|
428
|
+
function extractExports(content, filePath, domainOptions, fileImports) {
|
|
429
|
+
const exports2 = [];
|
|
430
|
+
const patterns = [
|
|
431
|
+
/export\s+function\s+(\w+)/g,
|
|
432
|
+
/export\s+class\s+(\w+)/g,
|
|
433
|
+
/export\s+const\s+(\w+)/g,
|
|
434
|
+
/export\s+type\s+(\w+)/g,
|
|
435
|
+
/export\s+interface\s+(\w+)/g,
|
|
436
|
+
/export\s+default/g
|
|
437
|
+
];
|
|
438
|
+
const types = [
|
|
439
|
+
"function",
|
|
440
|
+
"class",
|
|
441
|
+
"const",
|
|
442
|
+
"type",
|
|
443
|
+
"interface",
|
|
444
|
+
"default"
|
|
445
|
+
];
|
|
446
|
+
patterns.forEach((pattern, index) => {
|
|
447
|
+
let match;
|
|
448
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
449
|
+
const name = match[1] || "default";
|
|
450
|
+
const type = types[index];
|
|
451
|
+
const inferredDomain = inferDomain(
|
|
452
|
+
name,
|
|
453
|
+
filePath,
|
|
454
|
+
domainOptions,
|
|
455
|
+
fileImports
|
|
456
|
+
);
|
|
457
|
+
exports2.push({ name, type, inferredDomain });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
return exports2;
|
|
461
|
+
}
|
|
462
|
+
function inferDomain(name, filePath, domainOptions, fileImports) {
|
|
463
|
+
const lower = name.toLowerCase();
|
|
464
|
+
const tokens = Array.from(
|
|
465
|
+
new Set(
|
|
466
|
+
lower.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-z0-9]+/gi, " ").split(" ").filter(Boolean)
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
const defaultKeywords = [
|
|
470
|
+
"authentication",
|
|
471
|
+
"authorization",
|
|
472
|
+
"payment",
|
|
473
|
+
"invoice",
|
|
474
|
+
"customer",
|
|
475
|
+
"product",
|
|
476
|
+
"order",
|
|
477
|
+
"cart",
|
|
478
|
+
"user",
|
|
479
|
+
"admin",
|
|
480
|
+
"repository",
|
|
481
|
+
"controller",
|
|
482
|
+
"service",
|
|
483
|
+
"config",
|
|
484
|
+
"model",
|
|
485
|
+
"view",
|
|
486
|
+
"auth"
|
|
487
|
+
];
|
|
488
|
+
const domainKeywords = domainOptions?.domainKeywords?.length ? [...domainOptions.domainKeywords, ...defaultKeywords] : defaultKeywords;
|
|
489
|
+
for (const keyword of domainKeywords) {
|
|
490
|
+
if (tokens.includes(keyword)) return keyword;
|
|
491
|
+
}
|
|
492
|
+
for (const keyword of domainKeywords) {
|
|
493
|
+
if (lower.includes(keyword)) return keyword;
|
|
494
|
+
}
|
|
495
|
+
if (fileImports) {
|
|
496
|
+
for (const importPath of fileImports) {
|
|
497
|
+
const segments = importPath.split("/");
|
|
498
|
+
for (const segment of segments) {
|
|
499
|
+
const segLower = segment.toLowerCase();
|
|
500
|
+
const singularSegment = singularize(segLower);
|
|
501
|
+
for (const keyword of domainKeywords) {
|
|
502
|
+
if (singularSegment === keyword || segLower === keyword || segLower.includes(keyword))
|
|
503
|
+
return keyword;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (filePath) {
|
|
509
|
+
const segments = filePath.split("/");
|
|
510
|
+
for (const segment of segments) {
|
|
511
|
+
const segLower = segment.toLowerCase();
|
|
512
|
+
const singularSegment = singularize(segLower);
|
|
513
|
+
for (const keyword of domainKeywords) {
|
|
514
|
+
if (singularSegment === keyword || segLower === keyword) return keyword;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return "unknown";
|
|
519
|
+
}
|
|
520
|
+
function singularize(word) {
|
|
521
|
+
const irregulars = {
|
|
522
|
+
people: "person",
|
|
523
|
+
children: "child",
|
|
524
|
+
men: "man",
|
|
525
|
+
women: "woman"
|
|
526
|
+
};
|
|
527
|
+
if (irregulars[word]) return irregulars[word];
|
|
528
|
+
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
529
|
+
if (word.endsWith("ses")) return word.slice(0, -2);
|
|
530
|
+
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
531
|
+
return word;
|
|
532
|
+
}
|
|
411
533
|
function getCoUsageData(file, coUsageMatrix) {
|
|
412
|
-
const coImportedWith = coUsageMatrix.get(file) || /* @__PURE__ */ new Map();
|
|
413
|
-
const sharedImporters = [];
|
|
414
534
|
return {
|
|
415
535
|
file,
|
|
416
|
-
coImportedWith,
|
|
417
|
-
sharedImporters
|
|
536
|
+
coImportedWith: coUsageMatrix.get(file) || /* @__PURE__ */ new Map(),
|
|
537
|
+
sharedImporters: []
|
|
418
538
|
};
|
|
419
539
|
}
|
|
420
540
|
function findConsolidationCandidates(graph, coUsageMatrix, typeGraph, minCoUsage = 5, minSharedTypes = 2) {
|
|
@@ -422,9 +542,8 @@ function findConsolidationCandidates(graph, coUsageMatrix, typeGraph, minCoUsage
|
|
|
422
542
|
for (const [fileA, coUsages] of coUsageMatrix) {
|
|
423
543
|
const nodeA = graph.nodes.get(fileA);
|
|
424
544
|
if (!nodeA) continue;
|
|
425
|
-
for (const [fileB,
|
|
426
|
-
if (fileB <= fileA) continue;
|
|
427
|
-
if (coUsageCount < minCoUsage) continue;
|
|
545
|
+
for (const [fileB, count] of coUsages) {
|
|
546
|
+
if (fileB <= fileA || count < minCoUsage) continue;
|
|
428
547
|
const nodeB = graph.nodes.get(fileB);
|
|
429
548
|
if (!nodeB) continue;
|
|
430
549
|
const typesA = new Set(
|
|
@@ -434,28 +553,190 @@ function findConsolidationCandidates(graph, coUsageMatrix, typeGraph, minCoUsage
|
|
|
434
553
|
nodeB.exports.flatMap((e) => e.typeReferences || [])
|
|
435
554
|
);
|
|
436
555
|
const sharedTypes = Array.from(typesA).filter((t) => typesB.has(t));
|
|
437
|
-
if (sharedTypes.length >= minSharedTypes) {
|
|
438
|
-
const strength = coUsageCount / 10 + sharedTypes.length / 5;
|
|
439
|
-
candidates.push({
|
|
440
|
-
files: [fileA, fileB],
|
|
441
|
-
reason: `High co-usage (${coUsageCount}x) and ${sharedTypes.length} shared types`,
|
|
442
|
-
strength
|
|
443
|
-
});
|
|
444
|
-
} else if (coUsageCount >= minCoUsage * 2) {
|
|
445
|
-
const strength = coUsageCount / 10;
|
|
556
|
+
if (sharedTypes.length >= minSharedTypes || count >= minCoUsage * 2) {
|
|
446
557
|
candidates.push({
|
|
447
558
|
files: [fileA, fileB],
|
|
448
|
-
reason: `
|
|
449
|
-
strength
|
|
559
|
+
reason: `High co-usage (${count}x)`,
|
|
560
|
+
strength: count / 10
|
|
450
561
|
});
|
|
451
562
|
}
|
|
452
563
|
}
|
|
453
564
|
}
|
|
454
|
-
candidates.sort((a, b) => b.strength - a.strength);
|
|
455
|
-
return candidates;
|
|
565
|
+
return candidates.sort((a, b) => b.strength - a.strength);
|
|
456
566
|
}
|
|
457
567
|
|
|
458
|
-
// src/
|
|
568
|
+
// src/ast-utils.ts
|
|
569
|
+
function extractExportsWithAST(content, filePath, domainOptions, fileImports) {
|
|
570
|
+
try {
|
|
571
|
+
const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
|
|
572
|
+
return astExports.map((exp) => ({
|
|
573
|
+
name: exp.name,
|
|
574
|
+
type: exp.type,
|
|
575
|
+
inferredDomain: inferDomain(
|
|
576
|
+
exp.name,
|
|
577
|
+
filePath,
|
|
578
|
+
domainOptions,
|
|
579
|
+
fileImports
|
|
580
|
+
),
|
|
581
|
+
imports: exp.imports,
|
|
582
|
+
dependencies: exp.dependencies,
|
|
583
|
+
typeReferences: exp.typeReferences
|
|
584
|
+
}));
|
|
585
|
+
} catch (error) {
|
|
586
|
+
void error;
|
|
587
|
+
return extractExports(content, filePath, domainOptions, fileImports);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function isTestFile(filePath) {
|
|
591
|
+
const lower = filePath.toLowerCase();
|
|
592
|
+
return lower.includes(".test.") || lower.includes(".spec.") || lower.includes("/__tests__/") || lower.includes("/tests/") || lower.includes("/test/") || lower.includes("test-") || lower.includes("-test") || lower.includes("/__mocks__/") || lower.includes("/mocks/") || lower.includes("/fixtures/") || lower.includes(".mock.") || lower.includes(".fixture.") || lower.includes("/test-utils/");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/metrics.ts
|
|
596
|
+
function calculateEnhancedCohesion(exports2, filePath, options) {
|
|
597
|
+
if (exports2.length <= 1) return 1;
|
|
598
|
+
if (filePath && isTestFile(filePath)) return 1;
|
|
599
|
+
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
600
|
+
const domainCounts = /* @__PURE__ */ new Map();
|
|
601
|
+
for (const d of domains) domainCounts.set(d, (domainCounts.get(d) || 0) + 1);
|
|
602
|
+
if (domainCounts.size === 1 && domains[0] !== "unknown") {
|
|
603
|
+
if (!options?.weights) return 1;
|
|
604
|
+
}
|
|
605
|
+
const probs = Array.from(domainCounts.values()).map(
|
|
606
|
+
(c) => c / exports2.length
|
|
607
|
+
);
|
|
608
|
+
let domainEntropy = 0;
|
|
609
|
+
for (const p of probs) {
|
|
610
|
+
if (p > 0) domainEntropy -= p * Math.log2(p);
|
|
611
|
+
}
|
|
612
|
+
const maxEntropy = Math.log2(Math.max(2, domainCounts.size));
|
|
613
|
+
const domainScore = 1 - domainEntropy / maxEntropy;
|
|
614
|
+
let importScoreTotal = 0;
|
|
615
|
+
let pairsWithData = 0;
|
|
616
|
+
let anyImportData = false;
|
|
617
|
+
for (let i = 0; i < exports2.length; i++) {
|
|
618
|
+
for (let j = i + 1; j < exports2.length; j++) {
|
|
619
|
+
const exp1Imports = exports2[i].imports;
|
|
620
|
+
const exp2Imports = exports2[j].imports;
|
|
621
|
+
if (exp1Imports || exp2Imports) {
|
|
622
|
+
anyImportData = true;
|
|
623
|
+
const sim = (0, import_core2.calculateImportSimilarity)(
|
|
624
|
+
{ ...exports2[i], imports: exp1Imports || [] },
|
|
625
|
+
{ ...exports2[j], imports: exp2Imports || [] }
|
|
626
|
+
);
|
|
627
|
+
importScoreTotal += sim;
|
|
628
|
+
pairsWithData++;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const avgImportScore = pairsWithData > 0 ? importScoreTotal / pairsWithData : 0;
|
|
633
|
+
let score = 0;
|
|
634
|
+
if (anyImportData) {
|
|
635
|
+
score = domainScore * 0.4 + avgImportScore * 0.6;
|
|
636
|
+
if (score === 0 && domainScore === 0) score = 0.1;
|
|
637
|
+
} else {
|
|
638
|
+
score = domainScore;
|
|
639
|
+
}
|
|
640
|
+
let structuralScore = 0;
|
|
641
|
+
for (const exp of exports2) {
|
|
642
|
+
if (exp.dependencies && exp.dependencies.length > 0) {
|
|
643
|
+
structuralScore += 1;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (structuralScore > 0) {
|
|
647
|
+
score = Math.min(1, score + 0.1);
|
|
648
|
+
}
|
|
649
|
+
if (!options?.weights && !anyImportData && domainCounts.size === 1) return 1;
|
|
650
|
+
return score;
|
|
651
|
+
}
|
|
652
|
+
function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
|
|
653
|
+
if (!coUsageMatrix) return 1;
|
|
654
|
+
const coUsages = coUsageMatrix.get(file);
|
|
655
|
+
if (!coUsages || coUsages.size === 0) return 1;
|
|
656
|
+
let total = 0;
|
|
657
|
+
for (const count of coUsages.values()) total += count;
|
|
658
|
+
if (total === 0) return 1;
|
|
659
|
+
const probs = [];
|
|
660
|
+
for (const count of coUsages.values()) {
|
|
661
|
+
if (count > 0) probs.push(count / total);
|
|
662
|
+
}
|
|
663
|
+
if (probs.length <= 1) return 1;
|
|
664
|
+
let entropy = 0;
|
|
665
|
+
for (const prob of probs) {
|
|
666
|
+
entropy -= prob * Math.log2(prob);
|
|
667
|
+
}
|
|
668
|
+
const maxEntropy = Math.log2(probs.length);
|
|
669
|
+
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
670
|
+
}
|
|
671
|
+
function calculateFragmentation(files, domain, options) {
|
|
672
|
+
if (files.length <= 1) return 0;
|
|
673
|
+
const directories = new Set(
|
|
674
|
+
files.map((f) => f.split("/").slice(0, -1).join("/"))
|
|
675
|
+
);
|
|
676
|
+
const uniqueDirs = directories.size;
|
|
677
|
+
let score = 0;
|
|
678
|
+
if (options?.useLogScale) {
|
|
679
|
+
if (uniqueDirs <= 1) score = 0;
|
|
680
|
+
else {
|
|
681
|
+
const total = files.length;
|
|
682
|
+
const base = options.logBase || Math.E;
|
|
683
|
+
const num = Math.log(uniqueDirs) / Math.log(base);
|
|
684
|
+
const den = Math.log(total) / Math.log(base);
|
|
685
|
+
score = den > 0 ? num / den : 0;
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
score = (uniqueDirs - 1) / (files.length - 1);
|
|
689
|
+
}
|
|
690
|
+
if (options?.sharedImportRatio && options.sharedImportRatio > 0.5) {
|
|
691
|
+
const discount = (options.sharedImportRatio - 0.5) * 0.4;
|
|
692
|
+
score = score * (1 - discount);
|
|
693
|
+
}
|
|
694
|
+
return score;
|
|
695
|
+
}
|
|
696
|
+
function calculatePathEntropy(files) {
|
|
697
|
+
if (!files || files.length === 0) return 0;
|
|
698
|
+
const dirCounts = /* @__PURE__ */ new Map();
|
|
699
|
+
for (const f of files) {
|
|
700
|
+
const dir = f.split("/").slice(0, -1).join("/") || ".";
|
|
701
|
+
dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
|
|
702
|
+
}
|
|
703
|
+
const counts = Array.from(dirCounts.values());
|
|
704
|
+
if (counts.length <= 1) return 0;
|
|
705
|
+
const total = counts.reduce((s, v) => s + v, 0);
|
|
706
|
+
let entropy = 0;
|
|
707
|
+
for (const count of counts) {
|
|
708
|
+
const prob = count / total;
|
|
709
|
+
entropy -= prob * Math.log2(prob);
|
|
710
|
+
}
|
|
711
|
+
const maxEntropy = Math.log2(counts.length);
|
|
712
|
+
return maxEntropy > 0 ? entropy / maxEntropy : 0;
|
|
713
|
+
}
|
|
714
|
+
function calculateDirectoryDistance(files) {
|
|
715
|
+
if (!files || files.length <= 1) return 0;
|
|
716
|
+
const pathSegments = (p) => p.split("/").filter(Boolean);
|
|
717
|
+
const commonAncestorDepth = (a, b) => {
|
|
718
|
+
const minLen = Math.min(a.length, b.length);
|
|
719
|
+
let i = 0;
|
|
720
|
+
while (i < minLen && a[i] === b[i]) i++;
|
|
721
|
+
return i;
|
|
722
|
+
};
|
|
723
|
+
let totalNormalized = 0;
|
|
724
|
+
let comparisons = 0;
|
|
725
|
+
for (let i = 0; i < files.length; i++) {
|
|
726
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
727
|
+
const segA = pathSegments(files[i]);
|
|
728
|
+
const segB = pathSegments(files[j]);
|
|
729
|
+
const shared = commonAncestorDepth(segA, segB);
|
|
730
|
+
const maxDepth = Math.max(segA.length, segB.length);
|
|
731
|
+
totalNormalized += 1 - (maxDepth > 0 ? shared / maxDepth : 0);
|
|
732
|
+
comparisons++;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return comparisons > 0 ? totalNormalized / comparisons : 0;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/graph-builder.ts
|
|
739
|
+
var import_core3 = require("@aiready/core");
|
|
459
740
|
function extractDomainKeywordsFromPaths(files) {
|
|
460
741
|
const folderNames = /* @__PURE__ */ new Set();
|
|
461
742
|
for (const { file } of files) {
|
|
@@ -483,39 +764,29 @@ function extractDomainKeywordsFromPaths(files) {
|
|
|
483
764
|
for (const segment of segments) {
|
|
484
765
|
const normalized = segment.toLowerCase();
|
|
485
766
|
if (normalized && !skipFolders.has(normalized) && !normalized.includes(".")) {
|
|
486
|
-
|
|
487
|
-
folderNames.add(singular);
|
|
767
|
+
folderNames.add(singularize2(normalized));
|
|
488
768
|
}
|
|
489
769
|
}
|
|
490
770
|
}
|
|
491
771
|
return Array.from(folderNames);
|
|
492
772
|
}
|
|
493
|
-
function
|
|
773
|
+
function singularize2(word) {
|
|
494
774
|
const irregulars = {
|
|
495
775
|
people: "person",
|
|
496
776
|
children: "child",
|
|
497
777
|
men: "man",
|
|
498
778
|
women: "woman"
|
|
499
779
|
};
|
|
500
|
-
if (irregulars[word])
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (word.endsWith("
|
|
504
|
-
return word.slice(0, -3) + "y";
|
|
505
|
-
}
|
|
506
|
-
if (word.endsWith("ses")) {
|
|
507
|
-
return word.slice(0, -2);
|
|
508
|
-
}
|
|
509
|
-
if (word.endsWith("s") && word.length > 3) {
|
|
510
|
-
return word.slice(0, -1);
|
|
511
|
-
}
|
|
780
|
+
if (irregulars[word]) return irregulars[word];
|
|
781
|
+
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
782
|
+
if (word.endsWith("ses")) return word.slice(0, -2);
|
|
783
|
+
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
512
784
|
return word;
|
|
513
785
|
}
|
|
514
786
|
function buildDependencyGraph(files, options) {
|
|
515
787
|
const nodes = /* @__PURE__ */ new Map();
|
|
516
788
|
const edges = /* @__PURE__ */ new Map();
|
|
517
789
|
const autoDetectedKeywords = options?.domainKeywords ?? extractDomainKeywordsFromPaths(files);
|
|
518
|
-
void import_core.calculateImportSimilarity;
|
|
519
790
|
for (const { file, content } of files) {
|
|
520
791
|
const imports = extractImportsFromContent(content);
|
|
521
792
|
const exports2 = extractExportsWithAST(
|
|
@@ -524,15 +795,9 @@ function buildDependencyGraph(files, options) {
|
|
|
524
795
|
{ domainKeywords: autoDetectedKeywords },
|
|
525
796
|
imports
|
|
526
797
|
);
|
|
527
|
-
const tokenCost = (0,
|
|
798
|
+
const tokenCost = (0, import_core3.estimateTokens)(content);
|
|
528
799
|
const linesOfCode = content.split("\n").length;
|
|
529
|
-
nodes.set(file, {
|
|
530
|
-
file,
|
|
531
|
-
imports,
|
|
532
|
-
exports: exports2,
|
|
533
|
-
tokenCost,
|
|
534
|
-
linesOfCode
|
|
535
|
-
});
|
|
800
|
+
nodes.set(file, { file, imports, exports: exports2, tokenCost, linesOfCode });
|
|
536
801
|
edges.set(file, new Set(imports));
|
|
537
802
|
}
|
|
538
803
|
const graph = { nodes, edges };
|
|
@@ -562,11 +827,8 @@ function extractImportsFromContent(content) {
|
|
|
562
827
|
const imports = [];
|
|
563
828
|
const patterns = [
|
|
564
829
|
/import\s+.*?\s+from\s+['"](.+?)['"]/g,
|
|
565
|
-
// import ... from '...'
|
|
566
830
|
/import\s+['"](.+?)['"]/g,
|
|
567
|
-
// import '...'
|
|
568
831
|
/require\(['"](.+?)['"]\)/g
|
|
569
|
-
// require('...')
|
|
570
832
|
];
|
|
571
833
|
for (const pattern of patterns) {
|
|
572
834
|
let match;
|
|
@@ -580,31 +842,25 @@ function extractImportsFromContent(content) {
|
|
|
580
842
|
return [...new Set(imports)];
|
|
581
843
|
}
|
|
582
844
|
function calculateImportDepth(file, graph, visited = /* @__PURE__ */ new Set(), depth = 0) {
|
|
583
|
-
if (visited.has(file))
|
|
584
|
-
return depth;
|
|
585
|
-
}
|
|
845
|
+
if (visited.has(file)) return depth;
|
|
586
846
|
const dependencies = graph.edges.get(file);
|
|
587
|
-
if (!dependencies || dependencies.size === 0)
|
|
588
|
-
return depth;
|
|
589
|
-
}
|
|
847
|
+
if (!dependencies || dependencies.size === 0) return depth;
|
|
590
848
|
visited.add(file);
|
|
591
849
|
let maxDepth = depth;
|
|
592
850
|
for (const dep of dependencies) {
|
|
593
|
-
|
|
594
|
-
|
|
851
|
+
maxDepth = Math.max(
|
|
852
|
+
maxDepth,
|
|
853
|
+
calculateImportDepth(dep, graph, visited, depth + 1)
|
|
854
|
+
);
|
|
595
855
|
}
|
|
596
856
|
visited.delete(file);
|
|
597
857
|
return maxDepth;
|
|
598
858
|
}
|
|
599
859
|
function getTransitiveDependencies(file, graph, visited = /* @__PURE__ */ new Set()) {
|
|
600
|
-
if (visited.has(file))
|
|
601
|
-
return [];
|
|
602
|
-
}
|
|
860
|
+
if (visited.has(file)) return [];
|
|
603
861
|
visited.add(file);
|
|
604
862
|
const dependencies = graph.edges.get(file);
|
|
605
|
-
if (!dependencies || dependencies.size === 0)
|
|
606
|
-
return [];
|
|
607
|
-
}
|
|
863
|
+
if (!dependencies || dependencies.size === 0) return [];
|
|
608
864
|
const allDeps = [];
|
|
609
865
|
for (const dep of dependencies) {
|
|
610
866
|
allDeps.push(dep);
|
|
@@ -637,9 +893,7 @@ function detectCircularDependencies(graph) {
|
|
|
637
893
|
}
|
|
638
894
|
return;
|
|
639
895
|
}
|
|
640
|
-
if (visited.has(file))
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
896
|
+
if (visited.has(file)) return;
|
|
643
897
|
visited.add(file);
|
|
644
898
|
recursionStack.add(file);
|
|
645
899
|
path.push(file);
|
|
@@ -658,404 +912,23 @@ function detectCircularDependencies(graph) {
|
|
|
658
912
|
}
|
|
659
913
|
return cycles;
|
|
660
914
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
function isTestFile(filePath) {
|
|
665
|
-
const lower = filePath.toLowerCase();
|
|
666
|
-
return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
|
|
667
|
-
}
|
|
668
|
-
function calculateFragmentation(files, domain, options) {
|
|
669
|
-
if (files.length <= 1) return 0;
|
|
670
|
-
const directories = new Set(
|
|
671
|
-
files.map((f) => f.split("/").slice(0, -1).join("/"))
|
|
672
|
-
);
|
|
673
|
-
const uniqueDirs = directories.size;
|
|
674
|
-
if (options?.useLogScale) {
|
|
675
|
-
if (uniqueDirs <= 1) return 0;
|
|
676
|
-
const total = files.length;
|
|
677
|
-
const base = options.logBase || Math.E;
|
|
678
|
-
const num = Math.log(uniqueDirs) / Math.log(base);
|
|
679
|
-
const den = Math.log(total) / Math.log(base);
|
|
680
|
-
return den > 0 ? num / den : 0;
|
|
681
|
-
}
|
|
682
|
-
return (uniqueDirs - 1) / (files.length - 1);
|
|
683
|
-
}
|
|
684
|
-
function calculatePathEntropy(files) {
|
|
685
|
-
if (!files || files.length === 0) return 0;
|
|
686
|
-
const dirCounts = /* @__PURE__ */ new Map();
|
|
687
|
-
for (const f of files) {
|
|
688
|
-
const dir = f.split("/").slice(0, -1).join("/") || ".";
|
|
689
|
-
dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
|
|
690
|
-
}
|
|
691
|
-
const counts = Array.from(dirCounts.values());
|
|
692
|
-
if (counts.length <= 1) return 0;
|
|
693
|
-
const total = counts.reduce((s, v) => s + v, 0);
|
|
694
|
-
let entropy = 0;
|
|
695
|
-
for (const count of counts) {
|
|
696
|
-
const prob = count / total;
|
|
697
|
-
entropy -= prob * Math.log2(prob);
|
|
698
|
-
}
|
|
699
|
-
const maxEntropy = Math.log2(counts.length);
|
|
700
|
-
return maxEntropy > 0 ? entropy / maxEntropy : 0;
|
|
701
|
-
}
|
|
702
|
-
function calculateDirectoryDistance(files) {
|
|
703
|
-
if (!files || files.length <= 1) return 0;
|
|
704
|
-
function pathSegments(p) {
|
|
705
|
-
return p.split("/").filter(Boolean);
|
|
706
|
-
}
|
|
707
|
-
function commonAncestorDepth(a, b) {
|
|
708
|
-
const minLen = Math.min(a.length, b.length);
|
|
709
|
-
let i = 0;
|
|
710
|
-
while (i < minLen && a[i] === b[i]) i++;
|
|
711
|
-
return i;
|
|
712
|
-
}
|
|
713
|
-
let totalNormalized = 0;
|
|
714
|
-
let comparisons = 0;
|
|
715
|
-
for (let i = 0; i < files.length; i++) {
|
|
716
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
717
|
-
const segA = pathSegments(files[i]);
|
|
718
|
-
const segB = pathSegments(files[j]);
|
|
719
|
-
const shared = commonAncestorDepth(segA, segB);
|
|
720
|
-
const maxDepth = Math.max(segA.length, segB.length);
|
|
721
|
-
const normalizedShared = maxDepth > 0 ? shared / maxDepth : 0;
|
|
722
|
-
totalNormalized += 1 - normalizedShared;
|
|
723
|
-
comparisons++;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return comparisons > 0 ? totalNormalized / comparisons : 0;
|
|
727
|
-
}
|
|
728
|
-
function detectModuleClusters(graph, options) {
|
|
729
|
-
const domainMap = /* @__PURE__ */ new Map();
|
|
730
|
-
for (const [file, node] of graph.nodes.entries()) {
|
|
731
|
-
const domains = node.exports.map((e) => e.inferredDomain || "unknown");
|
|
732
|
-
const primaryDomain = domains[0] || "unknown";
|
|
733
|
-
if (!domainMap.has(primaryDomain)) {
|
|
734
|
-
domainMap.set(primaryDomain, []);
|
|
735
|
-
}
|
|
736
|
-
domainMap.get(primaryDomain).push(file);
|
|
737
|
-
}
|
|
738
|
-
const clusters = [];
|
|
739
|
-
for (const [domain, files] of domainMap.entries()) {
|
|
740
|
-
if (files.length < 2) continue;
|
|
741
|
-
const totalTokens = files.reduce((sum, file) => {
|
|
742
|
-
const node = graph.nodes.get(file);
|
|
743
|
-
return sum + (node?.tokenCost || 0);
|
|
744
|
-
}, 0);
|
|
745
|
-
const baseFragmentation = calculateFragmentation(files, domain, {
|
|
746
|
-
useLogScale: !!options?.useLogScale
|
|
747
|
-
});
|
|
748
|
-
let importSimilarityTotal = 0;
|
|
749
|
-
let importComparisons = 0;
|
|
750
|
-
for (let i = 0; i < files.length; i++) {
|
|
751
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
752
|
-
const f1 = files[i];
|
|
753
|
-
const f2 = files[j];
|
|
754
|
-
const n1 = graph.nodes.get(f1)?.imports || [];
|
|
755
|
-
const n2 = graph.nodes.get(f2)?.imports || [];
|
|
756
|
-
const similarity = n1.length === 0 && n2.length === 0 ? 0 : calculateJaccardSimilarity(n1, n2);
|
|
757
|
-
importSimilarityTotal += similarity;
|
|
758
|
-
importComparisons++;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
const importCohesion = importComparisons > 0 ? importSimilarityTotal / importComparisons : 0;
|
|
762
|
-
const couplingDiscountFactor = 1 - 0.2 * importCohesion;
|
|
763
|
-
const fragmentationScore = baseFragmentation * couplingDiscountFactor;
|
|
764
|
-
const pathEntropy = calculatePathEntropy(files);
|
|
765
|
-
const directoryDistance = calculateDirectoryDistance(files);
|
|
766
|
-
const avgCohesion = files.reduce((sum, file) => {
|
|
767
|
-
const node = graph.nodes.get(file);
|
|
768
|
-
return sum + (node ? calculateCohesion(node.exports, file, {
|
|
769
|
-
coUsageMatrix: graph.coUsageMatrix
|
|
770
|
-
}) : 0);
|
|
771
|
-
}, 0) / files.length;
|
|
772
|
-
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
773
|
-
const consolidationPlan = generateConsolidationPlan(
|
|
774
|
-
domain,
|
|
775
|
-
files,
|
|
776
|
-
targetFiles
|
|
777
|
-
);
|
|
778
|
-
clusters.push({
|
|
779
|
-
domain,
|
|
780
|
-
files,
|
|
781
|
-
totalTokens,
|
|
782
|
-
fragmentationScore,
|
|
783
|
-
pathEntropy,
|
|
784
|
-
directoryDistance,
|
|
785
|
-
importCohesion,
|
|
786
|
-
avgCohesion,
|
|
787
|
-
suggestedStructure: {
|
|
788
|
-
targetFiles,
|
|
789
|
-
consolidationPlan
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
return clusters.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
794
|
-
}
|
|
795
|
-
function extractExports(content, filePath, domainOptions, fileImports) {
|
|
796
|
-
const exports2 = [];
|
|
797
|
-
const patterns = [
|
|
798
|
-
/export\s+function\s+(\w+)/g,
|
|
799
|
-
/export\s+class\s+(\w+)/g,
|
|
800
|
-
/export\s+const\s+(\w+)/g,
|
|
801
|
-
/export\s+type\s+(\w+)/g,
|
|
802
|
-
/export\s+interface\s+(\w+)/g,
|
|
803
|
-
/export\s+default/g
|
|
804
|
-
];
|
|
805
|
-
const types = [
|
|
806
|
-
"function",
|
|
807
|
-
"class",
|
|
808
|
-
"const",
|
|
809
|
-
"type",
|
|
810
|
-
"interface",
|
|
811
|
-
"default"
|
|
812
|
-
];
|
|
813
|
-
patterns.forEach((pattern, index) => {
|
|
814
|
-
let match;
|
|
815
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
816
|
-
const name = match[1] || "default";
|
|
817
|
-
const type = types[index];
|
|
818
|
-
const inferredDomain = inferDomain(
|
|
819
|
-
name,
|
|
820
|
-
filePath,
|
|
821
|
-
domainOptions,
|
|
822
|
-
fileImports
|
|
823
|
-
);
|
|
824
|
-
exports2.push({ name, type, inferredDomain });
|
|
825
|
-
}
|
|
826
|
-
});
|
|
827
|
-
return exports2;
|
|
828
|
-
}
|
|
829
|
-
function inferDomain(name, filePath, domainOptions, fileImports) {
|
|
830
|
-
const lower = name.toLowerCase();
|
|
831
|
-
const tokens = Array.from(
|
|
832
|
-
new Set(
|
|
833
|
-
lower.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-z0-9]+/gi, " ").split(" ").filter(Boolean)
|
|
834
|
-
)
|
|
835
|
-
);
|
|
836
|
-
const defaultKeywords = [
|
|
837
|
-
"authentication",
|
|
838
|
-
"authorization",
|
|
839
|
-
"payment",
|
|
840
|
-
"invoice",
|
|
841
|
-
"customer",
|
|
842
|
-
"product",
|
|
843
|
-
"order",
|
|
844
|
-
"cart",
|
|
845
|
-
"user",
|
|
846
|
-
"admin",
|
|
847
|
-
"repository",
|
|
848
|
-
"controller",
|
|
849
|
-
"service",
|
|
850
|
-
"config",
|
|
851
|
-
"model",
|
|
852
|
-
"view",
|
|
853
|
-
"auth"
|
|
854
|
-
];
|
|
855
|
-
const domainKeywords = domainOptions?.domainKeywords && domainOptions.domainKeywords.length ? [...domainOptions.domainKeywords, ...defaultKeywords] : defaultKeywords;
|
|
856
|
-
for (const keyword of domainKeywords) {
|
|
857
|
-
if (tokens.includes(keyword)) {
|
|
858
|
-
return keyword;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
for (const keyword of domainKeywords) {
|
|
862
|
-
if (lower.includes(keyword)) {
|
|
863
|
-
return keyword;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (fileImports && fileImports.length > 0) {
|
|
867
|
-
for (const importPath of fileImports) {
|
|
868
|
-
const allSegments = importPath.split("/");
|
|
869
|
-
const relevantSegments = allSegments.filter((s) => {
|
|
870
|
-
if (!s) return false;
|
|
871
|
-
if (s === "." || s === "..") return false;
|
|
872
|
-
if (s.startsWith("@") && s.length === 1) return false;
|
|
873
|
-
return true;
|
|
874
|
-
}).map((s) => s.startsWith("@") ? s.slice(1) : s);
|
|
875
|
-
for (const segment of relevantSegments) {
|
|
876
|
-
const segLower = segment.toLowerCase();
|
|
877
|
-
const singularSegment = singularize(segLower);
|
|
878
|
-
for (const keyword of domainKeywords) {
|
|
879
|
-
if (singularSegment === keyword || segLower === keyword || segLower.includes(keyword)) {
|
|
880
|
-
return keyword;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
if (filePath) {
|
|
887
|
-
const pathSegments = filePath.toLowerCase().split("/");
|
|
888
|
-
for (const segment of pathSegments) {
|
|
889
|
-
const singularSegment = singularize(segment);
|
|
890
|
-
for (const keyword of domainKeywords) {
|
|
891
|
-
if (singularSegment === keyword || segment === keyword || segment.includes(keyword)) {
|
|
892
|
-
return keyword;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
return "unknown";
|
|
898
|
-
}
|
|
899
|
-
function generateConsolidationPlan(domain, files, targetFiles) {
|
|
900
|
-
const plan = [];
|
|
901
|
-
if (files.length <= targetFiles) {
|
|
902
|
-
return [`No consolidation needed for ${domain}`];
|
|
903
|
-
}
|
|
904
|
-
plan.push(
|
|
905
|
-
`Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s):`
|
|
906
|
-
);
|
|
907
|
-
const dirGroups = /* @__PURE__ */ new Map();
|
|
908
|
-
for (const file of files) {
|
|
909
|
-
const dir = file.split("/").slice(0, -1).join("/");
|
|
910
|
-
if (!dirGroups.has(dir)) {
|
|
911
|
-
dirGroups.set(dir, []);
|
|
912
|
-
}
|
|
913
|
-
dirGroups.get(dir).push(file);
|
|
914
|
-
}
|
|
915
|
-
plan.push(`1. Create unified ${domain} module file`);
|
|
916
|
-
plan.push(
|
|
917
|
-
`2. Move related functionality from ${files.length} scattered files`
|
|
918
|
-
);
|
|
919
|
-
plan.push(`3. Update imports in dependent files`);
|
|
920
|
-
plan.push(
|
|
921
|
-
`4. Remove old files after consolidation (verify with tests first)`
|
|
922
|
-
);
|
|
923
|
-
return plan;
|
|
924
|
-
}
|
|
925
|
-
function extractExportsWithAST(content, filePath, domainOptions, fileImports) {
|
|
926
|
-
try {
|
|
927
|
-
const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
|
|
928
|
-
return astExports.map((exp) => ({
|
|
929
|
-
name: exp.name,
|
|
930
|
-
type: exp.type,
|
|
931
|
-
inferredDomain: inferDomain(
|
|
932
|
-
exp.name,
|
|
933
|
-
filePath,
|
|
934
|
-
domainOptions,
|
|
935
|
-
fileImports
|
|
936
|
-
),
|
|
937
|
-
imports: exp.imports,
|
|
938
|
-
dependencies: exp.dependencies
|
|
939
|
-
}));
|
|
940
|
-
} catch (error) {
|
|
941
|
-
void error;
|
|
942
|
-
return extractExports(content, filePath, domainOptions, fileImports);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
function calculateEnhancedCohesion(exports2, filePath, options) {
|
|
946
|
-
if (exports2.length === 0) return 1;
|
|
947
|
-
if (exports2.length === 1) return 1;
|
|
948
|
-
if (filePath && isTestFile(filePath)) {
|
|
949
|
-
return 1;
|
|
950
|
-
}
|
|
951
|
-
const domainCohesion = calculateDomainCohesion(exports2);
|
|
952
|
-
const hasImportData = exports2.some((e) => e.imports && e.imports.length > 0);
|
|
953
|
-
const importCohesion = hasImportData ? calculateImportBasedCohesion(exports2) : void 0;
|
|
954
|
-
const coUsageMatrix = options?.coUsageMatrix;
|
|
955
|
-
const structuralCohesion = filePath && coUsageMatrix ? calculateStructuralCohesionFromCoUsage(filePath, coUsageMatrix) : void 0;
|
|
956
|
-
const defaultWeights = {
|
|
957
|
-
importBased: 0.5,
|
|
958
|
-
structural: 0.3,
|
|
959
|
-
domainBased: 0.2
|
|
960
|
-
};
|
|
961
|
-
const weights = { ...defaultWeights, ...options?.weights || {} };
|
|
962
|
-
const signals = [];
|
|
963
|
-
if (importCohesion !== void 0)
|
|
964
|
-
signals.push({ score: importCohesion, weight: weights.importBased });
|
|
965
|
-
if (structuralCohesion !== void 0)
|
|
966
|
-
signals.push({ score: structuralCohesion, weight: weights.structural });
|
|
967
|
-
signals.push({ score: domainCohesion, weight: weights.domainBased });
|
|
968
|
-
const totalWeight = signals.reduce((s, el) => s + el.weight, 0);
|
|
969
|
-
if (totalWeight === 0) return domainCohesion;
|
|
970
|
-
const combined = signals.reduce(
|
|
971
|
-
(sum, el) => sum + el.score * (el.weight / totalWeight),
|
|
972
|
-
0
|
|
973
|
-
);
|
|
974
|
-
return combined;
|
|
975
|
-
}
|
|
976
|
-
function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
|
|
977
|
-
if (!coUsageMatrix) return 1;
|
|
978
|
-
const coUsages = coUsageMatrix.get(file);
|
|
979
|
-
if (!coUsages || coUsages.size === 0) return 1;
|
|
980
|
-
let total = 0;
|
|
981
|
-
for (const count of coUsages.values()) total += count;
|
|
982
|
-
if (total === 0) return 1;
|
|
983
|
-
const probs = [];
|
|
984
|
-
for (const count of coUsages.values()) {
|
|
985
|
-
if (count > 0) probs.push(count / total);
|
|
986
|
-
}
|
|
987
|
-
if (probs.length <= 1) return 1;
|
|
988
|
-
let entropy = 0;
|
|
989
|
-
for (const prob of probs) {
|
|
990
|
-
entropy -= prob * Math.log2(prob);
|
|
991
|
-
}
|
|
992
|
-
const maxEntropy = Math.log2(probs.length);
|
|
993
|
-
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
994
|
-
}
|
|
995
|
-
function calculateImportBasedCohesion(exports2) {
|
|
996
|
-
const exportsWithImports = exports2.filter(
|
|
997
|
-
(e) => e.imports && e.imports.length > 0
|
|
998
|
-
);
|
|
999
|
-
if (exportsWithImports.length < 2) {
|
|
1000
|
-
return 1;
|
|
1001
|
-
}
|
|
1002
|
-
let totalSimilarity = 0;
|
|
1003
|
-
let comparisons = 0;
|
|
1004
|
-
for (let i = 0; i < exportsWithImports.length; i++) {
|
|
1005
|
-
for (let j = i + 1; j < exportsWithImports.length; j++) {
|
|
1006
|
-
const exp1 = exportsWithImports[i];
|
|
1007
|
-
const exp2 = exportsWithImports[j];
|
|
1008
|
-
const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
|
|
1009
|
-
totalSimilarity += similarity;
|
|
1010
|
-
comparisons++;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
return comparisons > 0 ? totalSimilarity / comparisons : 1;
|
|
1014
|
-
}
|
|
1015
|
-
function calculateJaccardSimilarity(arr1, arr2) {
|
|
1016
|
-
if (arr1.length === 0 && arr2.length === 0) return 1;
|
|
1017
|
-
if (arr1.length === 0 || arr2.length === 0) return 0;
|
|
1018
|
-
const set1 = new Set(arr1);
|
|
1019
|
-
const set2 = new Set(arr2);
|
|
1020
|
-
const intersection = new Set([...set1].filter((x) => set2.has(x)));
|
|
1021
|
-
const union = /* @__PURE__ */ new Set([...set1, ...set2]);
|
|
1022
|
-
return intersection.size / union.size;
|
|
1023
|
-
}
|
|
1024
|
-
function calculateDomainCohesion(exports2) {
|
|
1025
|
-
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
1026
|
-
const domainCounts = /* @__PURE__ */ new Map();
|
|
1027
|
-
for (const domain of domains) {
|
|
1028
|
-
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
1029
|
-
}
|
|
1030
|
-
const total = domains.length;
|
|
1031
|
-
let entropy = 0;
|
|
1032
|
-
for (const domainCount of domainCounts.values()) {
|
|
1033
|
-
const prob = domainCount / total;
|
|
1034
|
-
if (prob > 0) {
|
|
1035
|
-
entropy -= prob * Math.log2(prob);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
const maxEntropy = Math.log2(total);
|
|
1039
|
-
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
1040
|
-
}
|
|
1041
|
-
function classifyFile(node, cohesionScore, domains) {
|
|
1042
|
-
const { exports: exports2, imports, linesOfCode, file } = node;
|
|
1043
|
-
void imports;
|
|
1044
|
-
void linesOfCode;
|
|
915
|
+
|
|
916
|
+
// src/classifier.ts
|
|
917
|
+
function classifyFile(node, cohesionScore = 1, domains = []) {
|
|
1045
918
|
if (isBarrelExport(node)) {
|
|
1046
919
|
return "barrel-export";
|
|
1047
920
|
}
|
|
1048
|
-
if (
|
|
921
|
+
if (isTypeDefinition(node)) {
|
|
1049
922
|
return "type-definition";
|
|
1050
923
|
}
|
|
1051
|
-
if (
|
|
1052
|
-
return "
|
|
924
|
+
if (isNextJsPage(node)) {
|
|
925
|
+
return "nextjs-page";
|
|
1053
926
|
}
|
|
1054
927
|
if (isLambdaHandler(node)) {
|
|
1055
928
|
return "lambda-handler";
|
|
1056
929
|
}
|
|
1057
|
-
if (
|
|
1058
|
-
return "
|
|
930
|
+
if (isServiceFile(node)) {
|
|
931
|
+
return "service-file";
|
|
1059
932
|
}
|
|
1060
933
|
if (isEmailTemplate(node)) {
|
|
1061
934
|
return "email-template";
|
|
@@ -1063,224 +936,53 @@ function classifyFile(node, cohesionScore, domains) {
|
|
|
1063
936
|
if (isParserFile(node)) {
|
|
1064
937
|
return "parser-file";
|
|
1065
938
|
}
|
|
1066
|
-
if (isServiceFile(node)) {
|
|
1067
|
-
return "service-file";
|
|
1068
|
-
}
|
|
1069
939
|
if (isSessionFile(node)) {
|
|
1070
|
-
return "cohesive-module";
|
|
1071
|
-
}
|
|
1072
|
-
if (isNextJsPage(node)) {
|
|
1073
|
-
return "nextjs-page";
|
|
1074
|
-
}
|
|
1075
|
-
if (isUtilityFile(node)) {
|
|
940
|
+
if (cohesionScore >= 0.25 && domains.length <= 1) return "cohesive-module";
|
|
1076
941
|
return "utility-module";
|
|
1077
942
|
}
|
|
1078
|
-
if (
|
|
943
|
+
if (isUtilityModule(node)) {
|
|
1079
944
|
return "utility-module";
|
|
1080
945
|
}
|
|
1081
|
-
|
|
1082
|
-
const hasSingleDomain = uniqueDomains.length <= 1;
|
|
1083
|
-
if (hasSingleDomain) {
|
|
946
|
+
if (isConfigFile(node)) {
|
|
1084
947
|
return "cohesive-module";
|
|
1085
948
|
}
|
|
1086
|
-
if (
|
|
949
|
+
if (domains.length <= 1 && domains[0] !== "unknown") {
|
|
1087
950
|
return "cohesive-module";
|
|
1088
951
|
}
|
|
1089
|
-
|
|
1090
|
-
const hasLowCohesion = cohesionScore < 0.4;
|
|
1091
|
-
if (hasMultipleDomains && hasLowCohesion) {
|
|
952
|
+
if (domains.length > 1 && cohesionScore < 0.4) {
|
|
1092
953
|
return "mixed-concerns";
|
|
1093
954
|
}
|
|
1094
|
-
if (cohesionScore >= 0.
|
|
955
|
+
if (cohesionScore >= 0.7) {
|
|
1095
956
|
return "cohesive-module";
|
|
1096
957
|
}
|
|
1097
958
|
return "unknown";
|
|
1098
959
|
}
|
|
1099
960
|
function isBarrelExport(node) {
|
|
1100
|
-
const { file, exports: exports2, imports, linesOfCode } = node;
|
|
1101
|
-
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1102
|
-
const isIndexFile = fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx";
|
|
1103
|
-
const hasReExports = exports2.length > 0 && imports.length > 0;
|
|
1104
|
-
const highExportToLinesRatio = exports2.length > 3 && linesOfCode < exports2.length * 5;
|
|
1105
|
-
const sparseCode = linesOfCode > 0 && linesOfCode < 50 && exports2.length >= 2;
|
|
1106
|
-
if (isIndexFile && hasReExports) {
|
|
1107
|
-
return true;
|
|
1108
|
-
}
|
|
1109
|
-
if (highExportToLinesRatio && imports.length >= exports2.length * 0.5) {
|
|
1110
|
-
return true;
|
|
1111
|
-
}
|
|
1112
|
-
if (sparseCode && imports.length > 0) {
|
|
1113
|
-
return true;
|
|
1114
|
-
}
|
|
1115
|
-
return false;
|
|
1116
|
-
}
|
|
1117
|
-
function isTypeDefinitionFile(node) {
|
|
1118
961
|
const { file, exports: exports2 } = node;
|
|
1119
962
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1120
|
-
const
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
(e) => e.type === "type" || e.type === "interface"
|
|
963
|
+
const isIndexFile = fileName === "index.ts" || fileName === "index.js";
|
|
964
|
+
const isSmallAndManyExports = node.tokenCost < 1e3 && (exports2 || []).length > 5;
|
|
965
|
+
const isReexportPattern = (exports2 || []).length >= 5 && (exports2 || []).every(
|
|
966
|
+
(e) => e.type === "const" || e.type === "function" || e.type === "type" || e.type === "interface"
|
|
1125
967
|
);
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const
|
|
968
|
+
return !!isIndexFile || !!isSmallAndManyExports || !!isReexportPattern;
|
|
969
|
+
}
|
|
970
|
+
function isTypeDefinition(node) {
|
|
971
|
+
const { file } = node;
|
|
972
|
+
if (file.endsWith(".d.ts")) return true;
|
|
973
|
+
const nodeExports = node.exports || [];
|
|
974
|
+
const hasExports = nodeExports.length > 0;
|
|
975
|
+
const areAllTypes = hasExports && nodeExports.every((e) => e.type === "type" || e.type === "interface");
|
|
976
|
+
const allTypes = !!areAllTypes;
|
|
977
|
+
const isTypePath = file.toLowerCase().includes("/types/") || file.toLowerCase().includes("/interfaces/") || file.toLowerCase().includes("/models/");
|
|
978
|
+
return allTypes || isTypePath && hasExports;
|
|
979
|
+
}
|
|
980
|
+
function isUtilityModule(node) {
|
|
981
|
+
const { file } = node;
|
|
982
|
+
const isUtilPath = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/") || file.toLowerCase().includes("/util/") || file.toLowerCase().includes("/helper/");
|
|
1136
983
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
"schema",
|
|
1140
|
-
"settings",
|
|
1141
|
-
"options",
|
|
1142
|
-
"constants",
|
|
1143
|
-
"env",
|
|
1144
|
-
"environment",
|
|
1145
|
-
".config.",
|
|
1146
|
-
"-config.",
|
|
1147
|
-
"_config."
|
|
1148
|
-
];
|
|
1149
|
-
const isConfigName = configPatterns.some(
|
|
1150
|
-
(pattern) => fileName?.includes(pattern) || fileName?.startsWith(pattern) || fileName?.endsWith(`${pattern}.ts`)
|
|
1151
|
-
);
|
|
1152
|
-
const isConfigPath = file.toLowerCase().includes("/config/") || file.toLowerCase().includes("/schemas/") || file.toLowerCase().includes("/settings/");
|
|
1153
|
-
const hasSchemaExports = exports2.some(
|
|
1154
|
-
(e) => e.name.toLowerCase().includes("table") || e.name.toLowerCase().includes("schema") || e.name.toLowerCase().includes("config") || e.name.toLowerCase().includes("setting")
|
|
1155
|
-
);
|
|
1156
|
-
return isConfigName || isConfigPath || hasSchemaExports;
|
|
1157
|
-
}
|
|
1158
|
-
function isUtilityFile(node) {
|
|
1159
|
-
const { file, exports: exports2 } = node;
|
|
1160
|
-
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1161
|
-
const utilityPatterns = [
|
|
1162
|
-
"util",
|
|
1163
|
-
"utility",
|
|
1164
|
-
"utilities",
|
|
1165
|
-
"helper",
|
|
1166
|
-
"helpers",
|
|
1167
|
-
"common",
|
|
1168
|
-
"shared",
|
|
1169
|
-
"toolbox",
|
|
1170
|
-
"toolkit",
|
|
1171
|
-
".util.",
|
|
1172
|
-
"-util.",
|
|
1173
|
-
"_util.",
|
|
1174
|
-
"-utils.",
|
|
1175
|
-
".utils."
|
|
1176
|
-
];
|
|
1177
|
-
const isUtilityName = utilityPatterns.some(
|
|
1178
|
-
(pattern) => fileName?.includes(pattern)
|
|
1179
|
-
);
|
|
1180
|
-
const isUtilityPath = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/") || file.toLowerCase().includes("/common/") || file.toLowerCase().endsWith("-utils.ts") || file.toLowerCase().endsWith("-util.ts") || file.toLowerCase().endsWith("-helper.ts") || file.toLowerCase().endsWith("-helpers.ts");
|
|
1181
|
-
const hasManySmallExportsInUtilityContext = exports2.length >= 3 && exports2.every((e) => e.type === "function" || e.type === "const") && (isUtilityName || isUtilityPath);
|
|
1182
|
-
return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
|
|
1183
|
-
}
|
|
1184
|
-
function splitCamelCase(name) {
|
|
1185
|
-
return name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1186
|
-
}
|
|
1187
|
-
var SKIP_WORDS = /* @__PURE__ */ new Set([
|
|
1188
|
-
"get",
|
|
1189
|
-
"set",
|
|
1190
|
-
"create",
|
|
1191
|
-
"update",
|
|
1192
|
-
"delete",
|
|
1193
|
-
"fetch",
|
|
1194
|
-
"save",
|
|
1195
|
-
"load",
|
|
1196
|
-
"parse",
|
|
1197
|
-
"format",
|
|
1198
|
-
"validate",
|
|
1199
|
-
"convert",
|
|
1200
|
-
"transform",
|
|
1201
|
-
"build",
|
|
1202
|
-
"generate",
|
|
1203
|
-
"render",
|
|
1204
|
-
"send",
|
|
1205
|
-
"receive",
|
|
1206
|
-
"find",
|
|
1207
|
-
"list",
|
|
1208
|
-
"add",
|
|
1209
|
-
"remove",
|
|
1210
|
-
"insert",
|
|
1211
|
-
"upsert",
|
|
1212
|
-
"put",
|
|
1213
|
-
"read",
|
|
1214
|
-
"write",
|
|
1215
|
-
"check",
|
|
1216
|
-
"handle",
|
|
1217
|
-
"process",
|
|
1218
|
-
"compute",
|
|
1219
|
-
"calculate",
|
|
1220
|
-
"init",
|
|
1221
|
-
"reset",
|
|
1222
|
-
"clear",
|
|
1223
|
-
"pending",
|
|
1224
|
-
"active",
|
|
1225
|
-
"current",
|
|
1226
|
-
"new",
|
|
1227
|
-
"old",
|
|
1228
|
-
"all",
|
|
1229
|
-
"by",
|
|
1230
|
-
"with",
|
|
1231
|
-
"from",
|
|
1232
|
-
"to",
|
|
1233
|
-
"and",
|
|
1234
|
-
"or",
|
|
1235
|
-
"is",
|
|
1236
|
-
"has",
|
|
1237
|
-
"in",
|
|
1238
|
-
"on",
|
|
1239
|
-
"of",
|
|
1240
|
-
"the"
|
|
1241
|
-
]);
|
|
1242
|
-
function simpleSingularize(word) {
|
|
1243
|
-
if (word.endsWith("ies") && word.length > 3) return word.slice(0, -3) + "y";
|
|
1244
|
-
if (word.endsWith("ses") && word.length > 4) return word.slice(0, -2);
|
|
1245
|
-
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
1246
|
-
return word;
|
|
1247
|
-
}
|
|
1248
|
-
function extractEntityNouns(name) {
|
|
1249
|
-
return splitCamelCase(name).filter((token) => !SKIP_WORDS.has(token) && token.length > 2).map(simpleSingularize);
|
|
1250
|
-
}
|
|
1251
|
-
function allExportsShareEntityNoun(exports2) {
|
|
1252
|
-
if (exports2.length < 2 || exports2.length > 30) return false;
|
|
1253
|
-
const nounSets = exports2.map((e) => new Set(extractEntityNouns(e.name)));
|
|
1254
|
-
if (nounSets.some((s) => s.size === 0)) return false;
|
|
1255
|
-
const [first, ...rest] = nounSets;
|
|
1256
|
-
const commonNouns = Array.from(first).filter(
|
|
1257
|
-
(noun) => rest.every((s) => s.has(noun))
|
|
1258
|
-
);
|
|
1259
|
-
return commonNouns.length > 0;
|
|
1260
|
-
}
|
|
1261
|
-
function isDataAccessFile(node) {
|
|
1262
|
-
const { file, exports: exports2 } = node;
|
|
1263
|
-
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1264
|
-
const dalPatterns = [
|
|
1265
|
-
"dynamo",
|
|
1266
|
-
"database",
|
|
1267
|
-
"repository",
|
|
1268
|
-
"repo",
|
|
1269
|
-
"dao",
|
|
1270
|
-
"firestore",
|
|
1271
|
-
"postgres",
|
|
1272
|
-
"mysql",
|
|
1273
|
-
"mongo",
|
|
1274
|
-
"redis",
|
|
1275
|
-
"sqlite",
|
|
1276
|
-
"supabase",
|
|
1277
|
-
"prisma"
|
|
1278
|
-
];
|
|
1279
|
-
const isDalName = dalPatterns.some((p) => fileName?.includes(p));
|
|
1280
|
-
const isDalPath = file.toLowerCase().includes("/repositories/") || file.toLowerCase().includes("/dao/") || file.toLowerCase().includes("/data/");
|
|
1281
|
-
const hasDalExportPattern = exports2.length >= 1 && exports2.length <= 10 && allExportsShareEntityNoun(exports2);
|
|
1282
|
-
const isUtilityPathLocal = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/");
|
|
1283
|
-
return isDalPath || isDalName && hasDalExportPattern && !isUtilityPathLocal;
|
|
984
|
+
const isUtilName = fileName?.includes("utils.") || fileName?.includes("helpers.") || fileName?.includes("util.") || fileName?.includes("helper.");
|
|
985
|
+
return !!isUtilPath || !!isUtilName;
|
|
1284
986
|
}
|
|
1285
987
|
function isLambdaHandler(node) {
|
|
1286
988
|
const { file, exports: exports2 } = node;
|
|
@@ -1297,11 +999,10 @@ function isLambdaHandler(node) {
|
|
|
1297
999
|
(pattern) => fileName?.includes(pattern)
|
|
1298
1000
|
);
|
|
1299
1001
|
const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/lambda/") || file.toLowerCase().includes("/functions/");
|
|
1300
|
-
const hasHandlerExport = exports2.some(
|
|
1002
|
+
const hasHandlerExport = (exports2 || []).some(
|
|
1301
1003
|
(e) => e.name.toLowerCase() === "handler" || e.name.toLowerCase() === "main" || e.name.toLowerCase() === "lambdahandler" || e.name.toLowerCase().endsWith("handler")
|
|
1302
1004
|
);
|
|
1303
|
-
|
|
1304
|
-
return isHandlerName || isHandlerPath || hasHandlerExport || hasSingleEntryInHandlerContext;
|
|
1005
|
+
return !!isHandlerName || !!isHandlerPath || !!hasHandlerExport;
|
|
1305
1006
|
}
|
|
1306
1007
|
function isServiceFile(node) {
|
|
1307
1008
|
const { file, exports: exports2 } = node;
|
|
@@ -1311,11 +1012,11 @@ function isServiceFile(node) {
|
|
|
1311
1012
|
(pattern) => fileName?.includes(pattern)
|
|
1312
1013
|
);
|
|
1313
1014
|
const isServicePath = file.toLowerCase().includes("/services/");
|
|
1314
|
-
const hasServiceNamedExport = exports2.some(
|
|
1015
|
+
const hasServiceNamedExport = (exports2 || []).some(
|
|
1315
1016
|
(e) => e.name.toLowerCase().includes("service") || e.name.toLowerCase().endsWith("service")
|
|
1316
1017
|
);
|
|
1317
|
-
const hasClassExport = exports2.some((e) => e.type === "class");
|
|
1318
|
-
return isServiceName || isServicePath || hasServiceNamedExport && hasClassExport;
|
|
1018
|
+
const hasClassExport = (exports2 || []).some((e) => e.type === "class");
|
|
1019
|
+
return !!isServiceName || !!isServicePath || !!hasServiceNamedExport && !!hasClassExport;
|
|
1319
1020
|
}
|
|
1320
1021
|
function isEmailTemplate(node) {
|
|
1321
1022
|
const { file, exports: exports2 } = node;
|
|
@@ -1333,15 +1034,11 @@ function isEmailTemplate(node) {
|
|
|
1333
1034
|
const isEmailTemplateName = emailTemplatePatterns.some(
|
|
1334
1035
|
(pattern) => fileName?.includes(pattern)
|
|
1335
1036
|
);
|
|
1336
|
-
const isSpecificTemplateName = fileName?.includes("receipt") || fileName?.includes("invoice-email") || fileName?.includes("welcome-email") || fileName?.includes("notification-email") || fileName?.includes("writer") && fileName.includes("receipt");
|
|
1337
1037
|
const isEmailPath = file.toLowerCase().includes("/emails/") || file.toLowerCase().includes("/mail/") || file.toLowerCase().includes("/notifications/");
|
|
1338
|
-
const hasTemplateFunction = exports2.some(
|
|
1038
|
+
const hasTemplateFunction = (exports2 || []).some(
|
|
1339
1039
|
(e) => e.type === "function" && (e.name.toLowerCase().startsWith("render") || e.name.toLowerCase().startsWith("generate") || e.name.toLowerCase().includes("template") && e.name.toLowerCase().includes("email"))
|
|
1340
1040
|
);
|
|
1341
|
-
|
|
1342
|
-
(e) => e.name.toLowerCase().includes("template") && e.type === "function" || e.name.toLowerCase().includes("render") && e.type === "function" || e.name.toLowerCase().includes("email") && e.type !== "class"
|
|
1343
|
-
);
|
|
1344
|
-
return isEmailPath || isEmailTemplateName || isSpecificTemplateName || hasTemplateFunction && hasEmailExport;
|
|
1041
|
+
return !!isEmailPath || !!isEmailTemplateName || !!hasTemplateFunction;
|
|
1345
1042
|
}
|
|
1346
1043
|
function isParserFile(node) {
|
|
1347
1044
|
const { file, exports: exports2 } = node;
|
|
@@ -1353,55 +1050,51 @@ function isParserFile(node) {
|
|
|
1353
1050
|
"_parser.",
|
|
1354
1051
|
"transform",
|
|
1355
1052
|
".transform.",
|
|
1356
|
-
"-transform.",
|
|
1357
1053
|
"converter",
|
|
1358
|
-
".converter.",
|
|
1359
|
-
"-converter.",
|
|
1360
1054
|
"mapper",
|
|
1361
|
-
"
|
|
1362
|
-
"-mapper.",
|
|
1363
|
-
"serializer",
|
|
1364
|
-
".serializer.",
|
|
1365
|
-
"deterministic"
|
|
1366
|
-
// For base-parser-deterministic.ts pattern
|
|
1055
|
+
"serializer"
|
|
1367
1056
|
];
|
|
1368
1057
|
const isParserName = parserPatterns.some(
|
|
1369
1058
|
(pattern) => fileName?.includes(pattern)
|
|
1370
1059
|
);
|
|
1371
|
-
const isParserPath = file.toLowerCase().includes("/parsers/") || file.toLowerCase().includes("/transformers/")
|
|
1372
|
-
const
|
|
1373
|
-
(e) => e.
|
|
1060
|
+
const isParserPath = file.toLowerCase().includes("/parsers/") || file.toLowerCase().includes("/transformers/");
|
|
1061
|
+
const hasParseFunction = (exports2 || []).some(
|
|
1062
|
+
(e) => e.type === "function" && (e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("extract"))
|
|
1374
1063
|
);
|
|
1375
|
-
|
|
1376
|
-
(e) => e.type === "function" && (e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert") || e.name.toLowerCase().startsWith("map") || e.name.toLowerCase().startsWith("extract"))
|
|
1377
|
-
);
|
|
1378
|
-
return isParserName || isParserPath || hasParserExport || hasParseFunction;
|
|
1064
|
+
return !!isParserName || !!isParserPath || !!hasParseFunction;
|
|
1379
1065
|
}
|
|
1380
1066
|
function isSessionFile(node) {
|
|
1381
1067
|
const { file, exports: exports2 } = node;
|
|
1382
1068
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1383
|
-
const sessionPatterns = [
|
|
1384
|
-
"session",
|
|
1385
|
-
".session.",
|
|
1386
|
-
"-session.",
|
|
1387
|
-
"state",
|
|
1388
|
-
".state.",
|
|
1389
|
-
"-state.",
|
|
1390
|
-
"context",
|
|
1391
|
-
".context.",
|
|
1392
|
-
"-context.",
|
|
1393
|
-
"store",
|
|
1394
|
-
".store.",
|
|
1395
|
-
"-store."
|
|
1396
|
-
];
|
|
1069
|
+
const sessionPatterns = ["session", "state", "context", "store"];
|
|
1397
1070
|
const isSessionName = sessionPatterns.some(
|
|
1398
1071
|
(pattern) => fileName?.includes(pattern)
|
|
1399
1072
|
);
|
|
1400
|
-
const isSessionPath = file.toLowerCase().includes("/sessions/") || file.toLowerCase().includes("/state/")
|
|
1401
|
-
const hasSessionExport = exports2.some(
|
|
1402
|
-
(e) => e.name.toLowerCase().includes("session") || e.name.toLowerCase().includes("state") || e.name.toLowerCase().includes("
|
|
1073
|
+
const isSessionPath = file.toLowerCase().includes("/sessions/") || file.toLowerCase().includes("/state/");
|
|
1074
|
+
const hasSessionExport = (exports2 || []).some(
|
|
1075
|
+
(e) => e.name.toLowerCase().includes("session") || e.name.toLowerCase().includes("state") || e.name.toLowerCase().includes("store")
|
|
1403
1076
|
);
|
|
1404
|
-
return isSessionName || isSessionPath || hasSessionExport;
|
|
1077
|
+
return !!isSessionName || !!isSessionPath || !!hasSessionExport;
|
|
1078
|
+
}
|
|
1079
|
+
function isConfigFile(node) {
|
|
1080
|
+
const { file, exports: exports2 } = node;
|
|
1081
|
+
const lowerPath = file.toLowerCase();
|
|
1082
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1083
|
+
const configPatterns = [
|
|
1084
|
+
".config.",
|
|
1085
|
+
"tsconfig",
|
|
1086
|
+
"jest.config",
|
|
1087
|
+
"package.json",
|
|
1088
|
+
"aiready.json",
|
|
1089
|
+
"next.config",
|
|
1090
|
+
"sst.config"
|
|
1091
|
+
];
|
|
1092
|
+
const isConfigName = configPatterns.some((p) => fileName?.includes(p));
|
|
1093
|
+
const isConfigPath = lowerPath.includes("/config/") || lowerPath.includes("/settings/") || lowerPath.includes("/schemas/");
|
|
1094
|
+
const hasSchemaExports = (exports2 || []).some(
|
|
1095
|
+
(e) => e.name.toLowerCase().includes("schema") || e.name.toLowerCase().includes("config") || e.name.toLowerCase().includes("setting")
|
|
1096
|
+
);
|
|
1097
|
+
return !!isConfigName || !!isConfigPath || !!hasSchemaExports;
|
|
1405
1098
|
}
|
|
1406
1099
|
function isNextJsPage(node) {
|
|
1407
1100
|
const { file, exports: exports2 } = node;
|
|
@@ -1409,24 +1102,19 @@ function isNextJsPage(node) {
|
|
|
1409
1102
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1410
1103
|
const isInAppDir = lowerPath.includes("/app/") || lowerPath.startsWith("app/");
|
|
1411
1104
|
const isPageFile = fileName === "page.tsx" || fileName === "page.ts";
|
|
1412
|
-
if (!isInAppDir || !isPageFile)
|
|
1413
|
-
|
|
1414
|
-
}
|
|
1415
|
-
const exportNames = exports2.map((e) => e.name.toLowerCase());
|
|
1416
|
-
const hasDefaultExport = exports2.some((e) => e.type === "default");
|
|
1105
|
+
if (!isInAppDir || !isPageFile) return false;
|
|
1106
|
+
const hasDefaultExport = (exports2 || []).some((e) => e.type === "default");
|
|
1417
1107
|
const nextJsExports = [
|
|
1418
1108
|
"metadata",
|
|
1419
1109
|
"generatemetadata",
|
|
1420
1110
|
"faqjsonld",
|
|
1421
1111
|
"jsonld",
|
|
1422
|
-
"icon"
|
|
1423
|
-
"viewport",
|
|
1424
|
-
"dynamic"
|
|
1112
|
+
"icon"
|
|
1425
1113
|
];
|
|
1426
|
-
const hasNextJsExports =
|
|
1427
|
-
(
|
|
1114
|
+
const hasNextJsExports = (exports2 || []).some(
|
|
1115
|
+
(e) => nextJsExports.includes(e.name.toLowerCase())
|
|
1428
1116
|
);
|
|
1429
|
-
return hasDefaultExport || hasNextJsExports;
|
|
1117
|
+
return !!hasDefaultExport || !!hasNextJsExports;
|
|
1430
1118
|
}
|
|
1431
1119
|
function adjustCohesionForClassification(baseCohesion, classification, node) {
|
|
1432
1120
|
switch (classification) {
|
|
@@ -1434,55 +1122,24 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
|
|
|
1434
1122
|
return 1;
|
|
1435
1123
|
case "type-definition":
|
|
1436
1124
|
return 1;
|
|
1125
|
+
case "nextjs-page":
|
|
1126
|
+
return 1;
|
|
1437
1127
|
case "utility-module": {
|
|
1438
|
-
if (node
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1443
|
-
}
|
|
1128
|
+
if (node && hasRelatedExportNames(
|
|
1129
|
+
(node.exports || []).map((e) => e.name.toLowerCase())
|
|
1130
|
+
)) {
|
|
1131
|
+
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1444
1132
|
}
|
|
1445
1133
|
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1446
1134
|
}
|
|
1447
|
-
case "service-file":
|
|
1448
|
-
if (node?.exports.some((e) => e.type === "class")) {
|
|
1449
|
-
return Math.max(0.78, Math.min(1, baseCohesion + 0.4));
|
|
1450
|
-
}
|
|
1135
|
+
case "service-file":
|
|
1451
1136
|
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1452
|
-
|
|
1453
|
-
case "lambda-handler": {
|
|
1454
|
-
if (node) {
|
|
1455
|
-
const hasSingleEntry = node.exports.length === 1 || node.exports.some((e) => e.name.toLowerCase() === "handler");
|
|
1456
|
-
if (hasSingleEntry) {
|
|
1457
|
-
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1137
|
+
case "lambda-handler":
|
|
1460
1138
|
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1461
|
-
|
|
1462
|
-
case "email-template": {
|
|
1463
|
-
if (node) {
|
|
1464
|
-
const hasTemplateFunc = node.exports.some(
|
|
1465
|
-
(e) => e.name.toLowerCase().includes("render") || e.name.toLowerCase().includes("generate") || e.name.toLowerCase().includes("template")
|
|
1466
|
-
);
|
|
1467
|
-
if (hasTemplateFunc) {
|
|
1468
|
-
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1139
|
+
case "email-template":
|
|
1471
1140
|
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1472
|
-
|
|
1473
|
-
case "parser-file": {
|
|
1474
|
-
if (node) {
|
|
1475
|
-
const hasParseFunc = node.exports.some(
|
|
1476
|
-
(e) => e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert")
|
|
1477
|
-
);
|
|
1478
|
-
if (hasParseFunc) {
|
|
1479
|
-
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1141
|
+
case "parser-file":
|
|
1482
1142
|
return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
|
|
1483
|
-
}
|
|
1484
|
-
case "nextjs-page":
|
|
1485
|
-
return 1;
|
|
1486
1143
|
case "cohesive-module":
|
|
1487
1144
|
return Math.max(baseCohesion, 0.7);
|
|
1488
1145
|
case "mixed-concerns":
|
|
@@ -1495,136 +1152,138 @@ function hasRelatedExportNames(exportNames) {
|
|
|
1495
1152
|
if (exportNames.length < 2) return true;
|
|
1496
1153
|
const stems = /* @__PURE__ */ new Set();
|
|
1497
1154
|
const domains = /* @__PURE__ */ new Set();
|
|
1155
|
+
const verbs = [
|
|
1156
|
+
"get",
|
|
1157
|
+
"set",
|
|
1158
|
+
"create",
|
|
1159
|
+
"update",
|
|
1160
|
+
"delete",
|
|
1161
|
+
"fetch",
|
|
1162
|
+
"save",
|
|
1163
|
+
"load",
|
|
1164
|
+
"parse",
|
|
1165
|
+
"format",
|
|
1166
|
+
"validate"
|
|
1167
|
+
];
|
|
1168
|
+
const domainPatterns = [
|
|
1169
|
+
"user",
|
|
1170
|
+
"order",
|
|
1171
|
+
"product",
|
|
1172
|
+
"session",
|
|
1173
|
+
"email",
|
|
1174
|
+
"file",
|
|
1175
|
+
"db",
|
|
1176
|
+
"api",
|
|
1177
|
+
"config"
|
|
1178
|
+
];
|
|
1498
1179
|
for (const name of exportNames) {
|
|
1499
|
-
const verbs = [
|
|
1500
|
-
"get",
|
|
1501
|
-
"set",
|
|
1502
|
-
"create",
|
|
1503
|
-
"update",
|
|
1504
|
-
"delete",
|
|
1505
|
-
"fetch",
|
|
1506
|
-
"save",
|
|
1507
|
-
"load",
|
|
1508
|
-
"parse",
|
|
1509
|
-
"format",
|
|
1510
|
-
"validate",
|
|
1511
|
-
"convert",
|
|
1512
|
-
"transform",
|
|
1513
|
-
"build",
|
|
1514
|
-
"generate",
|
|
1515
|
-
"render",
|
|
1516
|
-
"send",
|
|
1517
|
-
"receive"
|
|
1518
|
-
];
|
|
1519
1180
|
for (const verb of verbs) {
|
|
1520
1181
|
if (name.startsWith(verb) && name.length > verb.length) {
|
|
1521
1182
|
stems.add(name.slice(verb.length).toLowerCase());
|
|
1522
1183
|
}
|
|
1523
1184
|
}
|
|
1524
|
-
const domainPatterns = [
|
|
1525
|
-
"user",
|
|
1526
|
-
"order",
|
|
1527
|
-
"product",
|
|
1528
|
-
"session",
|
|
1529
|
-
"email",
|
|
1530
|
-
"file",
|
|
1531
|
-
"db",
|
|
1532
|
-
"s3",
|
|
1533
|
-
"dynamo",
|
|
1534
|
-
"api",
|
|
1535
|
-
"config"
|
|
1536
|
-
];
|
|
1537
1185
|
for (const domain of domainPatterns) {
|
|
1538
|
-
if (name.includes(domain))
|
|
1539
|
-
|
|
1186
|
+
if (name.includes(domain)) domains.add(domain);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (stems.size === 1 || domains.size === 1) return true;
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
function adjustFragmentationForClassification(baseFragmentation, classification) {
|
|
1193
|
+
switch (classification) {
|
|
1194
|
+
case "barrel-export":
|
|
1195
|
+
return 0;
|
|
1196
|
+
case "type-definition":
|
|
1197
|
+
return 0;
|
|
1198
|
+
case "utility-module":
|
|
1199
|
+
case "service-file":
|
|
1200
|
+
case "lambda-handler":
|
|
1201
|
+
case "email-template":
|
|
1202
|
+
case "parser-file":
|
|
1203
|
+
case "nextjs-page":
|
|
1204
|
+
return baseFragmentation * 0.2;
|
|
1205
|
+
case "cohesive-module":
|
|
1206
|
+
return baseFragmentation * 0.3;
|
|
1207
|
+
case "mixed-concerns":
|
|
1208
|
+
return baseFragmentation;
|
|
1209
|
+
default:
|
|
1210
|
+
return baseFragmentation * 0.7;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/cluster-detector.ts
|
|
1215
|
+
function detectModuleClusters(graph, options) {
|
|
1216
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
1217
|
+
for (const [file, node] of graph.nodes.entries()) {
|
|
1218
|
+
const primaryDomain = node.exports[0]?.inferredDomain || "unknown";
|
|
1219
|
+
if (!domainMap.has(primaryDomain)) {
|
|
1220
|
+
domainMap.set(primaryDomain, []);
|
|
1221
|
+
}
|
|
1222
|
+
domainMap.get(primaryDomain).push(file);
|
|
1223
|
+
}
|
|
1224
|
+
const clusters = [];
|
|
1225
|
+
for (const [domain, files] of domainMap.entries()) {
|
|
1226
|
+
if (files.length < 2 || domain === "unknown") continue;
|
|
1227
|
+
const totalTokens = files.reduce((sum, file) => {
|
|
1228
|
+
const node = graph.nodes.get(file);
|
|
1229
|
+
return sum + (node?.tokenCost || 0);
|
|
1230
|
+
}, 0);
|
|
1231
|
+
let sharedImportRatio = 0;
|
|
1232
|
+
if (files.length >= 2) {
|
|
1233
|
+
const allImportSets = files.map(
|
|
1234
|
+
(f) => new Set(graph.nodes.get(f)?.imports || [])
|
|
1235
|
+
);
|
|
1236
|
+
let intersection = new Set(allImportSets[0]);
|
|
1237
|
+
let union = new Set(allImportSets[0]);
|
|
1238
|
+
for (let i = 1; i < allImportSets.length; i++) {
|
|
1239
|
+
const nextSet = allImportSets[i];
|
|
1240
|
+
intersection = new Set([...intersection].filter((x) => nextSet.has(x)));
|
|
1241
|
+
for (const x of nextSet) union.add(x);
|
|
1540
1242
|
}
|
|
1243
|
+
sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
|
|
1541
1244
|
}
|
|
1245
|
+
const fragmentation = calculateFragmentation(files, domain, {
|
|
1246
|
+
...options,
|
|
1247
|
+
sharedImportRatio
|
|
1248
|
+
});
|
|
1249
|
+
let totalCohesion = 0;
|
|
1250
|
+
files.forEach((f) => {
|
|
1251
|
+
const node = graph.nodes.get(f);
|
|
1252
|
+
if (node) totalCohesion += calculateEnhancedCohesion(node.exports);
|
|
1253
|
+
});
|
|
1254
|
+
const avgCohesion = totalCohesion / files.length;
|
|
1255
|
+
clusters.push({
|
|
1256
|
+
domain,
|
|
1257
|
+
files,
|
|
1258
|
+
totalTokens,
|
|
1259
|
+
fragmentationScore: fragmentation,
|
|
1260
|
+
avgCohesion,
|
|
1261
|
+
suggestedStructure: generateSuggestedStructure(
|
|
1262
|
+
files,
|
|
1263
|
+
totalTokens,
|
|
1264
|
+
fragmentation
|
|
1265
|
+
)
|
|
1266
|
+
});
|
|
1542
1267
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
if (uniquePrefixes.size === 1) return true;
|
|
1552
|
-
}
|
|
1553
|
-
const nounSets = exportNames.map((name) => {
|
|
1554
|
-
const tokens = name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1555
|
-
const skip = /* @__PURE__ */ new Set([
|
|
1556
|
-
"get",
|
|
1557
|
-
"set",
|
|
1558
|
-
"create",
|
|
1559
|
-
"update",
|
|
1560
|
-
"delete",
|
|
1561
|
-
"fetch",
|
|
1562
|
-
"save",
|
|
1563
|
-
"load",
|
|
1564
|
-
"parse",
|
|
1565
|
-
"format",
|
|
1566
|
-
"validate",
|
|
1567
|
-
"convert",
|
|
1568
|
-
"transform",
|
|
1569
|
-
"build",
|
|
1570
|
-
"generate",
|
|
1571
|
-
"render",
|
|
1572
|
-
"send",
|
|
1573
|
-
"receive",
|
|
1574
|
-
"find",
|
|
1575
|
-
"list",
|
|
1576
|
-
"add",
|
|
1577
|
-
"remove",
|
|
1578
|
-
"insert",
|
|
1579
|
-
"upsert",
|
|
1580
|
-
"put",
|
|
1581
|
-
"read",
|
|
1582
|
-
"write",
|
|
1583
|
-
"check",
|
|
1584
|
-
"handle",
|
|
1585
|
-
"process",
|
|
1586
|
-
"pending",
|
|
1587
|
-
"active",
|
|
1588
|
-
"current",
|
|
1589
|
-
"new",
|
|
1590
|
-
"old",
|
|
1591
|
-
"all"
|
|
1592
|
-
]);
|
|
1593
|
-
const singularize2 = (w) => w.endsWith("s") && w.length > 3 ? w.slice(0, -1) : w;
|
|
1594
|
-
return new Set(
|
|
1595
|
-
tokens.filter((t) => !skip.has(t) && t.length > 2).map(singularize2)
|
|
1596
|
-
);
|
|
1597
|
-
});
|
|
1598
|
-
if (nounSets.length >= 2 && nounSets.every((s) => s.size > 0)) {
|
|
1599
|
-
const [first, ...rest] = nounSets;
|
|
1600
|
-
const commonNouns = Array.from(first).filter(
|
|
1601
|
-
(n) => rest.every((s) => s.has(n))
|
|
1268
|
+
return clusters;
|
|
1269
|
+
}
|
|
1270
|
+
function generateSuggestedStructure(files, tokens, fragmentation) {
|
|
1271
|
+
const targetFiles = Math.max(1, Math.ceil(tokens / 1e4));
|
|
1272
|
+
const plan = [];
|
|
1273
|
+
if (fragmentation > 0.5) {
|
|
1274
|
+
plan.push(
|
|
1275
|
+
`Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
|
|
1602
1276
|
);
|
|
1603
|
-
if (commonNouns.length > 0) return true;
|
|
1604
1277
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
case "barrel-export":
|
|
1610
|
-
return 0;
|
|
1611
|
-
case "type-definition":
|
|
1612
|
-
return 0;
|
|
1613
|
-
case "utility-module":
|
|
1614
|
-
case "service-file":
|
|
1615
|
-
case "lambda-handler":
|
|
1616
|
-
case "email-template":
|
|
1617
|
-
case "parser-file":
|
|
1618
|
-
case "nextjs-page":
|
|
1619
|
-
return baseFragmentation * 0.2;
|
|
1620
|
-
case "cohesive-module":
|
|
1621
|
-
return baseFragmentation * 0.3;
|
|
1622
|
-
case "mixed-concerns":
|
|
1623
|
-
return baseFragmentation;
|
|
1624
|
-
default:
|
|
1625
|
-
return baseFragmentation * 0.7;
|
|
1278
|
+
if (tokens > 2e4) {
|
|
1279
|
+
plan.push(
|
|
1280
|
+
`Domain logic is very large (${Math.round(tokens / 1e3)}k tokens). Ensure clear sub-domain boundaries.`
|
|
1281
|
+
);
|
|
1626
1282
|
}
|
|
1283
|
+
return { targetFiles, consolidationPlan: plan };
|
|
1627
1284
|
}
|
|
1285
|
+
|
|
1286
|
+
// src/remediation.ts
|
|
1628
1287
|
function getClassificationRecommendations(classification, file, issues) {
|
|
1629
1288
|
switch (classification) {
|
|
1630
1289
|
case "barrel-export":
|
|
@@ -1682,9 +1341,167 @@ function getClassificationRecommendations(classification, file, issues) {
|
|
|
1682
1341
|
return issues;
|
|
1683
1342
|
}
|
|
1684
1343
|
}
|
|
1344
|
+
function getGeneralRecommendations(metrics, thresholds) {
|
|
1345
|
+
const recommendations = [];
|
|
1346
|
+
const issues = [];
|
|
1347
|
+
let severity = "info";
|
|
1348
|
+
if (metrics.contextBudget > thresholds.maxContextBudget) {
|
|
1349
|
+
issues.push(
|
|
1350
|
+
`High context budget: ${Math.round(metrics.contextBudget / 1e3)}k tokens`
|
|
1351
|
+
);
|
|
1352
|
+
recommendations.push(
|
|
1353
|
+
"Reduce dependencies or split the file to lower context window requirements"
|
|
1354
|
+
);
|
|
1355
|
+
severity = "major";
|
|
1356
|
+
}
|
|
1357
|
+
if (metrics.importDepth > thresholds.maxDepth) {
|
|
1358
|
+
issues.push(`Deep import chain: ${metrics.importDepth} levels`);
|
|
1359
|
+
recommendations.push("Flatten the dependency graph by reducing nesting");
|
|
1360
|
+
if (severity !== "critical") severity = "major";
|
|
1361
|
+
}
|
|
1362
|
+
if (metrics.circularDeps.length > 0) {
|
|
1363
|
+
issues.push(
|
|
1364
|
+
`Circular dependencies detected: ${metrics.circularDeps.length}`
|
|
1365
|
+
);
|
|
1366
|
+
recommendations.push(
|
|
1367
|
+
"Refactor to remove circular imports (use dependency injection or interfaces)"
|
|
1368
|
+
);
|
|
1369
|
+
severity = "critical";
|
|
1370
|
+
}
|
|
1371
|
+
if (metrics.cohesionScore < thresholds.minCohesion) {
|
|
1372
|
+
issues.push(`Low cohesion score: ${metrics.cohesionScore.toFixed(2)}`);
|
|
1373
|
+
recommendations.push(
|
|
1374
|
+
"Extract unrelated exports into separate domain-specific modules"
|
|
1375
|
+
);
|
|
1376
|
+
if (severity === "info") severity = "minor";
|
|
1377
|
+
}
|
|
1378
|
+
if (metrics.fragmentationScore > thresholds.maxFragmentation) {
|
|
1379
|
+
issues.push(
|
|
1380
|
+
`High domain fragmentation: ${metrics.fragmentationScore.toFixed(2)}`
|
|
1381
|
+
);
|
|
1382
|
+
recommendations.push(
|
|
1383
|
+
"Consolidate domain-related files into fewer directories"
|
|
1384
|
+
);
|
|
1385
|
+
if (severity === "info") severity = "minor";
|
|
1386
|
+
}
|
|
1387
|
+
return { recommendations, issues, severity };
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/analyzer.ts
|
|
1391
|
+
function calculateCohesion(exports2, filePath, options) {
|
|
1392
|
+
if (exports2.length <= 1) return 1;
|
|
1393
|
+
if (filePath && isTestFile(filePath)) return 1;
|
|
1394
|
+
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
1395
|
+
const uniqueDomains = new Set(domains.filter((d) => d !== "unknown"));
|
|
1396
|
+
const hasImports = exports2.some((e) => !!e.imports);
|
|
1397
|
+
if (!hasImports && !options?.weights) {
|
|
1398
|
+
if (uniqueDomains.size <= 1) return 1;
|
|
1399
|
+
return 0.4;
|
|
1400
|
+
}
|
|
1401
|
+
return calculateEnhancedCohesion(exports2, filePath, options);
|
|
1402
|
+
}
|
|
1403
|
+
function analyzeIssues(params) {
|
|
1404
|
+
const {
|
|
1405
|
+
file,
|
|
1406
|
+
importDepth,
|
|
1407
|
+
contextBudget,
|
|
1408
|
+
cohesionScore,
|
|
1409
|
+
fragmentationScore,
|
|
1410
|
+
maxDepth,
|
|
1411
|
+
maxContextBudget,
|
|
1412
|
+
minCohesion,
|
|
1413
|
+
maxFragmentation,
|
|
1414
|
+
circularDeps
|
|
1415
|
+
} = params;
|
|
1416
|
+
const issues = [];
|
|
1417
|
+
const recommendations = [];
|
|
1418
|
+
let severity = "info";
|
|
1419
|
+
let potentialSavings = 0;
|
|
1420
|
+
if (circularDeps.length > 0) {
|
|
1421
|
+
severity = "critical";
|
|
1422
|
+
issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
|
|
1423
|
+
recommendations.push(
|
|
1424
|
+
"Break circular dependencies by extracting interfaces or using dependency injection"
|
|
1425
|
+
);
|
|
1426
|
+
potentialSavings += contextBudget * 0.2;
|
|
1427
|
+
}
|
|
1428
|
+
if (importDepth > maxDepth * 1.5) {
|
|
1429
|
+
severity = "critical";
|
|
1430
|
+
issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
|
|
1431
|
+
recommendations.push("Flatten dependency tree or use facade pattern");
|
|
1432
|
+
potentialSavings += contextBudget * 0.3;
|
|
1433
|
+
} else if (importDepth > maxDepth) {
|
|
1434
|
+
if (severity !== "critical") severity = "major";
|
|
1435
|
+
issues.push(
|
|
1436
|
+
`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
|
|
1437
|
+
);
|
|
1438
|
+
recommendations.push("Consider reducing dependency depth");
|
|
1439
|
+
potentialSavings += contextBudget * 0.15;
|
|
1440
|
+
}
|
|
1441
|
+
if (contextBudget > maxContextBudget * 1.5) {
|
|
1442
|
+
severity = "critical";
|
|
1443
|
+
issues.push(
|
|
1444
|
+
`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
1445
|
+
);
|
|
1446
|
+
recommendations.push(
|
|
1447
|
+
"Split into smaller modules or reduce dependency tree"
|
|
1448
|
+
);
|
|
1449
|
+
potentialSavings += contextBudget * 0.4;
|
|
1450
|
+
} else if (contextBudget > maxContextBudget) {
|
|
1451
|
+
if (severity !== "critical") severity = "major";
|
|
1452
|
+
issues.push(
|
|
1453
|
+
`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
1454
|
+
);
|
|
1455
|
+
recommendations.push("Reduce file size or dependencies");
|
|
1456
|
+
potentialSavings += contextBudget * 0.2;
|
|
1457
|
+
}
|
|
1458
|
+
if (cohesionScore < minCohesion * 0.5) {
|
|
1459
|
+
if (severity !== "critical") severity = "major";
|
|
1460
|
+
issues.push(
|
|
1461
|
+
`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
|
|
1462
|
+
);
|
|
1463
|
+
recommendations.push(
|
|
1464
|
+
"Split file by domain - separate unrelated functionality"
|
|
1465
|
+
);
|
|
1466
|
+
potentialSavings += contextBudget * 0.25;
|
|
1467
|
+
} else if (cohesionScore < minCohesion) {
|
|
1468
|
+
if (severity === "info") severity = "minor";
|
|
1469
|
+
issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
|
|
1470
|
+
recommendations.push("Consider grouping related exports together");
|
|
1471
|
+
potentialSavings += contextBudget * 0.1;
|
|
1472
|
+
}
|
|
1473
|
+
if (fragmentationScore > maxFragmentation) {
|
|
1474
|
+
if (severity === "info" || severity === "minor") severity = "minor";
|
|
1475
|
+
issues.push(
|
|
1476
|
+
`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
|
|
1477
|
+
);
|
|
1478
|
+
recommendations.push("Consolidate with related files in same domain");
|
|
1479
|
+
potentialSavings += contextBudget * 0.3;
|
|
1480
|
+
}
|
|
1481
|
+
if (issues.length === 0) {
|
|
1482
|
+
issues.push("No significant issues detected");
|
|
1483
|
+
recommendations.push("File is well-structured for AI context usage");
|
|
1484
|
+
}
|
|
1485
|
+
if (isBuildArtifact(file)) {
|
|
1486
|
+
issues.push("Detected build artifact (bundled/output file)");
|
|
1487
|
+
recommendations.push("Exclude build outputs from analysis");
|
|
1488
|
+
severity = "info";
|
|
1489
|
+
potentialSavings = 0;
|
|
1490
|
+
}
|
|
1491
|
+
return {
|
|
1492
|
+
severity,
|
|
1493
|
+
issues,
|
|
1494
|
+
recommendations,
|
|
1495
|
+
potentialSavings: Math.floor(potentialSavings)
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function isBuildArtifact(filePath) {
|
|
1499
|
+
const lower = filePath.toLowerCase();
|
|
1500
|
+
return lower.includes("/node_modules/") || lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/out/") || lower.includes("/.next/");
|
|
1501
|
+
}
|
|
1685
1502
|
|
|
1686
1503
|
// src/scoring.ts
|
|
1687
|
-
var
|
|
1504
|
+
var import_core4 = require("@aiready/core");
|
|
1688
1505
|
function calculateContextScore(summary, costConfig) {
|
|
1689
1506
|
const {
|
|
1690
1507
|
avgContextBudget,
|
|
@@ -1780,14 +1597,16 @@ function calculateContextScore(summary, costConfig) {
|
|
|
1780
1597
|
priority: "high"
|
|
1781
1598
|
});
|
|
1782
1599
|
}
|
|
1783
|
-
const cfg = { ...
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1600
|
+
const cfg = { ...import_core4.DEFAULT_COST_CONFIG, ...costConfig };
|
|
1601
|
+
const estimatedMonthlyCost = (0, import_core4.calculateMonthlyCost)(
|
|
1602
|
+
avgContextBudget * (summary.totalFiles || 1),
|
|
1603
|
+
cfg
|
|
1604
|
+
);
|
|
1786
1605
|
const issues = [
|
|
1787
1606
|
...Array(criticalIssues).fill({ severity: "critical" }),
|
|
1788
1607
|
...Array(majorIssues).fill({ severity: "major" })
|
|
1789
1608
|
];
|
|
1790
|
-
const productivityImpact = (0,
|
|
1609
|
+
const productivityImpact = (0, import_core4.calculateProductivityImpact)(issues);
|
|
1791
1610
|
return {
|
|
1792
1611
|
toolName: "context-analyzer",
|
|
1793
1612
|
score,
|
|
@@ -1799,7 +1618,6 @@ function calculateContextScore(summary, costConfig) {
|
|
|
1799
1618
|
avgFragmentation: Math.round(avgFragmentation * 100) / 100,
|
|
1800
1619
|
criticalIssues,
|
|
1801
1620
|
majorIssues,
|
|
1802
|
-
// Business value metrics
|
|
1803
1621
|
estimatedMonthlyCost,
|
|
1804
1622
|
estimatedDeveloperHours: productivityImpact.totalHours
|
|
1805
1623
|
},
|
|
@@ -1807,10 +1625,18 @@ function calculateContextScore(summary, costConfig) {
|
|
|
1807
1625
|
recommendations
|
|
1808
1626
|
};
|
|
1809
1627
|
}
|
|
1628
|
+
function mapScoreToRating(score) {
|
|
1629
|
+
if (score >= 90) return "excellent";
|
|
1630
|
+
if (score >= 75) return "good";
|
|
1631
|
+
if (score >= 60) return "fair";
|
|
1632
|
+
if (score >= 40) return "needs work";
|
|
1633
|
+
return "critical";
|
|
1634
|
+
}
|
|
1810
1635
|
|
|
1811
|
-
// src/
|
|
1636
|
+
// src/defaults.ts
|
|
1637
|
+
var import_core5 = require("@aiready/core");
|
|
1812
1638
|
async function getSmartDefaults(directory, userOptions) {
|
|
1813
|
-
const files = await (0,
|
|
1639
|
+
const files = await (0, import_core5.scanFiles)({
|
|
1814
1640
|
rootDir: directory,
|
|
1815
1641
|
include: userOptions.include,
|
|
1816
1642
|
exclude: userOptions.exclude
|
|
@@ -1842,17 +1668,146 @@ async function getSmartDefaults(directory, userOptions) {
|
|
|
1842
1668
|
maxFragmentation = 0.8;
|
|
1843
1669
|
}
|
|
1844
1670
|
return {
|
|
1845
|
-
maxDepth,
|
|
1671
|
+
maxDepth,
|
|
1672
|
+
maxContextBudget,
|
|
1673
|
+
minCohesion,
|
|
1674
|
+
maxFragmentation,
|
|
1675
|
+
focus: "all",
|
|
1676
|
+
includeNodeModules: false,
|
|
1677
|
+
rootDir: userOptions.rootDir || directory,
|
|
1678
|
+
include: userOptions.include,
|
|
1679
|
+
exclude: userOptions.exclude
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// src/summary.ts
|
|
1684
|
+
function generateSummary(results) {
|
|
1685
|
+
if (results.length === 0) {
|
|
1686
|
+
return {
|
|
1687
|
+
totalFiles: 0,
|
|
1688
|
+
totalTokens: 0,
|
|
1689
|
+
avgContextBudget: 0,
|
|
1690
|
+
maxContextBudget: 0,
|
|
1691
|
+
avgImportDepth: 0,
|
|
1692
|
+
maxImportDepth: 0,
|
|
1693
|
+
deepFiles: [],
|
|
1694
|
+
avgFragmentation: 0,
|
|
1695
|
+
fragmentedModules: [],
|
|
1696
|
+
avgCohesion: 0,
|
|
1697
|
+
lowCohesionFiles: [],
|
|
1698
|
+
criticalIssues: 0,
|
|
1699
|
+
majorIssues: 0,
|
|
1700
|
+
minorIssues: 0,
|
|
1701
|
+
totalPotentialSavings: 0,
|
|
1702
|
+
topExpensiveFiles: []
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
const totalFiles = results.length;
|
|
1706
|
+
const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
|
|
1707
|
+
const totalContextBudget = results.reduce(
|
|
1708
|
+
(sum, r) => sum + r.contextBudget,
|
|
1709
|
+
0
|
|
1710
|
+
);
|
|
1711
|
+
const avgContextBudget = totalContextBudget / totalFiles;
|
|
1712
|
+
const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
|
|
1713
|
+
const avgImportDepth = results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
|
|
1714
|
+
const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
|
|
1715
|
+
const deepFiles = results.filter((r) => r.importDepth >= 5).map((r) => ({ file: r.file, depth: r.importDepth })).sort((a, b) => b.depth - a.depth).slice(0, 10);
|
|
1716
|
+
const avgFragmentation = results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
|
|
1717
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
1718
|
+
for (const result of results) {
|
|
1719
|
+
for (const domain of result.domains) {
|
|
1720
|
+
if (!moduleMap.has(domain)) moduleMap.set(domain, []);
|
|
1721
|
+
moduleMap.get(domain).push(result);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const fragmentedModules = [];
|
|
1725
|
+
for (const [domain, files] of moduleMap.entries()) {
|
|
1726
|
+
let jaccard2 = function(a, b) {
|
|
1727
|
+
const s1 = new Set(a || []);
|
|
1728
|
+
const s2 = new Set(b || []);
|
|
1729
|
+
if (s1.size === 0 && s2.size === 0) return 0;
|
|
1730
|
+
const inter = new Set([...s1].filter((x) => s2.has(x)));
|
|
1731
|
+
const uni = /* @__PURE__ */ new Set([...s1, ...s2]);
|
|
1732
|
+
return uni.size === 0 ? 0 : inter.size / uni.size;
|
|
1733
|
+
};
|
|
1734
|
+
var jaccard = jaccard2;
|
|
1735
|
+
if (files.length < 2) continue;
|
|
1736
|
+
const fragmentationScore = files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
|
|
1737
|
+
if (fragmentationScore < 0.3) continue;
|
|
1738
|
+
const totalTokens2 = files.reduce((sum, f) => sum + f.tokenCost, 0);
|
|
1739
|
+
const avgCohesion2 = files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
|
|
1740
|
+
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
1741
|
+
const filePaths = files.map((f) => f.file);
|
|
1742
|
+
const pathEntropy = calculatePathEntropy(filePaths);
|
|
1743
|
+
const directoryDistance = calculateDirectoryDistance(filePaths);
|
|
1744
|
+
let importSimTotal = 0;
|
|
1745
|
+
let importPairs = 0;
|
|
1746
|
+
for (let i = 0; i < files.length; i++) {
|
|
1747
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
1748
|
+
importSimTotal += jaccard2(
|
|
1749
|
+
files[i].dependencyList || [],
|
|
1750
|
+
files[j].dependencyList || []
|
|
1751
|
+
);
|
|
1752
|
+
importPairs++;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
|
|
1756
|
+
fragmentedModules.push({
|
|
1757
|
+
domain,
|
|
1758
|
+
files: files.map((f) => f.file),
|
|
1759
|
+
totalTokens: totalTokens2,
|
|
1760
|
+
fragmentationScore,
|
|
1761
|
+
avgCohesion: avgCohesion2,
|
|
1762
|
+
importCohesion,
|
|
1763
|
+
pathEntropy,
|
|
1764
|
+
directoryDistance,
|
|
1765
|
+
suggestedStructure: {
|
|
1766
|
+
targetFiles,
|
|
1767
|
+
consolidationPlan: [
|
|
1768
|
+
`Consolidate ${files.length} files across ${new Set(files.map((f) => f.file.split("/").slice(0, -1).join("/"))).size} directories`,
|
|
1769
|
+
`Target ~${targetFiles} core modules to reduce context switching`
|
|
1770
|
+
]
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
|
|
1775
|
+
const lowCohesionFiles = results.filter((r) => r.cohesionScore < 0.4).map((r) => ({ file: r.file, score: r.cohesionScore })).sort((a, b) => a.score - b.score).slice(0, 10);
|
|
1776
|
+
const criticalIssues = results.filter(
|
|
1777
|
+
(r) => r.severity === "critical"
|
|
1778
|
+
).length;
|
|
1779
|
+
const majorIssues = results.filter((r) => r.severity === "major").length;
|
|
1780
|
+
const minorIssues = results.filter((r) => r.severity === "minor").length;
|
|
1781
|
+
const totalPotentialSavings = results.reduce(
|
|
1782
|
+
(sum, r) => sum + r.potentialSavings,
|
|
1783
|
+
0
|
|
1784
|
+
);
|
|
1785
|
+
const topExpensiveFiles = results.sort((a, b) => b.contextBudget - a.contextBudget).slice(0, 10).map((r) => ({
|
|
1786
|
+
file: r.file,
|
|
1787
|
+
contextBudget: r.contextBudget,
|
|
1788
|
+
severity: r.severity
|
|
1789
|
+
}));
|
|
1790
|
+
return {
|
|
1791
|
+
totalFiles,
|
|
1792
|
+
totalTokens,
|
|
1793
|
+
avgContextBudget,
|
|
1846
1794
|
maxContextBudget,
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1795
|
+
avgImportDepth,
|
|
1796
|
+
maxImportDepth,
|
|
1797
|
+
deepFiles,
|
|
1798
|
+
avgFragmentation,
|
|
1799
|
+
fragmentedModules,
|
|
1800
|
+
avgCohesion,
|
|
1801
|
+
lowCohesionFiles,
|
|
1802
|
+
criticalIssues,
|
|
1803
|
+
majorIssues,
|
|
1804
|
+
minorIssues,
|
|
1805
|
+
totalPotentialSavings,
|
|
1806
|
+
topExpensiveFiles
|
|
1854
1807
|
};
|
|
1855
1808
|
}
|
|
1809
|
+
|
|
1810
|
+
// src/index.ts
|
|
1856
1811
|
async function analyzeContext(options) {
|
|
1857
1812
|
const {
|
|
1858
1813
|
maxDepth = 5,
|
|
@@ -1863,22 +1818,17 @@ async function analyzeContext(options) {
|
|
|
1863
1818
|
includeNodeModules = false,
|
|
1864
1819
|
...scanOptions
|
|
1865
1820
|
} = options;
|
|
1866
|
-
const files = await (0,
|
|
1821
|
+
const files = await (0, import_core7.scanFiles)({
|
|
1867
1822
|
...scanOptions,
|
|
1868
|
-
// Only add node_modules to exclude if includeNodeModules is false
|
|
1869
|
-
// The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
|
|
1870
|
-
// if user overrides the default exclude list
|
|
1871
1823
|
exclude: includeNodeModules && scanOptions.exclude ? scanOptions.exclude.filter(
|
|
1872
1824
|
(pattern) => pattern !== "**/node_modules/**"
|
|
1873
1825
|
) : scanOptions.exclude
|
|
1874
1826
|
});
|
|
1875
1827
|
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
1876
|
-
const tsJsFiles = files.filter((f) => !f.toLowerCase().endsWith(".py"));
|
|
1877
|
-
void tsJsFiles;
|
|
1878
1828
|
const fileContents = await Promise.all(
|
|
1879
1829
|
files.map(async (file) => ({
|
|
1880
1830
|
file,
|
|
1881
|
-
content: await (0,
|
|
1831
|
+
content: await (0, import_core7.readFileContent)(file)
|
|
1882
1832
|
}))
|
|
1883
1833
|
);
|
|
1884
1834
|
const graph = buildDependencyGraph(
|
|
@@ -1898,7 +1848,6 @@ async function analyzeContext(options) {
|
|
|
1898
1848
|
contextBudget: metric.contextBudget,
|
|
1899
1849
|
cohesionScore: metric.cohesion,
|
|
1900
1850
|
fragmentationScore: 0,
|
|
1901
|
-
// Python analyzer doesn't calculate fragmentation yet
|
|
1902
1851
|
maxDepth,
|
|
1903
1852
|
maxContextBudget,
|
|
1904
1853
|
minCohesion,
|
|
@@ -1912,7 +1861,6 @@ async function analyzeContext(options) {
|
|
|
1912
1861
|
tokenCost: Math.floor(
|
|
1913
1862
|
metric.contextBudget / (1 + metric.imports.length || 1)
|
|
1914
1863
|
),
|
|
1915
|
-
// Estimate
|
|
1916
1864
|
linesOfCode: metric.metrics.linesOfCode,
|
|
1917
1865
|
importDepth: metric.importDepth,
|
|
1918
1866
|
dependencyCount: metric.imports.length,
|
|
@@ -1924,13 +1872,11 @@ async function analyzeContext(options) {
|
|
|
1924
1872
|
),
|
|
1925
1873
|
cohesionScore: metric.cohesion,
|
|
1926
1874
|
domains: ["python"],
|
|
1927
|
-
// Generic for now
|
|
1928
1875
|
exportCount: metric.exports.length,
|
|
1929
1876
|
contextBudget: metric.contextBudget,
|
|
1930
1877
|
fragmentationScore: 0,
|
|
1931
1878
|
relatedFiles: [],
|
|
1932
1879
|
fileClassification: "unknown",
|
|
1933
|
-
// Python files not yet classified
|
|
1934
1880
|
severity,
|
|
1935
1881
|
issues,
|
|
1936
1882
|
recommendations,
|
|
@@ -1965,7 +1911,7 @@ async function analyzeContext(options) {
|
|
|
1965
1911
|
break;
|
|
1966
1912
|
}
|
|
1967
1913
|
}
|
|
1968
|
-
const {
|
|
1914
|
+
const { issues } = analyzeIssues({
|
|
1969
1915
|
file,
|
|
1970
1916
|
importDepth,
|
|
1971
1917
|
contextBudget,
|
|
@@ -1977,14 +1923,10 @@ async function analyzeContext(options) {
|
|
|
1977
1923
|
maxFragmentation,
|
|
1978
1924
|
circularDeps
|
|
1979
1925
|
});
|
|
1980
|
-
void severity;
|
|
1981
|
-
void issues;
|
|
1982
|
-
void recommendations;
|
|
1983
|
-
void potentialSavings;
|
|
1984
1926
|
const domains = [
|
|
1985
1927
|
...new Set(node.exports.map((e) => e.inferredDomain || "unknown"))
|
|
1986
1928
|
];
|
|
1987
|
-
const fileClassification = classifyFile(node
|
|
1929
|
+
const fileClassification = classifyFile(node);
|
|
1988
1930
|
const adjustedCohesionScore = adjustCohesionForClassification(
|
|
1989
1931
|
cohesionScore,
|
|
1990
1932
|
fileClassification,
|
|
@@ -2009,7 +1951,6 @@ async function analyzeContext(options) {
|
|
|
2009
1951
|
importDepth,
|
|
2010
1952
|
contextBudget,
|
|
2011
1953
|
cohesionScore: adjustedCohesionScore,
|
|
2012
|
-
// Use adjusted cohesion
|
|
2013
1954
|
fragmentationScore: adjustedFragmentationScore,
|
|
2014
1955
|
maxDepth,
|
|
2015
1956
|
maxContextBudget,
|
|
@@ -2026,7 +1967,6 @@ async function analyzeContext(options) {
|
|
|
2026
1967
|
dependencyList,
|
|
2027
1968
|
circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
|
|
2028
1969
|
cohesionScore: adjustedCohesionScore,
|
|
2029
|
-
// Report adjusted cohesion
|
|
2030
1970
|
domains,
|
|
2031
1971
|
exportCount: node.exports.length,
|
|
2032
1972
|
contextBudget,
|
|
@@ -2043,269 +1983,57 @@ async function analyzeContext(options) {
|
|
|
2043
1983
|
});
|
|
2044
1984
|
}
|
|
2045
1985
|
const allResults = [...results, ...pythonResults];
|
|
2046
|
-
|
|
1986
|
+
return allResults.sort((a, b) => {
|
|
2047
1987
|
const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
|
|
2048
1988
|
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
2049
1989
|
if (severityDiff !== 0) return severityDiff;
|
|
2050
1990
|
return b.contextBudget - a.contextBudget;
|
|
2051
1991
|
});
|
|
2052
|
-
return sorted;
|
|
2053
|
-
}
|
|
2054
|
-
function generateSummary(results) {
|
|
2055
|
-
if (results.length === 0) {
|
|
2056
|
-
return {
|
|
2057
|
-
totalFiles: 0,
|
|
2058
|
-
totalTokens: 0,
|
|
2059
|
-
avgContextBudget: 0,
|
|
2060
|
-
maxContextBudget: 0,
|
|
2061
|
-
avgImportDepth: 0,
|
|
2062
|
-
maxImportDepth: 0,
|
|
2063
|
-
deepFiles: [],
|
|
2064
|
-
avgFragmentation: 0,
|
|
2065
|
-
fragmentedModules: [],
|
|
2066
|
-
avgCohesion: 0,
|
|
2067
|
-
lowCohesionFiles: [],
|
|
2068
|
-
criticalIssues: 0,
|
|
2069
|
-
majorIssues: 0,
|
|
2070
|
-
minorIssues: 0,
|
|
2071
|
-
totalPotentialSavings: 0,
|
|
2072
|
-
topExpensiveFiles: []
|
|
2073
|
-
};
|
|
2074
|
-
}
|
|
2075
|
-
const totalFiles = results.length;
|
|
2076
|
-
const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
|
|
2077
|
-
const totalContextBudget = results.reduce(
|
|
2078
|
-
(sum, r) => sum + r.contextBudget,
|
|
2079
|
-
0
|
|
2080
|
-
);
|
|
2081
|
-
const avgContextBudget = totalContextBudget / totalFiles;
|
|
2082
|
-
const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
|
|
2083
|
-
const avgImportDepth = results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
|
|
2084
|
-
const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
|
|
2085
|
-
const deepFiles = results.filter((r) => r.importDepth >= 5).map((r) => ({ file: r.file, depth: r.importDepth })).sort((a, b) => b.depth - a.depth).slice(0, 10);
|
|
2086
|
-
const avgFragmentation = results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
|
|
2087
|
-
const moduleMap = /* @__PURE__ */ new Map();
|
|
2088
|
-
for (const result of results) {
|
|
2089
|
-
for (const domain of result.domains) {
|
|
2090
|
-
if (!moduleMap.has(domain)) {
|
|
2091
|
-
moduleMap.set(domain, []);
|
|
2092
|
-
}
|
|
2093
|
-
moduleMap.get(domain).push(result);
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
const fragmentedModules = [];
|
|
2097
|
-
for (const [domain, files] of moduleMap.entries()) {
|
|
2098
|
-
let jaccard2 = function(a, b) {
|
|
2099
|
-
const s1 = new Set(a || []);
|
|
2100
|
-
const s2 = new Set(b || []);
|
|
2101
|
-
if (s1.size === 0 && s2.size === 0) return 0;
|
|
2102
|
-
const inter = new Set([...s1].filter((x) => s2.has(x)));
|
|
2103
|
-
const uni = /* @__PURE__ */ new Set([...s1, ...s2]);
|
|
2104
|
-
return uni.size === 0 ? 0 : inter.size / uni.size;
|
|
2105
|
-
};
|
|
2106
|
-
var jaccard = jaccard2;
|
|
2107
|
-
if (files.length < 2) continue;
|
|
2108
|
-
const fragmentationScore = files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
|
|
2109
|
-
if (fragmentationScore < 0.3) continue;
|
|
2110
|
-
const totalTokens2 = files.reduce((sum, f) => sum + f.tokenCost, 0);
|
|
2111
|
-
const avgCohesion2 = files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
|
|
2112
|
-
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
2113
|
-
const filePaths = files.map((f) => f.file);
|
|
2114
|
-
const pathEntropy = calculatePathEntropy(filePaths);
|
|
2115
|
-
const directoryDistance = calculateDirectoryDistance(filePaths);
|
|
2116
|
-
let importSimTotal = 0;
|
|
2117
|
-
let importPairs = 0;
|
|
2118
|
-
for (let i = 0; i < files.length; i++) {
|
|
2119
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
2120
|
-
importSimTotal += jaccard2(
|
|
2121
|
-
files[i].dependencyList || [],
|
|
2122
|
-
files[j].dependencyList || []
|
|
2123
|
-
);
|
|
2124
|
-
importPairs++;
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
|
|
2128
|
-
fragmentedModules.push({
|
|
2129
|
-
domain,
|
|
2130
|
-
files: files.map((f) => f.file),
|
|
2131
|
-
totalTokens: totalTokens2,
|
|
2132
|
-
fragmentationScore,
|
|
2133
|
-
pathEntropy,
|
|
2134
|
-
directoryDistance,
|
|
2135
|
-
importCohesion,
|
|
2136
|
-
avgCohesion: avgCohesion2,
|
|
2137
|
-
suggestedStructure: {
|
|
2138
|
-
targetFiles,
|
|
2139
|
-
consolidationPlan: [
|
|
2140
|
-
`Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s)`,
|
|
2141
|
-
`Current token cost: ${totalTokens2.toLocaleString()}`,
|
|
2142
|
-
`Estimated savings: ${Math.floor(totalTokens2 * 0.3).toLocaleString()} tokens (30%)`
|
|
2143
|
-
]
|
|
2144
|
-
}
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
2148
|
-
const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
|
|
2149
|
-
const lowCohesionFiles = results.filter((r) => r.cohesionScore < 0.6).map((r) => ({ file: r.file, score: r.cohesionScore })).sort((a, b) => a.score - b.score).slice(0, 10);
|
|
2150
|
-
const criticalIssues = results.filter(
|
|
2151
|
-
(r) => r.severity === "critical"
|
|
2152
|
-
).length;
|
|
2153
|
-
const majorIssues = results.filter((r) => r.severity === "major").length;
|
|
2154
|
-
const minorIssues = results.filter((r) => r.severity === "minor").length;
|
|
2155
|
-
const totalPotentialSavings = results.reduce(
|
|
2156
|
-
(sum, r) => sum + r.potentialSavings,
|
|
2157
|
-
0
|
|
2158
|
-
);
|
|
2159
|
-
const topExpensiveFiles = results.sort((a, b) => b.contextBudget - a.contextBudget).slice(0, 10).map((r) => ({
|
|
2160
|
-
file: r.file,
|
|
2161
|
-
contextBudget: r.contextBudget,
|
|
2162
|
-
severity: r.severity
|
|
2163
|
-
}));
|
|
2164
|
-
return {
|
|
2165
|
-
totalFiles,
|
|
2166
|
-
totalTokens,
|
|
2167
|
-
avgContextBudget,
|
|
2168
|
-
maxContextBudget,
|
|
2169
|
-
avgImportDepth,
|
|
2170
|
-
maxImportDepth,
|
|
2171
|
-
deepFiles,
|
|
2172
|
-
avgFragmentation,
|
|
2173
|
-
fragmentedModules: fragmentedModules.slice(0, 10),
|
|
2174
|
-
avgCohesion,
|
|
2175
|
-
lowCohesionFiles,
|
|
2176
|
-
criticalIssues,
|
|
2177
|
-
majorIssues,
|
|
2178
|
-
minorIssues,
|
|
2179
|
-
totalPotentialSavings,
|
|
2180
|
-
topExpensiveFiles
|
|
2181
|
-
};
|
|
2182
|
-
}
|
|
2183
|
-
function analyzeIssues(params) {
|
|
2184
|
-
const {
|
|
2185
|
-
file,
|
|
2186
|
-
importDepth,
|
|
2187
|
-
contextBudget,
|
|
2188
|
-
cohesionScore,
|
|
2189
|
-
fragmentationScore,
|
|
2190
|
-
maxDepth,
|
|
2191
|
-
maxContextBudget,
|
|
2192
|
-
minCohesion,
|
|
2193
|
-
maxFragmentation,
|
|
2194
|
-
circularDeps
|
|
2195
|
-
} = params;
|
|
2196
|
-
const issues = [];
|
|
2197
|
-
const recommendations = [];
|
|
2198
|
-
let severity = "info";
|
|
2199
|
-
let potentialSavings = 0;
|
|
2200
|
-
if (circularDeps.length > 0) {
|
|
2201
|
-
severity = "critical";
|
|
2202
|
-
issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
|
|
2203
|
-
recommendations.push(
|
|
2204
|
-
"Break circular dependencies by extracting interfaces or using dependency injection"
|
|
2205
|
-
);
|
|
2206
|
-
potentialSavings += contextBudget * 0.2;
|
|
2207
|
-
}
|
|
2208
|
-
if (importDepth > maxDepth * 1.5) {
|
|
2209
|
-
severity = severity === "critical" ? "critical" : "critical";
|
|
2210
|
-
issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
|
|
2211
|
-
recommendations.push("Flatten dependency tree or use facade pattern");
|
|
2212
|
-
potentialSavings += contextBudget * 0.3;
|
|
2213
|
-
} else if (importDepth > maxDepth) {
|
|
2214
|
-
severity = severity === "critical" ? "critical" : "major";
|
|
2215
|
-
issues.push(
|
|
2216
|
-
`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
|
|
2217
|
-
);
|
|
2218
|
-
recommendations.push("Consider reducing dependency depth");
|
|
2219
|
-
potentialSavings += contextBudget * 0.15;
|
|
2220
|
-
}
|
|
2221
|
-
if (contextBudget > maxContextBudget * 1.5) {
|
|
2222
|
-
severity = severity === "critical" ? "critical" : "critical";
|
|
2223
|
-
issues.push(
|
|
2224
|
-
`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
2225
|
-
);
|
|
2226
|
-
recommendations.push(
|
|
2227
|
-
"Split into smaller modules or reduce dependency tree"
|
|
2228
|
-
);
|
|
2229
|
-
potentialSavings += contextBudget * 0.4;
|
|
2230
|
-
} else if (contextBudget > maxContextBudget) {
|
|
2231
|
-
severity = severity === "critical" || severity === "major" ? severity : "major";
|
|
2232
|
-
issues.push(
|
|
2233
|
-
`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
2234
|
-
);
|
|
2235
|
-
recommendations.push("Reduce file size or dependencies");
|
|
2236
|
-
potentialSavings += contextBudget * 0.2;
|
|
2237
|
-
}
|
|
2238
|
-
if (cohesionScore < minCohesion * 0.5) {
|
|
2239
|
-
severity = severity === "critical" ? "critical" : "major";
|
|
2240
|
-
issues.push(
|
|
2241
|
-
`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
|
|
2242
|
-
);
|
|
2243
|
-
recommendations.push(
|
|
2244
|
-
"Split file by domain - separate unrelated functionality"
|
|
2245
|
-
);
|
|
2246
|
-
potentialSavings += contextBudget * 0.25;
|
|
2247
|
-
} else if (cohesionScore < minCohesion) {
|
|
2248
|
-
severity = severity === "critical" || severity === "major" ? severity : "minor";
|
|
2249
|
-
issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
|
|
2250
|
-
recommendations.push("Consider grouping related exports together");
|
|
2251
|
-
potentialSavings += contextBudget * 0.1;
|
|
2252
|
-
}
|
|
2253
|
-
if (fragmentationScore > maxFragmentation) {
|
|
2254
|
-
severity = severity === "critical" || severity === "major" ? severity : "minor";
|
|
2255
|
-
issues.push(
|
|
2256
|
-
`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
|
|
2257
|
-
);
|
|
2258
|
-
recommendations.push("Consolidate with related files in same domain");
|
|
2259
|
-
potentialSavings += contextBudget * 0.3;
|
|
2260
|
-
}
|
|
2261
|
-
if (issues.length === 0) {
|
|
2262
|
-
issues.push("No significant issues detected");
|
|
2263
|
-
recommendations.push("File is well-structured for AI context usage");
|
|
2264
|
-
}
|
|
2265
|
-
if (isBuildArtifact(file)) {
|
|
2266
|
-
issues.push("Detected build artifact (bundled/output file)");
|
|
2267
|
-
recommendations.push(
|
|
2268
|
-
"Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis"
|
|
2269
|
-
);
|
|
2270
|
-
severity = downgradeSeverity(severity);
|
|
2271
|
-
potentialSavings = 0;
|
|
2272
|
-
}
|
|
2273
|
-
return {
|
|
2274
|
-
severity,
|
|
2275
|
-
issues,
|
|
2276
|
-
recommendations,
|
|
2277
|
-
potentialSavings: Math.floor(potentialSavings)
|
|
2278
|
-
};
|
|
2279
|
-
}
|
|
2280
|
-
function isBuildArtifact(filePath) {
|
|
2281
|
-
const lower = filePath.toLowerCase();
|
|
2282
|
-
return lower.includes("/node_modules/") || lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/out/") || lower.includes("/output/") || lower.includes("/cdk.out/") || lower.includes("/.next/") || /\/asset\.[^/]+\//.test(lower);
|
|
2283
|
-
}
|
|
2284
|
-
function downgradeSeverity(s) {
|
|
2285
|
-
switch (s) {
|
|
2286
|
-
case "critical":
|
|
2287
|
-
return "minor";
|
|
2288
|
-
case "major":
|
|
2289
|
-
return "minor";
|
|
2290
|
-
case "minor":
|
|
2291
|
-
return "info";
|
|
2292
|
-
default:
|
|
2293
|
-
return "info";
|
|
2294
|
-
}
|
|
2295
1992
|
}
|
|
2296
1993
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2297
1994
|
0 && (module.exports = {
|
|
1995
|
+
adjustCohesionForClassification,
|
|
2298
1996
|
adjustFragmentationForClassification,
|
|
2299
1997
|
analyzeContext,
|
|
1998
|
+
analyzeIssues,
|
|
2300
1999
|
buildCoUsageMatrix,
|
|
2000
|
+
buildDependencyGraph,
|
|
2301
2001
|
buildTypeGraph,
|
|
2002
|
+
calculateCohesion,
|
|
2003
|
+
calculateContextBudget,
|
|
2302
2004
|
calculateContextScore,
|
|
2005
|
+
calculateDirectoryDistance,
|
|
2303
2006
|
calculateDomainConfidence,
|
|
2007
|
+
calculateEnhancedCohesion,
|
|
2008
|
+
calculateFragmentation,
|
|
2009
|
+
calculateImportDepth,
|
|
2010
|
+
calculatePathEntropy,
|
|
2011
|
+
calculateStructuralCohesionFromCoUsage,
|
|
2304
2012
|
classifyFile,
|
|
2013
|
+
detectCircularDependencies,
|
|
2014
|
+
detectModuleClusters,
|
|
2015
|
+
extractDomainKeywordsFromPaths,
|
|
2016
|
+
extractExports,
|
|
2017
|
+
extractImportsFromContent,
|
|
2305
2018
|
findConsolidationCandidates,
|
|
2306
2019
|
findSemanticClusters,
|
|
2307
2020
|
generateSummary,
|
|
2021
|
+
getClassificationRecommendations,
|
|
2308
2022
|
getCoUsageData,
|
|
2023
|
+
getGeneralRecommendations,
|
|
2309
2024
|
getSmartDefaults,
|
|
2310
|
-
|
|
2025
|
+
getTransitiveDependencies,
|
|
2026
|
+
inferDomain,
|
|
2027
|
+
inferDomainFromSemantics,
|
|
2028
|
+
isBarrelExport,
|
|
2029
|
+
isConfigFile,
|
|
2030
|
+
isEmailTemplate,
|
|
2031
|
+
isLambdaHandler,
|
|
2032
|
+
isNextJsPage,
|
|
2033
|
+
isParserFile,
|
|
2034
|
+
isServiceFile,
|
|
2035
|
+
isSessionFile,
|
|
2036
|
+
isTypeDefinition,
|
|
2037
|
+
isUtilityModule,
|
|
2038
|
+
mapScoreToRating
|
|
2311
2039
|
});
|