@aiready/context-analyzer 0.9.35 → 0.9.38
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/.github/FUNDING.yml +2 -2
- package/.turbo/turbo-build.log +9 -9
- package/.turbo/turbo-lint.log +6 -0
- package/.turbo/turbo-test.log +23 -22
- package/CONTRIBUTING.md +10 -2
- package/dist/chunk-7LUSCLGR.mjs +2058 -0
- package/dist/chunk-EBXG2Q5Y.mjs +2059 -0
- package/dist/cli.js +351 -86
- package/dist/cli.mjs +137 -33
- package/dist/index.js +236 -59
- package/dist/index.mjs +1 -1
- package/dist/python-context-GOH747QU.mjs +202 -0
- package/dist/python-context-TBI5FVFY.mjs +203 -0
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +69 -17
- package/src/__tests__/auto-detection.test.ts +1 -1
- package/src/__tests__/enhanced-cohesion.test.ts +19 -7
- package/src/__tests__/file-classification.test.ts +189 -54
- package/src/__tests__/fragmentation-advanced.test.ts +2 -11
- package/src/__tests__/fragmentation-coupling.test.ts +8 -2
- package/src/__tests__/fragmentation-log.test.ts +9 -9
- package/src/__tests__/scoring.test.ts +19 -7
- package/src/__tests__/structural-cohesion.test.ts +33 -21
- package/src/analyzer.ts +727 -375
- package/src/analyzers/python-context.ts +34 -10
- package/src/cli.ts +223 -59
- package/src/index.ts +112 -55
- package/src/scoring.ts +53 -43
- package/src/semantic-analysis.ts +75 -56
- package/src/types.ts +12 -13
package/dist/index.js
CHANGED
|
@@ -43,7 +43,12 @@ async function analyzePythonContext(files, rootDir) {
|
|
|
43
43
|
return results;
|
|
44
44
|
}
|
|
45
45
|
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
46
|
-
|
|
46
|
+
void import_path.relative;
|
|
47
|
+
void import_path.join;
|
|
48
|
+
const dependencyGraph = await buildPythonDependencyGraph(
|
|
49
|
+
pythonFiles,
|
|
50
|
+
rootDir
|
|
51
|
+
);
|
|
47
52
|
for (const file of pythonFiles) {
|
|
48
53
|
try {
|
|
49
54
|
const code = await import_fs.default.promises.readFile(file, "utf-8");
|
|
@@ -59,10 +64,21 @@ async function analyzePythonContext(files, rootDir) {
|
|
|
59
64
|
type: exp.type
|
|
60
65
|
}));
|
|
61
66
|
const linesOfCode = code.split("\n").length;
|
|
62
|
-
const importDepth = await calculatePythonImportDepth(
|
|
63
|
-
|
|
67
|
+
const importDepth = await calculatePythonImportDepth(
|
|
68
|
+
file,
|
|
69
|
+
dependencyGraph,
|
|
70
|
+
/* @__PURE__ */ new Set()
|
|
71
|
+
);
|
|
72
|
+
const contextBudget = estimateContextBudget(
|
|
73
|
+
code,
|
|
74
|
+
imports,
|
|
75
|
+
dependencyGraph
|
|
76
|
+
);
|
|
64
77
|
const cohesion = calculatePythonCohesion(exports2, imports);
|
|
65
|
-
const circularDependencies = detectCircularDependencies2(
|
|
78
|
+
const circularDependencies = detectCircularDependencies2(
|
|
79
|
+
file,
|
|
80
|
+
dependencyGraph
|
|
81
|
+
);
|
|
66
82
|
results.push({
|
|
67
83
|
file,
|
|
68
84
|
importDepth,
|
|
@@ -100,6 +116,7 @@ async function buildPythonDependencyGraph(files, rootDir) {
|
|
|
100
116
|
}
|
|
101
117
|
graph.set(file, dependencies);
|
|
102
118
|
} catch (error) {
|
|
119
|
+
void error;
|
|
103
120
|
}
|
|
104
121
|
}
|
|
105
122
|
return graph;
|
|
@@ -163,6 +180,7 @@ async function calculatePythonImportDepth(file, dependencyGraph, visited, depth
|
|
|
163
180
|
return maxDepth;
|
|
164
181
|
}
|
|
165
182
|
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
183
|
+
void dependencyGraph;
|
|
166
184
|
let budget = (0, import_core3.estimateTokens)(code);
|
|
167
185
|
const avgTokensPerDep = 500;
|
|
168
186
|
budget += imports.length * avgTokensPerDep;
|
|
@@ -250,6 +268,7 @@ var import_core = require("@aiready/core");
|
|
|
250
268
|
function buildCoUsageMatrix(graph) {
|
|
251
269
|
const coUsageMatrix = /* @__PURE__ */ new Map();
|
|
252
270
|
for (const [sourceFile, node] of graph.nodes) {
|
|
271
|
+
void sourceFile;
|
|
253
272
|
const imports = node.imports;
|
|
254
273
|
for (let i = 0; i < imports.length; i++) {
|
|
255
274
|
const fileA = imports[i];
|
|
@@ -330,7 +349,7 @@ function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGr
|
|
|
330
349
|
const assignments = [];
|
|
331
350
|
const domainSignals = /* @__PURE__ */ new Map();
|
|
332
351
|
const coUsages = coUsageMatrix.get(file) || /* @__PURE__ */ new Map();
|
|
333
|
-
const strongCoUsages = Array.from(coUsages.entries()).filter(([
|
|
352
|
+
const strongCoUsages = Array.from(coUsages.entries()).filter(([, count]) => count >= 3).map(([coFile]) => coFile);
|
|
334
353
|
for (const coFile of strongCoUsages) {
|
|
335
354
|
const coNode = graph.nodes.get(coFile);
|
|
336
355
|
if (coNode) {
|
|
@@ -408,8 +427,12 @@ function findConsolidationCandidates(graph, coUsageMatrix, typeGraph, minCoUsage
|
|
|
408
427
|
if (coUsageCount < minCoUsage) continue;
|
|
409
428
|
const nodeB = graph.nodes.get(fileB);
|
|
410
429
|
if (!nodeB) continue;
|
|
411
|
-
const typesA = new Set(
|
|
412
|
-
|
|
430
|
+
const typesA = new Set(
|
|
431
|
+
nodeA.exports.flatMap((e) => e.typeReferences || [])
|
|
432
|
+
);
|
|
433
|
+
const typesB = new Set(
|
|
434
|
+
nodeB.exports.flatMap((e) => e.typeReferences || [])
|
|
435
|
+
);
|
|
413
436
|
const sharedTypes = Array.from(typesA).filter((t) => typesB.has(t));
|
|
414
437
|
if (sharedTypes.length >= minSharedTypes) {
|
|
415
438
|
const strength = coUsageCount / 10 + sharedTypes.length / 5;
|
|
@@ -437,7 +460,26 @@ function extractDomainKeywordsFromPaths(files) {
|
|
|
437
460
|
const folderNames = /* @__PURE__ */ new Set();
|
|
438
461
|
for (const { file } of files) {
|
|
439
462
|
const segments = file.split("/");
|
|
440
|
-
const skipFolders = /* @__PURE__ */ new Set([
|
|
463
|
+
const skipFolders = /* @__PURE__ */ new Set([
|
|
464
|
+
"src",
|
|
465
|
+
"lib",
|
|
466
|
+
"dist",
|
|
467
|
+
"build",
|
|
468
|
+
"node_modules",
|
|
469
|
+
"test",
|
|
470
|
+
"tests",
|
|
471
|
+
"__tests__",
|
|
472
|
+
"spec",
|
|
473
|
+
"e2e",
|
|
474
|
+
"scripts",
|
|
475
|
+
"components",
|
|
476
|
+
"utils",
|
|
477
|
+
"helpers",
|
|
478
|
+
"util",
|
|
479
|
+
"helper",
|
|
480
|
+
"api",
|
|
481
|
+
"apis"
|
|
482
|
+
]);
|
|
441
483
|
for (const segment of segments) {
|
|
442
484
|
const normalized = segment.toLowerCase();
|
|
443
485
|
if (normalized && !skipFolders.has(normalized) && !normalized.includes(".")) {
|
|
@@ -469,13 +511,19 @@ function singularize(word) {
|
|
|
469
511
|
}
|
|
470
512
|
return word;
|
|
471
513
|
}
|
|
472
|
-
function buildDependencyGraph(files) {
|
|
514
|
+
function buildDependencyGraph(files, options) {
|
|
473
515
|
const nodes = /* @__PURE__ */ new Map();
|
|
474
516
|
const edges = /* @__PURE__ */ new Map();
|
|
475
|
-
const autoDetectedKeywords = extractDomainKeywordsFromPaths(files);
|
|
517
|
+
const autoDetectedKeywords = options?.domainKeywords ?? extractDomainKeywordsFromPaths(files);
|
|
518
|
+
void import_core.calculateImportSimilarity;
|
|
476
519
|
for (const { file, content } of files) {
|
|
477
520
|
const imports = extractImportsFromContent(content);
|
|
478
|
-
const exports2 = extractExportsWithAST(
|
|
521
|
+
const exports2 = extractExportsWithAST(
|
|
522
|
+
content,
|
|
523
|
+
file,
|
|
524
|
+
{ domainKeywords: autoDetectedKeywords },
|
|
525
|
+
imports
|
|
526
|
+
);
|
|
479
527
|
const tokenCost = (0, import_core.estimateTokens)(content);
|
|
480
528
|
const linesOfCode = content.split("\n").length;
|
|
481
529
|
nodes.set(file, {
|
|
@@ -619,7 +667,9 @@ function isTestFile(filePath) {
|
|
|
619
667
|
}
|
|
620
668
|
function calculateFragmentation(files, domain, options) {
|
|
621
669
|
if (files.length <= 1) return 0;
|
|
622
|
-
const directories = new Set(
|
|
670
|
+
const directories = new Set(
|
|
671
|
+
files.map((f) => f.split("/").slice(0, -1).join("/"))
|
|
672
|
+
);
|
|
623
673
|
const uniqueDirs = directories.size;
|
|
624
674
|
if (options?.useLogScale) {
|
|
625
675
|
if (uniqueDirs <= 1) return 0;
|
|
@@ -692,7 +742,9 @@ function detectModuleClusters(graph, options) {
|
|
|
692
742
|
const node = graph.nodes.get(file);
|
|
693
743
|
return sum + (node?.tokenCost || 0);
|
|
694
744
|
}, 0);
|
|
695
|
-
const baseFragmentation = calculateFragmentation(files, domain, {
|
|
745
|
+
const baseFragmentation = calculateFragmentation(files, domain, {
|
|
746
|
+
useLogScale: !!options?.useLogScale
|
|
747
|
+
});
|
|
696
748
|
let importSimilarityTotal = 0;
|
|
697
749
|
let importComparisons = 0;
|
|
698
750
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -713,7 +765,9 @@ function detectModuleClusters(graph, options) {
|
|
|
713
765
|
const directoryDistance = calculateDirectoryDistance(files);
|
|
714
766
|
const avgCohesion = files.reduce((sum, file) => {
|
|
715
767
|
const node = graph.nodes.get(file);
|
|
716
|
-
return sum + (node ? calculateCohesion(node.exports, file, {
|
|
768
|
+
return sum + (node ? calculateCohesion(node.exports, file, {
|
|
769
|
+
coUsageMatrix: graph.coUsageMatrix
|
|
770
|
+
}) : 0);
|
|
717
771
|
}, 0) / files.length;
|
|
718
772
|
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
719
773
|
const consolidationPlan = generateConsolidationPlan(
|
|
@@ -761,7 +815,12 @@ function extractExports(content, filePath, domainOptions, fileImports) {
|
|
|
761
815
|
while ((match = pattern.exec(content)) !== null) {
|
|
762
816
|
const name = match[1] || "default";
|
|
763
817
|
const type = types[index];
|
|
764
|
-
const inferredDomain = inferDomain(
|
|
818
|
+
const inferredDomain = inferDomain(
|
|
819
|
+
name,
|
|
820
|
+
filePath,
|
|
821
|
+
domainOptions,
|
|
822
|
+
fileImports
|
|
823
|
+
);
|
|
765
824
|
exports2.push({ name, type, inferredDomain });
|
|
766
825
|
}
|
|
767
826
|
});
|
|
@@ -869,11 +928,17 @@ function extractExportsWithAST(content, filePath, domainOptions, fileImports) {
|
|
|
869
928
|
return astExports.map((exp) => ({
|
|
870
929
|
name: exp.name,
|
|
871
930
|
type: exp.type,
|
|
872
|
-
inferredDomain: inferDomain(
|
|
931
|
+
inferredDomain: inferDomain(
|
|
932
|
+
exp.name,
|
|
933
|
+
filePath,
|
|
934
|
+
domainOptions,
|
|
935
|
+
fileImports
|
|
936
|
+
),
|
|
873
937
|
imports: exp.imports,
|
|
874
938
|
dependencies: exp.dependencies
|
|
875
939
|
}));
|
|
876
940
|
} catch (error) {
|
|
941
|
+
void error;
|
|
877
942
|
return extractExports(content, filePath, domainOptions, fileImports);
|
|
878
943
|
}
|
|
879
944
|
}
|
|
@@ -888,15 +953,24 @@ function calculateEnhancedCohesion(exports2, filePath, options) {
|
|
|
888
953
|
const importCohesion = hasImportData ? calculateImportBasedCohesion(exports2) : void 0;
|
|
889
954
|
const coUsageMatrix = options?.coUsageMatrix;
|
|
890
955
|
const structuralCohesion = filePath && coUsageMatrix ? calculateStructuralCohesionFromCoUsage(filePath, coUsageMatrix) : void 0;
|
|
891
|
-
const defaultWeights = {
|
|
956
|
+
const defaultWeights = {
|
|
957
|
+
importBased: 0.5,
|
|
958
|
+
structural: 0.3,
|
|
959
|
+
domainBased: 0.2
|
|
960
|
+
};
|
|
892
961
|
const weights = { ...defaultWeights, ...options?.weights || {} };
|
|
893
962
|
const signals = [];
|
|
894
|
-
if (importCohesion !== void 0)
|
|
895
|
-
|
|
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 });
|
|
896
967
|
signals.push({ score: domainCohesion, weight: weights.domainBased });
|
|
897
968
|
const totalWeight = signals.reduce((s, el) => s + el.weight, 0);
|
|
898
969
|
if (totalWeight === 0) return domainCohesion;
|
|
899
|
-
const combined = signals.reduce(
|
|
970
|
+
const combined = signals.reduce(
|
|
971
|
+
(sum, el) => sum + el.score * (el.weight / totalWeight),
|
|
972
|
+
0
|
|
973
|
+
);
|
|
900
974
|
return combined;
|
|
901
975
|
}
|
|
902
976
|
function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
|
|
@@ -919,7 +993,9 @@ function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
|
|
|
919
993
|
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
920
994
|
}
|
|
921
995
|
function calculateImportBasedCohesion(exports2) {
|
|
922
|
-
const exportsWithImports = exports2.filter(
|
|
996
|
+
const exportsWithImports = exports2.filter(
|
|
997
|
+
(e) => e.imports && e.imports.length > 0
|
|
998
|
+
);
|
|
923
999
|
if (exportsWithImports.length < 2) {
|
|
924
1000
|
return 1;
|
|
925
1001
|
}
|
|
@@ -964,6 +1040,8 @@ function calculateDomainCohesion(exports2) {
|
|
|
964
1040
|
}
|
|
965
1041
|
function classifyFile(node, cohesionScore, domains) {
|
|
966
1042
|
const { exports: exports2, imports, linesOfCode, file } = node;
|
|
1043
|
+
void imports;
|
|
1044
|
+
void linesOfCode;
|
|
967
1045
|
if (isBarrelExport(node)) {
|
|
968
1046
|
return "barrel-export";
|
|
969
1047
|
}
|
|
@@ -1042,8 +1120,12 @@ function isTypeDefinitionFile(node) {
|
|
|
1042
1120
|
const isTypesFile = fileName?.includes("types") || fileName?.includes(".d.ts") || fileName === "types.ts" || fileName === "interfaces.ts";
|
|
1043
1121
|
const lowerPath = file.toLowerCase();
|
|
1044
1122
|
const isTypesPath = lowerPath.includes("/types/") || lowerPath.includes("/typings/") || lowerPath.includes("/@types/") || lowerPath.startsWith("types/") || lowerPath.startsWith("typings/");
|
|
1045
|
-
const typeExports = exports2.filter(
|
|
1046
|
-
|
|
1123
|
+
const typeExports = exports2.filter(
|
|
1124
|
+
(e) => e.type === "type" || e.type === "interface"
|
|
1125
|
+
);
|
|
1126
|
+
const runtimeExports = exports2.filter(
|
|
1127
|
+
(e) => e.type === "function" || e.type === "class" || e.type === "const"
|
|
1128
|
+
);
|
|
1047
1129
|
const mostlyTypes = exports2.length > 0 && typeExports.length > runtimeExports.length && typeExports.length / exports2.length > 0.7;
|
|
1048
1130
|
const pureTypeFile = exports2.length > 0 && typeExports.length === exports2.length;
|
|
1049
1131
|
const emptyOrReExportInTypesDir = isTypesPath && exports2.length === 0;
|
|
@@ -1224,12 +1306,7 @@ function isLambdaHandler(node) {
|
|
|
1224
1306
|
function isServiceFile(node) {
|
|
1225
1307
|
const { file, exports: exports2 } = node;
|
|
1226
1308
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1227
|
-
const servicePatterns = [
|
|
1228
|
-
"service",
|
|
1229
|
-
".service.",
|
|
1230
|
-
"-service.",
|
|
1231
|
-
"_service."
|
|
1232
|
-
];
|
|
1309
|
+
const servicePatterns = ["service", ".service.", "-service.", "_service."];
|
|
1233
1310
|
const isServiceName = servicePatterns.some(
|
|
1234
1311
|
(pattern) => fileName?.includes(pattern)
|
|
1235
1312
|
);
|
|
@@ -1337,7 +1414,15 @@ function isNextJsPage(node) {
|
|
|
1337
1414
|
}
|
|
1338
1415
|
const exportNames = exports2.map((e) => e.name.toLowerCase());
|
|
1339
1416
|
const hasDefaultExport = exports2.some((e) => e.type === "default");
|
|
1340
|
-
const nextJsExports = [
|
|
1417
|
+
const nextJsExports = [
|
|
1418
|
+
"metadata",
|
|
1419
|
+
"generatemetadata",
|
|
1420
|
+
"faqjsonld",
|
|
1421
|
+
"jsonld",
|
|
1422
|
+
"icon",
|
|
1423
|
+
"viewport",
|
|
1424
|
+
"dynamic"
|
|
1425
|
+
];
|
|
1341
1426
|
const hasNextJsExports = exportNames.some(
|
|
1342
1427
|
(name) => nextJsExports.includes(name) || name.includes("jsonld")
|
|
1343
1428
|
);
|
|
@@ -1411,13 +1496,44 @@ function hasRelatedExportNames(exportNames) {
|
|
|
1411
1496
|
const stems = /* @__PURE__ */ new Set();
|
|
1412
1497
|
const domains = /* @__PURE__ */ new Set();
|
|
1413
1498
|
for (const name of exportNames) {
|
|
1414
|
-
const verbs = [
|
|
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
|
+
];
|
|
1415
1519
|
for (const verb of verbs) {
|
|
1416
1520
|
if (name.startsWith(verb) && name.length > verb.length) {
|
|
1417
1521
|
stems.add(name.slice(verb.length).toLowerCase());
|
|
1418
1522
|
}
|
|
1419
1523
|
}
|
|
1420
|
-
const domainPatterns = [
|
|
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
|
+
];
|
|
1421
1537
|
for (const domain of domainPatterns) {
|
|
1422
1538
|
if (name.includes(domain)) {
|
|
1423
1539
|
domains.add(domain);
|
|
@@ -1475,11 +1591,15 @@ function hasRelatedExportNames(exportNames) {
|
|
|
1475
1591
|
"all"
|
|
1476
1592
|
]);
|
|
1477
1593
|
const singularize2 = (w) => w.endsWith("s") && w.length > 3 ? w.slice(0, -1) : w;
|
|
1478
|
-
return new Set(
|
|
1594
|
+
return new Set(
|
|
1595
|
+
tokens.filter((t) => !skip.has(t) && t.length > 2).map(singularize2)
|
|
1596
|
+
);
|
|
1479
1597
|
});
|
|
1480
1598
|
if (nounSets.length >= 2 && nounSets.every((s) => s.size > 0)) {
|
|
1481
1599
|
const [first, ...rest] = nounSets;
|
|
1482
|
-
const commonNouns = Array.from(first).filter(
|
|
1600
|
+
const commonNouns = Array.from(first).filter(
|
|
1601
|
+
(n) => rest.every((s) => s.has(n))
|
|
1602
|
+
);
|
|
1483
1603
|
if (commonNouns.length > 0) return true;
|
|
1484
1604
|
}
|
|
1485
1605
|
return false;
|
|
@@ -1624,7 +1744,10 @@ function calculateContextScore(summary, costConfig) {
|
|
|
1624
1744
|
}
|
|
1625
1745
|
const recommendations = [];
|
|
1626
1746
|
if (avgContextBudget > 1e4) {
|
|
1627
|
-
const estimatedImpact = Math.min(
|
|
1747
|
+
const estimatedImpact = Math.min(
|
|
1748
|
+
15,
|
|
1749
|
+
Math.round((avgContextBudget - 1e4) / 1e3)
|
|
1750
|
+
);
|
|
1628
1751
|
recommendations.push({
|
|
1629
1752
|
action: "Reduce file dependencies to lower context requirements",
|
|
1630
1753
|
estimatedImpact,
|
|
@@ -1640,7 +1763,10 @@ function calculateContextScore(summary, costConfig) {
|
|
|
1640
1763
|
});
|
|
1641
1764
|
}
|
|
1642
1765
|
if (avgFragmentation > 0.5) {
|
|
1643
|
-
const estimatedImpact = Math.min(
|
|
1766
|
+
const estimatedImpact = Math.min(
|
|
1767
|
+
12,
|
|
1768
|
+
Math.round((avgFragmentation - 0.5) * 40)
|
|
1769
|
+
);
|
|
1644
1770
|
recommendations.push({
|
|
1645
1771
|
action: "Consolidate related code into cohesive modules",
|
|
1646
1772
|
estimatedImpact,
|
|
@@ -1742,21 +1868,29 @@ async function analyzeContext(options) {
|
|
|
1742
1868
|
// Only add node_modules to exclude if includeNodeModules is false
|
|
1743
1869
|
// The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
|
|
1744
1870
|
// if user overrides the default exclude list
|
|
1745
|
-
exclude: includeNodeModules && scanOptions.exclude ? scanOptions.exclude.filter(
|
|
1871
|
+
exclude: includeNodeModules && scanOptions.exclude ? scanOptions.exclude.filter(
|
|
1872
|
+
(pattern) => pattern !== "**/node_modules/**"
|
|
1873
|
+
) : scanOptions.exclude
|
|
1746
1874
|
});
|
|
1747
1875
|
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
1748
1876
|
const tsJsFiles = files.filter((f) => !f.toLowerCase().endsWith(".py"));
|
|
1877
|
+
void tsJsFiles;
|
|
1749
1878
|
const fileContents = await Promise.all(
|
|
1750
1879
|
files.map(async (file) => ({
|
|
1751
1880
|
file,
|
|
1752
1881
|
content: await (0, import_core4.readFileContent)(file)
|
|
1753
1882
|
}))
|
|
1754
1883
|
);
|
|
1755
|
-
const graph = buildDependencyGraph(
|
|
1884
|
+
const graph = buildDependencyGraph(
|
|
1885
|
+
fileContents.filter((f) => !f.file.toLowerCase().endsWith(".py"))
|
|
1886
|
+
);
|
|
1756
1887
|
let pythonResults = [];
|
|
1757
1888
|
if (pythonFiles.length > 0) {
|
|
1758
1889
|
const { analyzePythonContext: analyzePythonContext2 } = await Promise.resolve().then(() => (init_python_context(), python_context_exports));
|
|
1759
|
-
const pythonMetrics = await analyzePythonContext2(
|
|
1890
|
+
const pythonMetrics = await analyzePythonContext2(
|
|
1891
|
+
pythonFiles,
|
|
1892
|
+
scanOptions.rootDir || options.rootDir || "."
|
|
1893
|
+
);
|
|
1760
1894
|
pythonResults = pythonMetrics.map((metric) => {
|
|
1761
1895
|
const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
|
|
1762
1896
|
file: metric.file,
|
|
@@ -1769,17 +1903,25 @@ async function analyzeContext(options) {
|
|
|
1769
1903
|
maxContextBudget,
|
|
1770
1904
|
minCohesion,
|
|
1771
1905
|
maxFragmentation,
|
|
1772
|
-
circularDeps: metric.metrics.circularDependencies.map(
|
|
1906
|
+
circularDeps: metric.metrics.circularDependencies.map(
|
|
1907
|
+
(cycle) => cycle.split(" \u2192 ")
|
|
1908
|
+
)
|
|
1773
1909
|
});
|
|
1774
1910
|
return {
|
|
1775
1911
|
file: metric.file,
|
|
1776
|
-
tokenCost: Math.floor(
|
|
1912
|
+
tokenCost: Math.floor(
|
|
1913
|
+
metric.contextBudget / (1 + metric.imports.length || 1)
|
|
1914
|
+
),
|
|
1777
1915
|
// Estimate
|
|
1778
1916
|
linesOfCode: metric.metrics.linesOfCode,
|
|
1779
1917
|
importDepth: metric.importDepth,
|
|
1780
1918
|
dependencyCount: metric.imports.length,
|
|
1781
|
-
dependencyList: metric.imports.map(
|
|
1782
|
-
|
|
1919
|
+
dependencyList: metric.imports.map(
|
|
1920
|
+
(imp) => imp.resolvedPath || imp.source
|
|
1921
|
+
),
|
|
1922
|
+
circularDeps: metric.metrics.circularDependencies.map(
|
|
1923
|
+
(cycle) => cycle.split(" \u2192 ")
|
|
1924
|
+
),
|
|
1783
1925
|
cohesionScore: metric.cohesion,
|
|
1784
1926
|
domains: ["python"],
|
|
1785
1927
|
// Generic for now
|
|
@@ -1812,7 +1954,9 @@ async function analyzeContext(options) {
|
|
|
1812
1954
|
const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
|
|
1813
1955
|
const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
|
|
1814
1956
|
const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
1815
|
-
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file, {
|
|
1957
|
+
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file, {
|
|
1958
|
+
coUsageMatrix: graph.coUsageMatrix
|
|
1959
|
+
}) : 1;
|
|
1816
1960
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
1817
1961
|
const relatedFiles = [];
|
|
1818
1962
|
for (const cluster of clusters) {
|
|
@@ -1833,6 +1977,10 @@ async function analyzeContext(options) {
|
|
|
1833
1977
|
maxFragmentation,
|
|
1834
1978
|
circularDeps
|
|
1835
1979
|
});
|
|
1980
|
+
void severity;
|
|
1981
|
+
void issues;
|
|
1982
|
+
void recommendations;
|
|
1983
|
+
void potentialSavings;
|
|
1836
1984
|
const domains = [
|
|
1837
1985
|
...new Set(node.exports.map((e) => e.inferredDomain || "unknown"))
|
|
1838
1986
|
];
|
|
@@ -1887,7 +2035,10 @@ async function analyzeContext(options) {
|
|
|
1887
2035
|
fileClassification,
|
|
1888
2036
|
severity: adjustedSeverity,
|
|
1889
2037
|
issues: adjustedIssues,
|
|
1890
|
-
recommendations: [
|
|
2038
|
+
recommendations: [
|
|
2039
|
+
...finalRecommendations,
|
|
2040
|
+
...classificationRecommendations.slice(0, 1)
|
|
2041
|
+
],
|
|
1891
2042
|
potentialSavings: adjustedSavings
|
|
1892
2043
|
});
|
|
1893
2044
|
}
|
|
@@ -1966,7 +2117,10 @@ function generateSummary(results) {
|
|
|
1966
2117
|
let importPairs = 0;
|
|
1967
2118
|
for (let i = 0; i < files.length; i++) {
|
|
1968
2119
|
for (let j = i + 1; j < files.length; j++) {
|
|
1969
|
-
importSimTotal += jaccard2(
|
|
2120
|
+
importSimTotal += jaccard2(
|
|
2121
|
+
files[i].dependencyList || [],
|
|
2122
|
+
files[j].dependencyList || []
|
|
2123
|
+
);
|
|
1970
2124
|
importPairs++;
|
|
1971
2125
|
}
|
|
1972
2126
|
}
|
|
@@ -1993,7 +2147,9 @@ function generateSummary(results) {
|
|
|
1993
2147
|
fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
1994
2148
|
const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
|
|
1995
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);
|
|
1996
|
-
const criticalIssues = results.filter(
|
|
2150
|
+
const criticalIssues = results.filter(
|
|
2151
|
+
(r) => r.severity === "critical"
|
|
2152
|
+
).length;
|
|
1997
2153
|
const majorIssues = results.filter((r) => r.severity === "major").length;
|
|
1998
2154
|
const minorIssues = results.filter((r) => r.severity === "minor").length;
|
|
1999
2155
|
const totalPotentialSavings = results.reduce(
|
|
@@ -2043,10 +2199,10 @@ function analyzeIssues(params) {
|
|
|
2043
2199
|
let potentialSavings = 0;
|
|
2044
2200
|
if (circularDeps.length > 0) {
|
|
2045
2201
|
severity = "critical";
|
|
2046
|
-
issues.push(
|
|
2047
|
-
|
|
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"
|
|
2048
2205
|
);
|
|
2049
|
-
recommendations.push("Break circular dependencies by extracting interfaces or using dependency injection");
|
|
2050
2206
|
potentialSavings += contextBudget * 0.2;
|
|
2051
2207
|
}
|
|
2052
2208
|
if (importDepth > maxDepth * 1.5) {
|
|
@@ -2056,25 +2212,37 @@ function analyzeIssues(params) {
|
|
|
2056
2212
|
potentialSavings += contextBudget * 0.3;
|
|
2057
2213
|
} else if (importDepth > maxDepth) {
|
|
2058
2214
|
severity = severity === "critical" ? "critical" : "major";
|
|
2059
|
-
issues.push(
|
|
2215
|
+
issues.push(
|
|
2216
|
+
`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
|
|
2217
|
+
);
|
|
2060
2218
|
recommendations.push("Consider reducing dependency depth");
|
|
2061
2219
|
potentialSavings += contextBudget * 0.15;
|
|
2062
2220
|
}
|
|
2063
2221
|
if (contextBudget > maxContextBudget * 1.5) {
|
|
2064
2222
|
severity = severity === "critical" ? "critical" : "critical";
|
|
2065
|
-
issues.push(
|
|
2066
|
-
|
|
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
|
+
);
|
|
2067
2229
|
potentialSavings += contextBudget * 0.4;
|
|
2068
2230
|
} else if (contextBudget > maxContextBudget) {
|
|
2069
2231
|
severity = severity === "critical" || severity === "major" ? severity : "major";
|
|
2070
|
-
issues.push(
|
|
2232
|
+
issues.push(
|
|
2233
|
+
`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
2234
|
+
);
|
|
2071
2235
|
recommendations.push("Reduce file size or dependencies");
|
|
2072
2236
|
potentialSavings += contextBudget * 0.2;
|
|
2073
2237
|
}
|
|
2074
2238
|
if (cohesionScore < minCohesion * 0.5) {
|
|
2075
2239
|
severity = severity === "critical" ? "critical" : "major";
|
|
2076
|
-
issues.push(
|
|
2077
|
-
|
|
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
|
+
);
|
|
2078
2246
|
potentialSavings += contextBudget * 0.25;
|
|
2079
2247
|
} else if (cohesionScore < minCohesion) {
|
|
2080
2248
|
severity = severity === "critical" || severity === "major" ? severity : "minor";
|
|
@@ -2084,7 +2252,9 @@ function analyzeIssues(params) {
|
|
|
2084
2252
|
}
|
|
2085
2253
|
if (fragmentationScore > maxFragmentation) {
|
|
2086
2254
|
severity = severity === "critical" || severity === "major" ? severity : "minor";
|
|
2087
|
-
issues.push(
|
|
2255
|
+
issues.push(
|
|
2256
|
+
`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
|
|
2257
|
+
);
|
|
2088
2258
|
recommendations.push("Consolidate with related files in same domain");
|
|
2089
2259
|
potentialSavings += contextBudget * 0.3;
|
|
2090
2260
|
}
|
|
@@ -2094,11 +2264,18 @@ function analyzeIssues(params) {
|
|
|
2094
2264
|
}
|
|
2095
2265
|
if (isBuildArtifact(file)) {
|
|
2096
2266
|
issues.push("Detected build artifact (bundled/output file)");
|
|
2097
|
-
recommendations.push(
|
|
2267
|
+
recommendations.push(
|
|
2268
|
+
"Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis"
|
|
2269
|
+
);
|
|
2098
2270
|
severity = downgradeSeverity(severity);
|
|
2099
2271
|
potentialSavings = 0;
|
|
2100
2272
|
}
|
|
2101
|
-
return {
|
|
2273
|
+
return {
|
|
2274
|
+
severity,
|
|
2275
|
+
issues,
|
|
2276
|
+
recommendations,
|
|
2277
|
+
potentialSavings: Math.floor(potentialSavings)
|
|
2278
|
+
};
|
|
2102
2279
|
}
|
|
2103
2280
|
function isBuildArtifact(filePath) {
|
|
2104
2281
|
const lower = filePath.toLowerCase();
|