@aiready/context-analyzer 0.21.10 → 0.21.12
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 +28 -28
- package/dist/chunk-BW463GQB.mjs +1767 -0
- package/dist/chunk-CAX2MOUZ.mjs +1801 -0
- package/dist/chunk-J5TA3AZU.mjs +1795 -0
- package/dist/chunk-UXC6QUZ7.mjs +1801 -0
- package/dist/chunk-WTQJNY4U.mjs +1785 -0
- package/dist/chunk-XBFM2Z4O.mjs +1792 -0
- package/dist/cli.js +282 -220
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +3 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +286 -224
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/cluster-detector.test.ts +5 -8
- package/src/__tests__/fragmentation-coupling.test.ts +6 -3
- package/src/analyzer.ts +1 -224
- package/src/classifier.ts +11 -0
- package/src/cluster-detector.ts +22 -5
- package/src/heuristics.ts +150 -81
- package/src/mapper.ts +118 -0
- package/src/metrics.ts +13 -12
- package/src/orchestrator.ts +136 -0
- package/src/remediation.ts +5 -0
- package/src/types.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -336,7 +336,7 @@ var import_core11 = require("@aiready/core");
|
|
|
336
336
|
// src/provider.ts
|
|
337
337
|
var import_core9 = require("@aiready/core");
|
|
338
338
|
|
|
339
|
-
// src/
|
|
339
|
+
// src/orchestrator.ts
|
|
340
340
|
var import_core6 = require("@aiready/core");
|
|
341
341
|
|
|
342
342
|
// src/metrics.ts
|
|
@@ -649,16 +649,17 @@ function calculateEnhancedCohesion(exports2, filePath, options) {
|
|
|
649
649
|
if (filePath && isTestFile(filePath)) return 1;
|
|
650
650
|
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
651
651
|
const domainCounts = /* @__PURE__ */ new Map();
|
|
652
|
-
for (const
|
|
652
|
+
for (const domain of domains)
|
|
653
|
+
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
653
654
|
if (domainCounts.size === 1 && domains[0] !== "unknown") {
|
|
654
655
|
if (!options?.weights) return 1;
|
|
655
656
|
}
|
|
656
657
|
const probs = Array.from(domainCounts.values()).map(
|
|
657
|
-
(
|
|
658
|
+
(count) => count / exports2.length
|
|
658
659
|
);
|
|
659
660
|
let domainEntropy = 0;
|
|
660
|
-
for (const
|
|
661
|
-
if (
|
|
661
|
+
for (const prob of probs) {
|
|
662
|
+
if (prob > 0) domainEntropy -= prob * Math.log2(prob);
|
|
662
663
|
}
|
|
663
664
|
const maxEntropy = Math.log2(Math.max(2, domainCounts.size));
|
|
664
665
|
const domainScore = 1 - domainEntropy / maxEntropy;
|
|
@@ -719,7 +720,7 @@ function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
|
|
|
719
720
|
function calculateFragmentation(files, domain, options) {
|
|
720
721
|
if (files.length <= 1) return 0;
|
|
721
722
|
const directories = new Set(
|
|
722
|
-
files.map((
|
|
723
|
+
files.map((file) => file.split("/").slice(0, -1).join("/"))
|
|
723
724
|
);
|
|
724
725
|
const uniqueDirs = directories.size;
|
|
725
726
|
let score = options?.useLogScale ? uniqueDirs <= 1 ? 0 : Math.log(uniqueDirs) / Math.log(options.logBase || Math.E) / (Math.log(files.length) / Math.log(options.logBase || Math.E)) : (uniqueDirs - 1) / (files.length - 1);
|
|
@@ -732,13 +733,13 @@ function calculateFragmentation(files, domain, options) {
|
|
|
732
733
|
function calculatePathEntropy(files) {
|
|
733
734
|
if (!files || files.length === 0) return 0;
|
|
734
735
|
const dirCounts = /* @__PURE__ */ new Map();
|
|
735
|
-
for (const
|
|
736
|
-
const dir =
|
|
736
|
+
for (const file of files) {
|
|
737
|
+
const dir = file.split("/").slice(0, -1).join("/") || ".";
|
|
737
738
|
dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
|
|
738
739
|
}
|
|
739
740
|
const counts = Array.from(dirCounts.values());
|
|
740
741
|
if (counts.length <= 1) return 0;
|
|
741
|
-
const total = counts.reduce((
|
|
742
|
+
const total = counts.reduce((sum, value) => sum + value, 0);
|
|
742
743
|
let entropy = 0;
|
|
743
744
|
for (const count of counts) {
|
|
744
745
|
const prob = count / total;
|
|
@@ -749,11 +750,11 @@ function calculatePathEntropy(files) {
|
|
|
749
750
|
}
|
|
750
751
|
function calculateDirectoryDistance(files) {
|
|
751
752
|
if (!files || files.length <= 1) return 0;
|
|
752
|
-
const pathSegments = (
|
|
753
|
-
const commonAncestorDepth = (
|
|
754
|
-
const minLen = Math.min(
|
|
753
|
+
const pathSegments = (pathStr) => pathStr.split("/").filter(Boolean);
|
|
754
|
+
const commonAncestorDepth = (pathA, pathB) => {
|
|
755
|
+
const minLen = Math.min(pathA.length, pathB.length);
|
|
755
756
|
let i = 0;
|
|
756
|
-
while (i < minLen &&
|
|
757
|
+
while (i < minLen && pathA[i] === pathB[i]) i++;
|
|
757
758
|
return i;
|
|
758
759
|
};
|
|
759
760
|
let totalNormalized = 0;
|
|
@@ -1002,86 +1003,67 @@ function detectCircularDependencies(graph) {
|
|
|
1002
1003
|
return detectGraphCycles(graph.edges);
|
|
1003
1004
|
}
|
|
1004
1005
|
|
|
1005
|
-
// src/cluster-detector.ts
|
|
1006
|
-
function detectModuleClusters(graph, options) {
|
|
1007
|
-
const domainMap = /* @__PURE__ */ new Map();
|
|
1008
|
-
for (const [file, node] of graph.nodes.entries()) {
|
|
1009
|
-
const primaryDomain = node.exports[0]?.inferredDomain || "unknown";
|
|
1010
|
-
if (!domainMap.has(primaryDomain)) {
|
|
1011
|
-
domainMap.set(primaryDomain, []);
|
|
1012
|
-
}
|
|
1013
|
-
domainMap.get(primaryDomain).push(file);
|
|
1014
|
-
}
|
|
1015
|
-
const clusters = [];
|
|
1016
|
-
const generateSuggestedStructure = (files, tokens, fragmentation) => {
|
|
1017
|
-
const targetFiles = Math.max(1, Math.ceil(tokens / 1e4));
|
|
1018
|
-
const plan = [];
|
|
1019
|
-
if (fragmentation > 0.5) {
|
|
1020
|
-
plan.push(
|
|
1021
|
-
`Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
|
|
1022
|
-
);
|
|
1023
|
-
}
|
|
1024
|
-
if (tokens > 2e4) {
|
|
1025
|
-
plan.push(
|
|
1026
|
-
`Domain logic is very large (${Math.round(tokens / 1e3)}k tokens). Ensure clear sub-domain boundaries.`
|
|
1027
|
-
);
|
|
1028
|
-
}
|
|
1029
|
-
return { targetFiles, consolidationPlan: plan };
|
|
1030
|
-
};
|
|
1031
|
-
for (const [domain, files] of domainMap.entries()) {
|
|
1032
|
-
if (files.length < 2 || domain === "unknown") continue;
|
|
1033
|
-
const totalTokens = files.reduce((sum, file) => {
|
|
1034
|
-
const node = graph.nodes.get(file);
|
|
1035
|
-
return sum + (node?.tokenCost || 0);
|
|
1036
|
-
}, 0);
|
|
1037
|
-
let sharedImportRatio = 0;
|
|
1038
|
-
if (files.length >= 2) {
|
|
1039
|
-
const allImportSets = files.map(
|
|
1040
|
-
(f) => new Set(graph.nodes.get(f)?.imports || [])
|
|
1041
|
-
);
|
|
1042
|
-
let intersection = new Set(allImportSets[0]);
|
|
1043
|
-
const union = new Set(allImportSets[0]);
|
|
1044
|
-
for (let i = 1; i < allImportSets.length; i++) {
|
|
1045
|
-
const nextSet = allImportSets[i];
|
|
1046
|
-
intersection = new Set([...intersection].filter((x) => nextSet.has(x)));
|
|
1047
|
-
for (const x of nextSet) union.add(x);
|
|
1048
|
-
}
|
|
1049
|
-
sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
|
|
1050
|
-
}
|
|
1051
|
-
const fragmentation = calculateFragmentation(files, domain, {
|
|
1052
|
-
...options,
|
|
1053
|
-
sharedImportRatio
|
|
1054
|
-
});
|
|
1055
|
-
let totalCohesion = 0;
|
|
1056
|
-
files.forEach((f) => {
|
|
1057
|
-
const node = graph.nodes.get(f);
|
|
1058
|
-
if (node) totalCohesion += calculateEnhancedCohesion(node.exports);
|
|
1059
|
-
});
|
|
1060
|
-
const avgCohesion = totalCohesion / files.length;
|
|
1061
|
-
clusters.push({
|
|
1062
|
-
domain,
|
|
1063
|
-
files,
|
|
1064
|
-
totalTokens,
|
|
1065
|
-
fragmentationScore: fragmentation,
|
|
1066
|
-
avgCohesion,
|
|
1067
|
-
suggestedStructure: generateSuggestedStructure(
|
|
1068
|
-
files,
|
|
1069
|
-
totalTokens,
|
|
1070
|
-
fragmentation
|
|
1071
|
-
)
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
return clusters;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
1006
|
// src/heuristics.ts
|
|
1007
|
+
var BARREL_EXPORT_MIN_EXPORTS = 5;
|
|
1008
|
+
var BARREL_EXPORT_TOKEN_LIMIT = 1e3;
|
|
1009
|
+
var HANDLER_NAME_PATTERNS = [
|
|
1010
|
+
"handler",
|
|
1011
|
+
".handler.",
|
|
1012
|
+
"-handler.",
|
|
1013
|
+
"lambda",
|
|
1014
|
+
".lambda.",
|
|
1015
|
+
"-lambda."
|
|
1016
|
+
];
|
|
1017
|
+
var SERVICE_NAME_PATTERNS = [
|
|
1018
|
+
"service",
|
|
1019
|
+
".service.",
|
|
1020
|
+
"-service.",
|
|
1021
|
+
"_service."
|
|
1022
|
+
];
|
|
1023
|
+
var EMAIL_NAME_PATTERNS = [
|
|
1024
|
+
"-email-",
|
|
1025
|
+
".email.",
|
|
1026
|
+
"_email_",
|
|
1027
|
+
"-template",
|
|
1028
|
+
".template.",
|
|
1029
|
+
"_template",
|
|
1030
|
+
"-mail.",
|
|
1031
|
+
".mail."
|
|
1032
|
+
];
|
|
1033
|
+
var PARSER_NAME_PATTERNS = [
|
|
1034
|
+
"parser",
|
|
1035
|
+
".parser.",
|
|
1036
|
+
"-parser.",
|
|
1037
|
+
"_parser.",
|
|
1038
|
+
"transform",
|
|
1039
|
+
"converter",
|
|
1040
|
+
"mapper",
|
|
1041
|
+
"serializer"
|
|
1042
|
+
];
|
|
1043
|
+
var SESSION_NAME_PATTERNS = ["session", "state", "context", "store"];
|
|
1044
|
+
var NEXTJS_METADATA_EXPORTS = [
|
|
1045
|
+
"metadata",
|
|
1046
|
+
"generatemetadata",
|
|
1047
|
+
"faqjsonld",
|
|
1048
|
+
"jsonld",
|
|
1049
|
+
"icon"
|
|
1050
|
+
];
|
|
1051
|
+
var CONFIG_NAME_PATTERNS = [
|
|
1052
|
+
".config.",
|
|
1053
|
+
"tsconfig",
|
|
1054
|
+
"jest.config",
|
|
1055
|
+
"package.json",
|
|
1056
|
+
"aiready.json",
|
|
1057
|
+
"next.config",
|
|
1058
|
+
"sst.config"
|
|
1059
|
+
];
|
|
1078
1060
|
function isBarrelExport(node) {
|
|
1079
1061
|
const { file, exports: exports2 } = node;
|
|
1080
1062
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1081
1063
|
const isIndexFile = fileName === "index.ts" || fileName === "index.js";
|
|
1082
|
-
const isSmallAndManyExports = node.tokenCost <
|
|
1083
|
-
const isReexportPattern = (exports2 || []).length >=
|
|
1084
|
-
(
|
|
1064
|
+
const isSmallAndManyExports = node.tokenCost < BARREL_EXPORT_TOKEN_LIMIT && (exports2 || []).length > BARREL_EXPORT_MIN_EXPORTS;
|
|
1065
|
+
const isReexportPattern = (exports2 || []).length >= BARREL_EXPORT_MIN_EXPORTS && (exports2 || []).every(
|
|
1066
|
+
(exp) => ["const", "function", "type", "interface"].includes(exp.type)
|
|
1085
1067
|
);
|
|
1086
1068
|
return !!isIndexFile || !!isSmallAndManyExports || !!isReexportPattern;
|
|
1087
1069
|
}
|
|
@@ -1090,7 +1072,9 @@ function isTypeDefinition(node) {
|
|
|
1090
1072
|
if (file.endsWith(".d.ts")) return true;
|
|
1091
1073
|
const nodeExports = node.exports || [];
|
|
1092
1074
|
const hasExports = nodeExports.length > 0;
|
|
1093
|
-
const areAllTypes = hasExports && nodeExports.every(
|
|
1075
|
+
const areAllTypes = hasExports && nodeExports.every(
|
|
1076
|
+
(exp) => exp.type === "type" || exp.type === "interface"
|
|
1077
|
+
);
|
|
1094
1078
|
const isTypePath = /\/(types|interfaces|models)\//i.test(file);
|
|
1095
1079
|
return !!areAllTypes || isTypePath && hasExports;
|
|
1096
1080
|
}
|
|
@@ -1104,81 +1088,65 @@ function isUtilityModule(node) {
|
|
|
1104
1088
|
function isLambdaHandler(node) {
|
|
1105
1089
|
const { file, exports: exports2 } = node;
|
|
1106
1090
|
const fileName = file.split("/").pop()?.toLowerCase() || "";
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
"-handler.",
|
|
1111
|
-
"lambda",
|
|
1112
|
-
".lambda.",
|
|
1113
|
-
"-lambda."
|
|
1114
|
-
];
|
|
1115
|
-
const isHandlerName = handlerPatterns.some((p) => fileName.includes(p));
|
|
1091
|
+
const isHandlerName = HANDLER_NAME_PATTERNS.some(
|
|
1092
|
+
(pattern) => fileName.includes(pattern)
|
|
1093
|
+
);
|
|
1116
1094
|
const isHandlerPath = /\/(handlers|lambdas|lambda|functions)\//i.test(file);
|
|
1117
1095
|
const hasHandlerExport = (exports2 || []).some(
|
|
1118
|
-
(
|
|
1096
|
+
(exp) => ["handler", "main", "lambdahandler"].includes(exp.name.toLowerCase()) || exp.name.toLowerCase().endsWith("handler")
|
|
1119
1097
|
);
|
|
1120
1098
|
return isHandlerName || isHandlerPath || hasHandlerExport;
|
|
1121
1099
|
}
|
|
1122
1100
|
function isServiceFile(node) {
|
|
1123
1101
|
const { file, exports: exports2 } = node;
|
|
1124
1102
|
const fileName = file.split("/").pop()?.toLowerCase() || "";
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1103
|
+
const isServiceName = SERVICE_NAME_PATTERNS.some(
|
|
1104
|
+
(pattern) => fileName.includes(pattern)
|
|
1105
|
+
);
|
|
1127
1106
|
const isServicePath = file.toLowerCase().includes("/services/");
|
|
1128
1107
|
const hasServiceNamedExport = (exports2 || []).some(
|
|
1129
|
-
(
|
|
1108
|
+
(exp) => exp.name.toLowerCase().includes("service")
|
|
1109
|
+
);
|
|
1110
|
+
const hasClassExport = (exports2 || []).some(
|
|
1111
|
+
(exp) => exp.type === "class"
|
|
1130
1112
|
);
|
|
1131
|
-
const hasClassExport = (exports2 || []).some((e) => e.type === "class");
|
|
1132
1113
|
return isServiceName || isServicePath || hasServiceNamedExport && hasClassExport;
|
|
1133
1114
|
}
|
|
1134
1115
|
function isEmailTemplate(node) {
|
|
1135
1116
|
const { file, exports: exports2 } = node;
|
|
1136
1117
|
const fileName = file.split("/").pop()?.toLowerCase() || "";
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
"_email_",
|
|
1141
|
-
"-template",
|
|
1142
|
-
".template.",
|
|
1143
|
-
"_template",
|
|
1144
|
-
"-mail.",
|
|
1145
|
-
".mail."
|
|
1146
|
-
];
|
|
1147
|
-
const isEmailName = emailPatterns.some((p) => fileName.includes(p));
|
|
1118
|
+
const isEmailName = EMAIL_NAME_PATTERNS.some(
|
|
1119
|
+
(pattern) => fileName.includes(pattern)
|
|
1120
|
+
);
|
|
1148
1121
|
const isEmailPath = /\/(emails|mail|notifications)\//i.test(file);
|
|
1149
1122
|
const hasTemplateFunction = (exports2 || []).some(
|
|
1150
|
-
(
|
|
1123
|
+
(exp) => exp.type === "function" && (exp.name.toLowerCase().startsWith("render") || exp.name.toLowerCase().startsWith("generate"))
|
|
1151
1124
|
);
|
|
1152
1125
|
return isEmailPath || isEmailName || hasTemplateFunction;
|
|
1153
1126
|
}
|
|
1154
1127
|
function isParserFile(node) {
|
|
1155
1128
|
const { file, exports: exports2 } = node;
|
|
1156
1129
|
const fileName = file.split("/").pop()?.toLowerCase() || "";
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
"-parser.",
|
|
1161
|
-
"_parser.",
|
|
1162
|
-
"transform",
|
|
1163
|
-
"converter",
|
|
1164
|
-
"mapper",
|
|
1165
|
-
"serializer"
|
|
1166
|
-
];
|
|
1167
|
-
const isParserName = parserPatterns.some((p) => fileName.includes(p));
|
|
1130
|
+
const isParserName = PARSER_NAME_PATTERNS.some(
|
|
1131
|
+
(pattern) => fileName.includes(pattern)
|
|
1132
|
+
);
|
|
1168
1133
|
const isParserPath = /\/(parsers|transformers)\//i.test(file);
|
|
1169
1134
|
const hasParseFunction = (exports2 || []).some(
|
|
1170
|
-
(
|
|
1135
|
+
(exp) => exp.type === "function" && (exp.name.toLowerCase().startsWith("parse") || exp.name.toLowerCase().startsWith("transform"))
|
|
1171
1136
|
);
|
|
1172
1137
|
return isParserName || isParserPath || hasParseFunction;
|
|
1173
1138
|
}
|
|
1174
1139
|
function isSessionFile(node) {
|
|
1175
1140
|
const { file, exports: exports2 } = node;
|
|
1176
1141
|
const fileName = file.split("/").pop()?.toLowerCase() || "";
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1142
|
+
const isSessionName = SESSION_NAME_PATTERNS.some(
|
|
1143
|
+
(pattern) => fileName.includes(pattern)
|
|
1144
|
+
);
|
|
1179
1145
|
const isSessionPath = /\/(sessions|state)\//i.test(file);
|
|
1180
1146
|
const hasSessionExport = (exports2 || []).some(
|
|
1181
|
-
(
|
|
1147
|
+
(exp) => ["session", "state", "store"].some(
|
|
1148
|
+
(pattern) => exp.name.toLowerCase().includes(pattern)
|
|
1149
|
+
)
|
|
1182
1150
|
);
|
|
1183
1151
|
return isSessionName || isSessionPath || hasSessionExport;
|
|
1184
1152
|
}
|
|
@@ -1189,16 +1157,11 @@ function isNextJsPage(node) {
|
|
|
1189
1157
|
const isInAppDir = lowerPath.includes("/app/") || lowerPath.startsWith("app/");
|
|
1190
1158
|
if (!isInAppDir || fileName !== "page.tsx" && fileName !== "page.ts")
|
|
1191
1159
|
return false;
|
|
1192
|
-
const hasDefaultExport = (exports2 || []).some(
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
"generatemetadata",
|
|
1196
|
-
"faqjsonld",
|
|
1197
|
-
"jsonld",
|
|
1198
|
-
"icon"
|
|
1199
|
-
];
|
|
1160
|
+
const hasDefaultExport = (exports2 || []).some(
|
|
1161
|
+
(exp) => exp.type === "default"
|
|
1162
|
+
);
|
|
1200
1163
|
const hasNextJsExport = (exports2 || []).some(
|
|
1201
|
-
(
|
|
1164
|
+
(exp) => NEXTJS_METADATA_EXPORTS.includes(exp.name.toLowerCase())
|
|
1202
1165
|
);
|
|
1203
1166
|
return hasDefaultExport || hasNextJsExport;
|
|
1204
1167
|
}
|
|
@@ -1206,24 +1169,21 @@ function isConfigFile(node) {
|
|
|
1206
1169
|
const { file, exports: exports2 } = node;
|
|
1207
1170
|
const lowerPath = file.toLowerCase();
|
|
1208
1171
|
const fileName = file.split("/").pop()?.toLowerCase() || "";
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
"jest.config",
|
|
1213
|
-
"package.json",
|
|
1214
|
-
"aiready.json",
|
|
1215
|
-
"next.config",
|
|
1216
|
-
"sst.config"
|
|
1217
|
-
];
|
|
1218
|
-
const isConfigName = configPatterns.some((p) => fileName.includes(p));
|
|
1172
|
+
const isConfigName = CONFIG_NAME_PATTERNS.some(
|
|
1173
|
+
(pattern) => fileName.includes(pattern)
|
|
1174
|
+
);
|
|
1219
1175
|
const isConfigPath = /\/(config|settings|schemas)\//i.test(lowerPath);
|
|
1220
1176
|
const hasSchemaExport = (exports2 || []).some(
|
|
1221
|
-
(
|
|
1222
|
-
(
|
|
1177
|
+
(exp) => ["schema", "config", "setting"].some(
|
|
1178
|
+
(pattern) => exp.name.toLowerCase().includes(pattern)
|
|
1223
1179
|
)
|
|
1224
1180
|
);
|
|
1225
1181
|
return isConfigName || isConfigPath || hasSchemaExport;
|
|
1226
1182
|
}
|
|
1183
|
+
function isHubAndSpokeFile(node) {
|
|
1184
|
+
const { file } = node;
|
|
1185
|
+
return /\/packages\/[a-zA-Z0-9-]+\/src\//.test(file);
|
|
1186
|
+
}
|
|
1227
1187
|
|
|
1228
1188
|
// src/classifier.ts
|
|
1229
1189
|
var Classification = {
|
|
@@ -1236,6 +1196,7 @@ var Classification = {
|
|
|
1236
1196
|
PARSER: "parser-file",
|
|
1237
1197
|
COHESIVE_MODULE: "cohesive-module",
|
|
1238
1198
|
UTILITY_MODULE: "utility-module",
|
|
1199
|
+
SPOKE_MODULE: "spoke-module",
|
|
1239
1200
|
MIXED_CONCERNS: "mixed-concerns",
|
|
1240
1201
|
UNKNOWN: "unknown"
|
|
1241
1202
|
};
|
|
@@ -1272,6 +1233,9 @@ function classifyFile(node, cohesionScore = 1, domains = []) {
|
|
|
1272
1233
|
if (isConfigFile(node)) {
|
|
1273
1234
|
return Classification.COHESIVE_MODULE;
|
|
1274
1235
|
}
|
|
1236
|
+
if (isHubAndSpokeFile(node)) {
|
|
1237
|
+
return Classification.SPOKE_MODULE;
|
|
1238
|
+
}
|
|
1275
1239
|
if (domains.length <= 1 && domains[0] !== "unknown") {
|
|
1276
1240
|
return Classification.COHESIVE_MODULE;
|
|
1277
1241
|
}
|
|
@@ -1307,6 +1271,8 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
|
|
|
1307
1271
|
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1308
1272
|
case Classification.PARSER:
|
|
1309
1273
|
return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
|
|
1274
|
+
case Classification.SPOKE_MODULE:
|
|
1275
|
+
return Math.max(baseCohesion, 0.6);
|
|
1310
1276
|
case Classification.COHESIVE_MODULE:
|
|
1311
1277
|
return Math.max(baseCohesion, 0.7);
|
|
1312
1278
|
case Classification.MIXED_CONCERNS:
|
|
@@ -1369,6 +1335,9 @@ function adjustFragmentationForClassification(baseFragmentation, classification)
|
|
|
1369
1335
|
case Classification.PARSER:
|
|
1370
1336
|
case Classification.NEXTJS_PAGE:
|
|
1371
1337
|
return baseFragmentation * 0.2;
|
|
1338
|
+
case Classification.SPOKE_MODULE:
|
|
1339
|
+
return baseFragmentation * 0.15;
|
|
1340
|
+
// Heavily discount intentional monorepo separation
|
|
1372
1341
|
case Classification.COHESIVE_MODULE:
|
|
1373
1342
|
return baseFragmentation * 0.3;
|
|
1374
1343
|
case Classification.MIXED_CONCERNS:
|
|
@@ -1378,6 +1347,88 @@ function adjustFragmentationForClassification(baseFragmentation, classification)
|
|
|
1378
1347
|
}
|
|
1379
1348
|
}
|
|
1380
1349
|
|
|
1350
|
+
// src/cluster-detector.ts
|
|
1351
|
+
function detectModuleClusters(graph, options) {
|
|
1352
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
1353
|
+
for (const [file, node] of graph.nodes.entries()) {
|
|
1354
|
+
const primaryDomain = node.exports[0]?.inferredDomain || "unknown";
|
|
1355
|
+
if (!domainMap.has(primaryDomain)) {
|
|
1356
|
+
domainMap.set(primaryDomain, []);
|
|
1357
|
+
}
|
|
1358
|
+
domainMap.get(primaryDomain).push(file);
|
|
1359
|
+
}
|
|
1360
|
+
const clusters = [];
|
|
1361
|
+
const generateSuggestedStructure = (files, tokens, fragmentation) => {
|
|
1362
|
+
const targetFiles = Math.max(1, Math.ceil(tokens / 1e4));
|
|
1363
|
+
const plan = [];
|
|
1364
|
+
if (fragmentation > 0.5) {
|
|
1365
|
+
plan.push(
|
|
1366
|
+
`Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
if (tokens > 2e4) {
|
|
1370
|
+
plan.push(
|
|
1371
|
+
`Domain logic is very large (${Math.round(tokens / 1e3)}k tokens). Ensure clear sub-domain boundaries.`
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
return { targetFiles, consolidationPlan: plan };
|
|
1375
|
+
};
|
|
1376
|
+
for (const [domain, files] of domainMap.entries()) {
|
|
1377
|
+
if (files.length < 2 || domain === "unknown") continue;
|
|
1378
|
+
const totalTokens = files.reduce((sum, file) => {
|
|
1379
|
+
const node = graph.nodes.get(file);
|
|
1380
|
+
return sum + (node?.tokenCost || 0);
|
|
1381
|
+
}, 0);
|
|
1382
|
+
let sharedImportRatio = 0;
|
|
1383
|
+
if (files.length >= 2) {
|
|
1384
|
+
const allImportSets = files.map(
|
|
1385
|
+
(f) => new Set(graph.nodes.get(f)?.imports || [])
|
|
1386
|
+
);
|
|
1387
|
+
let intersection = new Set(allImportSets[0]);
|
|
1388
|
+
const union = new Set(allImportSets[0]);
|
|
1389
|
+
for (let i = 1; i < allImportSets.length; i++) {
|
|
1390
|
+
const nextSet = allImportSets[i];
|
|
1391
|
+
intersection = new Set([...intersection].filter((x) => nextSet.has(x)));
|
|
1392
|
+
for (const x of nextSet) union.add(x);
|
|
1393
|
+
}
|
|
1394
|
+
sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
|
|
1395
|
+
}
|
|
1396
|
+
const rawFragmentation = calculateFragmentation(files, domain, {
|
|
1397
|
+
...options,
|
|
1398
|
+
sharedImportRatio
|
|
1399
|
+
});
|
|
1400
|
+
let totalCohesion = 0;
|
|
1401
|
+
let totalAdjustedFragmentation = 0;
|
|
1402
|
+
files.forEach((f) => {
|
|
1403
|
+
const node = graph.nodes.get(f);
|
|
1404
|
+
if (node) {
|
|
1405
|
+
const cohesion = calculateEnhancedCohesion(node.exports);
|
|
1406
|
+
totalCohesion += cohesion;
|
|
1407
|
+
const classification = classifyFile(node, cohesion);
|
|
1408
|
+
totalAdjustedFragmentation += adjustFragmentationForClassification(
|
|
1409
|
+
rawFragmentation,
|
|
1410
|
+
classification
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
const avgCohesion = totalCohesion / files.length;
|
|
1415
|
+
const fragmentationScore = totalAdjustedFragmentation / files.length;
|
|
1416
|
+
clusters.push({
|
|
1417
|
+
domain,
|
|
1418
|
+
files,
|
|
1419
|
+
totalTokens,
|
|
1420
|
+
fragmentationScore,
|
|
1421
|
+
avgCohesion,
|
|
1422
|
+
suggestedStructure: generateSuggestedStructure(
|
|
1423
|
+
files,
|
|
1424
|
+
totalTokens,
|
|
1425
|
+
fragmentationScore
|
|
1426
|
+
)
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
return clusters;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1381
1432
|
// src/remediation.ts
|
|
1382
1433
|
function getClassificationRecommendations(classification, file, issues) {
|
|
1383
1434
|
switch (classification) {
|
|
@@ -1426,6 +1477,11 @@ function getClassificationRecommendations(classification, file, issues) {
|
|
|
1426
1477
|
"Next.js App Router page detected - metadata/JSON-LD/component pattern is cohesive",
|
|
1427
1478
|
"Multiple exports (metadata, faqJsonLd, default) serve single page purpose"
|
|
1428
1479
|
];
|
|
1480
|
+
case "spoke-module":
|
|
1481
|
+
return [
|
|
1482
|
+
"Spoke module detected - intentional monorepo separation is good for modularity",
|
|
1483
|
+
"Ensure this spoke only exports what is necessary for the hub or other spokes"
|
|
1484
|
+
];
|
|
1429
1485
|
case "mixed-concerns":
|
|
1430
1486
|
return [
|
|
1431
1487
|
"Consider splitting this file by domain",
|
|
@@ -1482,7 +1538,79 @@ function getGeneralRecommendations(metrics, thresholds) {
|
|
|
1482
1538
|
return { recommendations, issues, severity };
|
|
1483
1539
|
}
|
|
1484
1540
|
|
|
1485
|
-
// src/
|
|
1541
|
+
// src/mapper.ts
|
|
1542
|
+
function mapNodeToResult(node, graph, clusters, allCircularDeps, options) {
|
|
1543
|
+
const file = node.file;
|
|
1544
|
+
const tokenCost = node.tokenCost;
|
|
1545
|
+
const importDepth = calculateImportDepth(file, graph);
|
|
1546
|
+
const transitiveDeps = getTransitiveDependencies(file, graph);
|
|
1547
|
+
const contextBudget = calculateContextBudget(file, graph);
|
|
1548
|
+
const circularDeps = allCircularDeps.filter((cycle) => cycle.includes(file));
|
|
1549
|
+
const cluster = clusters.find((c) => c.files.includes(file));
|
|
1550
|
+
const rawFragmentationScore = cluster ? cluster.fragmentationScore : 0;
|
|
1551
|
+
const rawCohesionScore = calculateEnhancedCohesion(
|
|
1552
|
+
node.exports,
|
|
1553
|
+
file,
|
|
1554
|
+
options
|
|
1555
|
+
);
|
|
1556
|
+
const fileClassification = classifyFile(node, rawCohesionScore);
|
|
1557
|
+
const cohesionScore = adjustCohesionForClassification(
|
|
1558
|
+
rawCohesionScore,
|
|
1559
|
+
fileClassification
|
|
1560
|
+
);
|
|
1561
|
+
const fragmentationScore = adjustFragmentationForClassification(
|
|
1562
|
+
rawFragmentationScore,
|
|
1563
|
+
fileClassification
|
|
1564
|
+
);
|
|
1565
|
+
const { severity, issues, recommendations, potentialSavings } = analyzeIssues(
|
|
1566
|
+
{
|
|
1567
|
+
file,
|
|
1568
|
+
importDepth,
|
|
1569
|
+
contextBudget,
|
|
1570
|
+
cohesionScore,
|
|
1571
|
+
fragmentationScore,
|
|
1572
|
+
maxDepth: options.maxDepth,
|
|
1573
|
+
maxContextBudget: options.maxContextBudget,
|
|
1574
|
+
minCohesion: options.minCohesion,
|
|
1575
|
+
maxFragmentation: options.maxFragmentation,
|
|
1576
|
+
circularDeps
|
|
1577
|
+
}
|
|
1578
|
+
);
|
|
1579
|
+
const classRecs = getClassificationRecommendations(
|
|
1580
|
+
fileClassification,
|
|
1581
|
+
file,
|
|
1582
|
+
issues
|
|
1583
|
+
);
|
|
1584
|
+
const allRecommendations = Array.from(
|
|
1585
|
+
/* @__PURE__ */ new Set([...recommendations, ...classRecs])
|
|
1586
|
+
);
|
|
1587
|
+
return {
|
|
1588
|
+
file,
|
|
1589
|
+
tokenCost,
|
|
1590
|
+
linesOfCode: node.linesOfCode,
|
|
1591
|
+
importDepth,
|
|
1592
|
+
dependencyCount: transitiveDeps.length,
|
|
1593
|
+
dependencyList: transitiveDeps,
|
|
1594
|
+
circularDeps,
|
|
1595
|
+
cohesionScore,
|
|
1596
|
+
domains: Array.from(
|
|
1597
|
+
new Set(
|
|
1598
|
+
node.exports.flatMap((e) => e.domains?.map((d) => d.domain) || [])
|
|
1599
|
+
)
|
|
1600
|
+
),
|
|
1601
|
+
exportCount: node.exports.length,
|
|
1602
|
+
contextBudget,
|
|
1603
|
+
fragmentationScore,
|
|
1604
|
+
relatedFiles: cluster ? cluster.files : [],
|
|
1605
|
+
fileClassification,
|
|
1606
|
+
severity,
|
|
1607
|
+
issues,
|
|
1608
|
+
recommendations: allRecommendations,
|
|
1609
|
+
potentialSavings
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/orchestrator.ts
|
|
1486
1614
|
function calculateCohesion(exports2, filePath, options) {
|
|
1487
1615
|
return calculateEnhancedCohesion(exports2, filePath, options);
|
|
1488
1616
|
}
|
|
@@ -1535,10 +1663,8 @@ async function analyzeContext(options) {
|
|
|
1535
1663
|
file: metric.file,
|
|
1536
1664
|
tokenCost: 0,
|
|
1537
1665
|
linesOfCode: 0,
|
|
1538
|
-
// Not provided by python context yet
|
|
1539
1666
|
importDepth: metric.importDepth,
|
|
1540
1667
|
dependencyCount: 0,
|
|
1541
|
-
// Not provided
|
|
1542
1668
|
dependencyList: [],
|
|
1543
1669
|
circularDeps: [],
|
|
1544
1670
|
cohesionScore: metric.cohesion,
|
|
@@ -1558,76 +1684,12 @@ async function analyzeContext(options) {
|
|
|
1558
1684
|
const clusters = detectModuleClusters(graph);
|
|
1559
1685
|
const allCircularDeps = detectCircularDependencies(graph);
|
|
1560
1686
|
const results = Array.from(graph.nodes.values()).map(
|
|
1561
|
-
(node) => {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
const circularDeps = allCircularDeps.filter(
|
|
1568
|
-
(cycle) => cycle.includes(file)
|
|
1569
|
-
);
|
|
1570
|
-
const cluster = clusters.find((c) => c.files.includes(file));
|
|
1571
|
-
const rawFragmentationScore = cluster ? cluster.fragmentationScore : 0;
|
|
1572
|
-
const rawCohesionScore = calculateEnhancedCohesion(
|
|
1573
|
-
node.exports,
|
|
1574
|
-
file,
|
|
1575
|
-
options
|
|
1576
|
-
);
|
|
1577
|
-
const fileClassification = classifyFile(node, rawCohesionScore);
|
|
1578
|
-
const cohesionScore = adjustCohesionForClassification(
|
|
1579
|
-
rawCohesionScore,
|
|
1580
|
-
fileClassification
|
|
1581
|
-
);
|
|
1582
|
-
const fragmentationScore = adjustFragmentationForClassification(
|
|
1583
|
-
rawFragmentationScore,
|
|
1584
|
-
fileClassification
|
|
1585
|
-
);
|
|
1586
|
-
const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
|
|
1587
|
-
file,
|
|
1588
|
-
importDepth,
|
|
1589
|
-
contextBudget,
|
|
1590
|
-
cohesionScore,
|
|
1591
|
-
fragmentationScore,
|
|
1592
|
-
maxDepth,
|
|
1593
|
-
maxContextBudget,
|
|
1594
|
-
minCohesion,
|
|
1595
|
-
maxFragmentation,
|
|
1596
|
-
circularDeps
|
|
1597
|
-
});
|
|
1598
|
-
const classRecs = getClassificationRecommendations(
|
|
1599
|
-
fileClassification,
|
|
1600
|
-
file,
|
|
1601
|
-
issues
|
|
1602
|
-
);
|
|
1603
|
-
const allRecommendations = Array.from(
|
|
1604
|
-
/* @__PURE__ */ new Set([...recommendations, ...classRecs])
|
|
1605
|
-
);
|
|
1606
|
-
return {
|
|
1607
|
-
file,
|
|
1608
|
-
tokenCost,
|
|
1609
|
-
linesOfCode: node.linesOfCode,
|
|
1610
|
-
importDepth,
|
|
1611
|
-
dependencyCount: transitiveDeps.length,
|
|
1612
|
-
dependencyList: transitiveDeps,
|
|
1613
|
-
circularDeps,
|
|
1614
|
-
cohesionScore,
|
|
1615
|
-
domains: Array.from(
|
|
1616
|
-
new Set(
|
|
1617
|
-
node.exports.flatMap((e) => e.domains?.map((d) => d.domain) || [])
|
|
1618
|
-
)
|
|
1619
|
-
),
|
|
1620
|
-
exportCount: node.exports.length,
|
|
1621
|
-
contextBudget,
|
|
1622
|
-
fragmentationScore,
|
|
1623
|
-
relatedFiles: cluster ? cluster.files : [],
|
|
1624
|
-
fileClassification,
|
|
1625
|
-
severity,
|
|
1626
|
-
issues,
|
|
1627
|
-
recommendations: allRecommendations,
|
|
1628
|
-
potentialSavings
|
|
1629
|
-
};
|
|
1630
|
-
}
|
|
1687
|
+
(node) => mapNodeToResult(node, graph, clusters, allCircularDeps, {
|
|
1688
|
+
maxDepth,
|
|
1689
|
+
maxContextBudget,
|
|
1690
|
+
minCohesion,
|
|
1691
|
+
maxFragmentation
|
|
1692
|
+
})
|
|
1631
1693
|
);
|
|
1632
1694
|
return [...results, ...pythonResults];
|
|
1633
1695
|
}
|