@aiready/context-analyzer 0.9.25 → 0.9.28
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 +22 -24
- package/dist/chunk-FYI56A5M.mjs +1892 -0
- package/dist/chunk-I77HFFZU.mjs +1876 -0
- package/dist/chunk-KYSZF5N6.mjs +1876 -0
- package/dist/chunk-M64RHH4D.mjs +1896 -0
- package/dist/chunk-OP4G6GLN.mjs +1876 -0
- package/dist/chunk-P3T3H27S.mjs +1895 -0
- package/dist/chunk-PJD4VCIH.mjs +1722 -0
- package/dist/chunk-VBWXHKGD.mjs +1895 -0
- package/dist/cli.js +497 -36
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +502 -32
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/file-classification.test.ts +560 -9
- package/src/analyzer.ts +709 -26
- package/src/index.ts +12 -4
- package/src/scoring.ts +28 -1
- package/src/types.ts +6 -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_core3.getParser)("dummy.py");
|
|
41
41
|
if (!parser) {
|
|
42
42
|
console.warn("Python parser not available");
|
|
43
43
|
return results;
|
|
@@ -86,7 +86,7 @@ async function analyzePythonContext(files, rootDir) {
|
|
|
86
86
|
}
|
|
87
87
|
async function buildPythonDependencyGraph(files, rootDir) {
|
|
88
88
|
const graph = /* @__PURE__ */ new Map();
|
|
89
|
-
const parser = (0,
|
|
89
|
+
const parser = (0, import_core3.getParser)("dummy.py");
|
|
90
90
|
if (!parser) return graph;
|
|
91
91
|
for (const file of files) {
|
|
92
92
|
try {
|
|
@@ -167,7 +167,7 @@ async function calculatePythonImportDepth(file, dependencyGraph, visited, depth
|
|
|
167
167
|
return maxDepth;
|
|
168
168
|
}
|
|
169
169
|
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
170
|
-
let budget = (0,
|
|
170
|
+
let budget = (0, import_core3.estimateTokens)(code);
|
|
171
171
|
const avgTokensPerDep = 500;
|
|
172
172
|
budget += imports.length * avgTokensPerDep;
|
|
173
173
|
return budget;
|
|
@@ -217,11 +217,11 @@ function detectCircularDependencies2(file, dependencyGraph) {
|
|
|
217
217
|
dfs(file, []);
|
|
218
218
|
return [...new Set(circular)];
|
|
219
219
|
}
|
|
220
|
-
var
|
|
220
|
+
var import_core3, import_path;
|
|
221
221
|
var init_python_context = __esm({
|
|
222
222
|
"src/analyzers/python-context.ts"() {
|
|
223
223
|
"use strict";
|
|
224
|
-
|
|
224
|
+
import_core3 = require("@aiready/core");
|
|
225
225
|
import_path = require("path");
|
|
226
226
|
}
|
|
227
227
|
});
|
|
@@ -244,7 +244,7 @@ __export(index_exports, {
|
|
|
244
244
|
inferDomainFromSemantics: () => inferDomainFromSemantics
|
|
245
245
|
});
|
|
246
246
|
module.exports = __toCommonJS(index_exports);
|
|
247
|
-
var
|
|
247
|
+
var import_core4 = require("@aiready/core");
|
|
248
248
|
|
|
249
249
|
// src/analyzer.ts
|
|
250
250
|
var import_core = require("@aiready/core");
|
|
@@ -645,9 +645,9 @@ function calculatePathEntropy(files) {
|
|
|
645
645
|
if (counts.length <= 1) return 0;
|
|
646
646
|
const total = counts.reduce((s, v) => s + v, 0);
|
|
647
647
|
let entropy = 0;
|
|
648
|
-
for (const
|
|
649
|
-
const
|
|
650
|
-
entropy -=
|
|
648
|
+
for (const count of counts) {
|
|
649
|
+
const prob = count / total;
|
|
650
|
+
entropy -= prob * Math.log2(prob);
|
|
651
651
|
}
|
|
652
652
|
const maxEntropy = Math.log2(counts.length);
|
|
653
653
|
return maxEntropy > 0 ? entropy / maxEntropy : 0;
|
|
@@ -915,8 +915,8 @@ function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
|
|
|
915
915
|
}
|
|
916
916
|
if (probs.length <= 1) return 1;
|
|
917
917
|
let entropy = 0;
|
|
918
|
-
for (const
|
|
919
|
-
entropy -=
|
|
918
|
+
for (const prob of probs) {
|
|
919
|
+
entropy -= prob * Math.log2(prob);
|
|
920
920
|
}
|
|
921
921
|
const maxEntropy = Math.log2(probs.length);
|
|
922
922
|
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
@@ -956,10 +956,10 @@ function calculateDomainCohesion(exports2) {
|
|
|
956
956
|
}
|
|
957
957
|
const total = domains.length;
|
|
958
958
|
let entropy = 0;
|
|
959
|
-
for (const
|
|
960
|
-
const
|
|
961
|
-
if (
|
|
962
|
-
entropy -=
|
|
959
|
+
for (const domainCount of domainCounts.values()) {
|
|
960
|
+
const prob = domainCount / total;
|
|
961
|
+
if (prob > 0) {
|
|
962
|
+
entropy -= prob * Math.log2(prob);
|
|
963
963
|
}
|
|
964
964
|
}
|
|
965
965
|
const maxEntropy = Math.log2(total);
|
|
@@ -976,13 +976,39 @@ function classifyFile(node, cohesionScore, domains) {
|
|
|
976
976
|
if (isConfigOrSchemaFile(node)) {
|
|
977
977
|
return "cohesive-module";
|
|
978
978
|
}
|
|
979
|
+
if (isLambdaHandler(node)) {
|
|
980
|
+
return "lambda-handler";
|
|
981
|
+
}
|
|
982
|
+
if (isDataAccessFile(node)) {
|
|
983
|
+
return "cohesive-module";
|
|
984
|
+
}
|
|
985
|
+
if (isEmailTemplate(node)) {
|
|
986
|
+
return "email-template";
|
|
987
|
+
}
|
|
988
|
+
if (isParserFile(node)) {
|
|
989
|
+
return "parser-file";
|
|
990
|
+
}
|
|
991
|
+
if (isServiceFile(node)) {
|
|
992
|
+
return "service-file";
|
|
993
|
+
}
|
|
994
|
+
if (isSessionFile(node)) {
|
|
995
|
+
return "cohesive-module";
|
|
996
|
+
}
|
|
997
|
+
if (isNextJsPage(node)) {
|
|
998
|
+
return "nextjs-page";
|
|
999
|
+
}
|
|
1000
|
+
if (isUtilityFile(node)) {
|
|
1001
|
+
return "utility-module";
|
|
1002
|
+
}
|
|
1003
|
+
if (file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/")) {
|
|
1004
|
+
return "utility-module";
|
|
1005
|
+
}
|
|
979
1006
|
const uniqueDomains = domains.filter((d) => d !== "unknown");
|
|
980
1007
|
const hasSingleDomain = uniqueDomains.length <= 1;
|
|
981
|
-
const hasReasonableCohesion = cohesionScore >= 0.5;
|
|
982
1008
|
if (hasSingleDomain) {
|
|
983
1009
|
return "cohesive-module";
|
|
984
1010
|
}
|
|
985
|
-
if (
|
|
1011
|
+
if (allExportsShareEntityNoun(exports2)) {
|
|
986
1012
|
return "cohesive-module";
|
|
987
1013
|
}
|
|
988
1014
|
const hasMultipleDomains = uniqueDomains.length > 1;
|
|
@@ -1017,10 +1043,14 @@ function isTypeDefinitionFile(node) {
|
|
|
1017
1043
|
const { file, exports: exports2 } = node;
|
|
1018
1044
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1019
1045
|
const isTypesFile = fileName?.includes("types") || fileName?.includes(".d.ts") || fileName === "types.ts" || fileName === "interfaces.ts";
|
|
1046
|
+
const lowerPath = file.toLowerCase();
|
|
1047
|
+
const isTypesPath = lowerPath.includes("/types/") || lowerPath.includes("/typings/") || lowerPath.includes("/@types/") || lowerPath.startsWith("types/") || lowerPath.startsWith("typings/");
|
|
1020
1048
|
const typeExports = exports2.filter((e) => e.type === "type" || e.type === "interface");
|
|
1021
1049
|
const runtimeExports = exports2.filter((e) => e.type === "function" || e.type === "class" || e.type === "const");
|
|
1022
1050
|
const mostlyTypes = exports2.length > 0 && typeExports.length > runtimeExports.length && typeExports.length / exports2.length > 0.7;
|
|
1023
|
-
|
|
1051
|
+
const pureTypeFile = exports2.length > 0 && typeExports.length === exports2.length;
|
|
1052
|
+
const emptyOrReExportInTypesDir = isTypesPath && exports2.length === 0;
|
|
1053
|
+
return isTypesFile || isTypesPath || mostlyTypes || pureTypeFile || emptyOrReExportInTypesDir;
|
|
1024
1054
|
}
|
|
1025
1055
|
function isConfigOrSchemaFile(node) {
|
|
1026
1056
|
const { file, exports: exports2 } = node;
|
|
@@ -1057,21 +1087,405 @@ function isUtilityFile(node) {
|
|
|
1057
1087
|
"helpers",
|
|
1058
1088
|
"common",
|
|
1059
1089
|
"shared",
|
|
1060
|
-
"lib",
|
|
1061
1090
|
"toolbox",
|
|
1062
1091
|
"toolkit",
|
|
1063
1092
|
".util.",
|
|
1064
1093
|
"-util.",
|
|
1065
|
-
"_util."
|
|
1094
|
+
"_util.",
|
|
1095
|
+
"-utils.",
|
|
1096
|
+
".utils."
|
|
1066
1097
|
];
|
|
1067
1098
|
const isUtilityName = utilityPatterns.some(
|
|
1068
1099
|
(pattern) => fileName?.includes(pattern)
|
|
1069
1100
|
);
|
|
1070
|
-
const isUtilityPath = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/") || file.toLowerCase().includes("/
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1101
|
+
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");
|
|
1102
|
+
const hasManySmallExportsInUtilityContext = exports2.length >= 3 && exports2.every((e) => e.type === "function" || e.type === "const") && (isUtilityName || isUtilityPath);
|
|
1103
|
+
return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
|
|
1104
|
+
}
|
|
1105
|
+
function splitCamelCase(name) {
|
|
1106
|
+
return name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1107
|
+
}
|
|
1108
|
+
var SKIP_WORDS = /* @__PURE__ */ new Set([
|
|
1109
|
+
"get",
|
|
1110
|
+
"set",
|
|
1111
|
+
"create",
|
|
1112
|
+
"update",
|
|
1113
|
+
"delete",
|
|
1114
|
+
"fetch",
|
|
1115
|
+
"save",
|
|
1116
|
+
"load",
|
|
1117
|
+
"parse",
|
|
1118
|
+
"format",
|
|
1119
|
+
"validate",
|
|
1120
|
+
"convert",
|
|
1121
|
+
"transform",
|
|
1122
|
+
"build",
|
|
1123
|
+
"generate",
|
|
1124
|
+
"render",
|
|
1125
|
+
"send",
|
|
1126
|
+
"receive",
|
|
1127
|
+
"find",
|
|
1128
|
+
"list",
|
|
1129
|
+
"add",
|
|
1130
|
+
"remove",
|
|
1131
|
+
"insert",
|
|
1132
|
+
"upsert",
|
|
1133
|
+
"put",
|
|
1134
|
+
"read",
|
|
1135
|
+
"write",
|
|
1136
|
+
"check",
|
|
1137
|
+
"handle",
|
|
1138
|
+
"process",
|
|
1139
|
+
"compute",
|
|
1140
|
+
"calculate",
|
|
1141
|
+
"init",
|
|
1142
|
+
"reset",
|
|
1143
|
+
"clear",
|
|
1144
|
+
"pending",
|
|
1145
|
+
"active",
|
|
1146
|
+
"current",
|
|
1147
|
+
"new",
|
|
1148
|
+
"old",
|
|
1149
|
+
"all",
|
|
1150
|
+
"by",
|
|
1151
|
+
"with",
|
|
1152
|
+
"from",
|
|
1153
|
+
"to",
|
|
1154
|
+
"and",
|
|
1155
|
+
"or",
|
|
1156
|
+
"is",
|
|
1157
|
+
"has",
|
|
1158
|
+
"in",
|
|
1159
|
+
"on",
|
|
1160
|
+
"of",
|
|
1161
|
+
"the"
|
|
1162
|
+
]);
|
|
1163
|
+
function simpleSingularize(word) {
|
|
1164
|
+
if (word.endsWith("ies") && word.length > 3) return word.slice(0, -3) + "y";
|
|
1165
|
+
if (word.endsWith("ses") && word.length > 4) return word.slice(0, -2);
|
|
1166
|
+
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
1167
|
+
return word;
|
|
1168
|
+
}
|
|
1169
|
+
function extractEntityNouns(name) {
|
|
1170
|
+
return splitCamelCase(name).filter((token) => !SKIP_WORDS.has(token) && token.length > 2).map(simpleSingularize);
|
|
1171
|
+
}
|
|
1172
|
+
function allExportsShareEntityNoun(exports2) {
|
|
1173
|
+
if (exports2.length < 2 || exports2.length > 30) return false;
|
|
1174
|
+
const nounSets = exports2.map((e) => new Set(extractEntityNouns(e.name)));
|
|
1175
|
+
if (nounSets.some((s) => s.size === 0)) return false;
|
|
1176
|
+
const [first, ...rest] = nounSets;
|
|
1177
|
+
const commonNouns = Array.from(first).filter(
|
|
1178
|
+
(noun) => rest.every((s) => s.has(noun))
|
|
1179
|
+
);
|
|
1180
|
+
return commonNouns.length > 0;
|
|
1181
|
+
}
|
|
1182
|
+
function isDataAccessFile(node) {
|
|
1183
|
+
const { file, exports: exports2 } = node;
|
|
1184
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1185
|
+
const dalPatterns = [
|
|
1186
|
+
"dynamo",
|
|
1187
|
+
"database",
|
|
1188
|
+
"repository",
|
|
1189
|
+
"repo",
|
|
1190
|
+
"dao",
|
|
1191
|
+
"firestore",
|
|
1192
|
+
"postgres",
|
|
1193
|
+
"mysql",
|
|
1194
|
+
"mongo",
|
|
1195
|
+
"redis",
|
|
1196
|
+
"sqlite",
|
|
1197
|
+
"supabase",
|
|
1198
|
+
"prisma"
|
|
1199
|
+
];
|
|
1200
|
+
const isDalName = dalPatterns.some((p) => fileName?.includes(p));
|
|
1201
|
+
const isDalPath = file.toLowerCase().includes("/repositories/") || file.toLowerCase().includes("/dao/") || file.toLowerCase().includes("/data/");
|
|
1202
|
+
const hasDalExportPattern = exports2.length >= 1 && exports2.length <= 10 && allExportsShareEntityNoun(exports2);
|
|
1203
|
+
const isUtilityPathLocal = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/");
|
|
1204
|
+
return isDalPath || isDalName && hasDalExportPattern && !isUtilityPathLocal;
|
|
1205
|
+
}
|
|
1206
|
+
function isLambdaHandler(node) {
|
|
1207
|
+
const { file, exports: exports2 } = node;
|
|
1208
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1209
|
+
const handlerPatterns = [
|
|
1210
|
+
"handler",
|
|
1211
|
+
".handler.",
|
|
1212
|
+
"-handler.",
|
|
1213
|
+
"lambda",
|
|
1214
|
+
".lambda.",
|
|
1215
|
+
"-lambda."
|
|
1216
|
+
];
|
|
1217
|
+
const isHandlerName = handlerPatterns.some(
|
|
1218
|
+
(pattern) => fileName?.includes(pattern)
|
|
1219
|
+
);
|
|
1220
|
+
const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/lambda/") || file.toLowerCase().includes("/functions/");
|
|
1221
|
+
const hasHandlerExport = exports2.some(
|
|
1222
|
+
(e) => e.name.toLowerCase() === "handler" || e.name.toLowerCase() === "main" || e.name.toLowerCase() === "lambdahandler" || e.name.toLowerCase().endsWith("handler")
|
|
1223
|
+
);
|
|
1224
|
+
const hasSingleEntryInHandlerContext = exports2.length === 1 && (exports2[0].type === "function" || exports2[0].name === "default") && (isHandlerPath || isHandlerName);
|
|
1225
|
+
return isHandlerName || isHandlerPath || hasHandlerExport || hasSingleEntryInHandlerContext;
|
|
1226
|
+
}
|
|
1227
|
+
function isServiceFile(node) {
|
|
1228
|
+
const { file, exports: exports2 } = node;
|
|
1229
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1230
|
+
const servicePatterns = [
|
|
1231
|
+
"service",
|
|
1232
|
+
".service.",
|
|
1233
|
+
"-service.",
|
|
1234
|
+
"_service."
|
|
1235
|
+
];
|
|
1236
|
+
const isServiceName = servicePatterns.some(
|
|
1237
|
+
(pattern) => fileName?.includes(pattern)
|
|
1238
|
+
);
|
|
1239
|
+
const isServicePath = file.toLowerCase().includes("/services/");
|
|
1240
|
+
const hasServiceNamedExport = exports2.some(
|
|
1241
|
+
(e) => e.name.toLowerCase().includes("service") || e.name.toLowerCase().endsWith("service")
|
|
1242
|
+
);
|
|
1243
|
+
const hasClassExport = exports2.some((e) => e.type === "class");
|
|
1244
|
+
return isServiceName || isServicePath || hasServiceNamedExport && hasClassExport;
|
|
1245
|
+
}
|
|
1246
|
+
function isEmailTemplate(node) {
|
|
1247
|
+
const { file, exports: exports2 } = node;
|
|
1248
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1249
|
+
const emailTemplatePatterns = [
|
|
1250
|
+
"-email-",
|
|
1251
|
+
".email.",
|
|
1252
|
+
"_email_",
|
|
1253
|
+
"-template",
|
|
1254
|
+
".template.",
|
|
1255
|
+
"_template",
|
|
1256
|
+
"-mail.",
|
|
1257
|
+
".mail."
|
|
1258
|
+
];
|
|
1259
|
+
const isEmailTemplateName = emailTemplatePatterns.some(
|
|
1260
|
+
(pattern) => fileName?.includes(pattern)
|
|
1261
|
+
);
|
|
1262
|
+
const isSpecificTemplateName = fileName?.includes("receipt") || fileName?.includes("invoice-email") || fileName?.includes("welcome-email") || fileName?.includes("notification-email") || fileName?.includes("writer") && fileName.includes("receipt");
|
|
1263
|
+
const isEmailPath = file.toLowerCase().includes("/emails/") || file.toLowerCase().includes("/mail/") || file.toLowerCase().includes("/notifications/");
|
|
1264
|
+
const hasTemplateFunction = exports2.some(
|
|
1265
|
+
(e) => e.type === "function" && (e.name.toLowerCase().startsWith("render") || e.name.toLowerCase().startsWith("generate") || e.name.toLowerCase().includes("template") && e.name.toLowerCase().includes("email"))
|
|
1266
|
+
);
|
|
1267
|
+
const hasEmailExport = exports2.some(
|
|
1268
|
+
(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"
|
|
1269
|
+
);
|
|
1270
|
+
return isEmailPath || isEmailTemplateName || isSpecificTemplateName || hasTemplateFunction && hasEmailExport;
|
|
1271
|
+
}
|
|
1272
|
+
function isParserFile(node) {
|
|
1273
|
+
const { file, exports: exports2 } = node;
|
|
1274
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1275
|
+
const parserPatterns = [
|
|
1276
|
+
"parser",
|
|
1277
|
+
".parser.",
|
|
1278
|
+
"-parser.",
|
|
1279
|
+
"_parser.",
|
|
1280
|
+
"transform",
|
|
1281
|
+
".transform.",
|
|
1282
|
+
"-transform.",
|
|
1283
|
+
"converter",
|
|
1284
|
+
".converter.",
|
|
1285
|
+
"-converter.",
|
|
1286
|
+
"mapper",
|
|
1287
|
+
".mapper.",
|
|
1288
|
+
"-mapper.",
|
|
1289
|
+
"serializer",
|
|
1290
|
+
".serializer.",
|
|
1291
|
+
"deterministic"
|
|
1292
|
+
// For base-parser-deterministic.ts pattern
|
|
1293
|
+
];
|
|
1294
|
+
const isParserName = parserPatterns.some(
|
|
1295
|
+
(pattern) => fileName?.includes(pattern)
|
|
1296
|
+
);
|
|
1297
|
+
const isParserPath = file.toLowerCase().includes("/parsers/") || file.toLowerCase().includes("/transformers/") || file.toLowerCase().includes("/converters/") || file.toLowerCase().includes("/mappers/");
|
|
1298
|
+
const hasParserExport = exports2.some(
|
|
1299
|
+
(e) => e.name.toLowerCase().includes("parse") || e.name.toLowerCase().includes("transform") || e.name.toLowerCase().includes("convert") || e.name.toLowerCase().includes("map") || e.name.toLowerCase().includes("serialize") || e.name.toLowerCase().includes("deserialize")
|
|
1300
|
+
);
|
|
1301
|
+
const hasParseFunction = exports2.some(
|
|
1302
|
+
(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"))
|
|
1303
|
+
);
|
|
1304
|
+
return isParserName || isParserPath || hasParserExport || hasParseFunction;
|
|
1305
|
+
}
|
|
1306
|
+
function isSessionFile(node) {
|
|
1307
|
+
const { file, exports: exports2 } = node;
|
|
1308
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1309
|
+
const sessionPatterns = [
|
|
1310
|
+
"session",
|
|
1311
|
+
".session.",
|
|
1312
|
+
"-session.",
|
|
1313
|
+
"state",
|
|
1314
|
+
".state.",
|
|
1315
|
+
"-state.",
|
|
1316
|
+
"context",
|
|
1317
|
+
".context.",
|
|
1318
|
+
"-context.",
|
|
1319
|
+
"store",
|
|
1320
|
+
".store.",
|
|
1321
|
+
"-store."
|
|
1322
|
+
];
|
|
1323
|
+
const isSessionName = sessionPatterns.some(
|
|
1324
|
+
(pattern) => fileName?.includes(pattern)
|
|
1325
|
+
);
|
|
1326
|
+
const isSessionPath = file.toLowerCase().includes("/sessions/") || file.toLowerCase().includes("/state/") || file.toLowerCase().includes("/context/") || file.toLowerCase().includes("/store/");
|
|
1327
|
+
const hasSessionExport = exports2.some(
|
|
1328
|
+
(e) => e.name.toLowerCase().includes("session") || e.name.toLowerCase().includes("state") || e.name.toLowerCase().includes("context") || e.name.toLowerCase().includes("manager") || e.name.toLowerCase().includes("store")
|
|
1329
|
+
);
|
|
1330
|
+
return isSessionName || isSessionPath || hasSessionExport;
|
|
1331
|
+
}
|
|
1332
|
+
function isNextJsPage(node) {
|
|
1333
|
+
const { file, exports: exports2 } = node;
|
|
1334
|
+
const lowerPath = file.toLowerCase();
|
|
1335
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1336
|
+
const isInAppDir = lowerPath.includes("/app/") || lowerPath.startsWith("app/");
|
|
1337
|
+
const isPageFile = fileName === "page.tsx" || fileName === "page.ts";
|
|
1338
|
+
if (!isInAppDir || !isPageFile) {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
const exportNames = exports2.map((e) => e.name.toLowerCase());
|
|
1342
|
+
const hasDefaultExport = exports2.some((e) => e.type === "default");
|
|
1343
|
+
const nextJsExports = ["metadata", "generatemetadata", "faqjsonld", "jsonld", "icon", "viewport", "dynamic"];
|
|
1344
|
+
const hasNextJsExports = exportNames.some(
|
|
1345
|
+
(name) => nextJsExports.includes(name) || name.includes("jsonld")
|
|
1073
1346
|
);
|
|
1074
|
-
return
|
|
1347
|
+
return hasDefaultExport || hasNextJsExports;
|
|
1348
|
+
}
|
|
1349
|
+
function adjustCohesionForClassification(baseCohesion, classification, node) {
|
|
1350
|
+
switch (classification) {
|
|
1351
|
+
case "barrel-export":
|
|
1352
|
+
return 1;
|
|
1353
|
+
case "type-definition":
|
|
1354
|
+
return 1;
|
|
1355
|
+
case "utility-module": {
|
|
1356
|
+
if (node) {
|
|
1357
|
+
const exportNames = node.exports.map((e) => e.name.toLowerCase());
|
|
1358
|
+
const hasRelatedNames = hasRelatedExportNames(exportNames);
|
|
1359
|
+
if (hasRelatedNames) {
|
|
1360
|
+
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1364
|
+
}
|
|
1365
|
+
case "service-file": {
|
|
1366
|
+
if (node?.exports.some((e) => e.type === "class")) {
|
|
1367
|
+
return Math.max(0.78, Math.min(1, baseCohesion + 0.4));
|
|
1368
|
+
}
|
|
1369
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1370
|
+
}
|
|
1371
|
+
case "lambda-handler": {
|
|
1372
|
+
if (node) {
|
|
1373
|
+
const hasSingleEntry = node.exports.length === 1 || node.exports.some((e) => e.name.toLowerCase() === "handler");
|
|
1374
|
+
if (hasSingleEntry) {
|
|
1375
|
+
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1379
|
+
}
|
|
1380
|
+
case "email-template": {
|
|
1381
|
+
if (node) {
|
|
1382
|
+
const hasTemplateFunc = node.exports.some(
|
|
1383
|
+
(e) => e.name.toLowerCase().includes("render") || e.name.toLowerCase().includes("generate") || e.name.toLowerCase().includes("template")
|
|
1384
|
+
);
|
|
1385
|
+
if (hasTemplateFunc) {
|
|
1386
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1390
|
+
}
|
|
1391
|
+
case "parser-file": {
|
|
1392
|
+
if (node) {
|
|
1393
|
+
const hasParseFunc = node.exports.some(
|
|
1394
|
+
(e) => e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert")
|
|
1395
|
+
);
|
|
1396
|
+
if (hasParseFunc) {
|
|
1397
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
|
|
1401
|
+
}
|
|
1402
|
+
case "nextjs-page":
|
|
1403
|
+
return 1;
|
|
1404
|
+
case "cohesive-module":
|
|
1405
|
+
return Math.max(baseCohesion, 0.7);
|
|
1406
|
+
case "mixed-concerns":
|
|
1407
|
+
return baseCohesion;
|
|
1408
|
+
default:
|
|
1409
|
+
return Math.min(1, baseCohesion + 0.1);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function hasRelatedExportNames(exportNames) {
|
|
1413
|
+
if (exportNames.length < 2) return true;
|
|
1414
|
+
const stems = /* @__PURE__ */ new Set();
|
|
1415
|
+
const domains = /* @__PURE__ */ new Set();
|
|
1416
|
+
for (const name of exportNames) {
|
|
1417
|
+
const verbs = ["get", "set", "create", "update", "delete", "fetch", "save", "load", "parse", "format", "validate", "convert", "transform", "build", "generate", "render", "send", "receive"];
|
|
1418
|
+
for (const verb of verbs) {
|
|
1419
|
+
if (name.startsWith(verb) && name.length > verb.length) {
|
|
1420
|
+
stems.add(name.slice(verb.length).toLowerCase());
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const domainPatterns = ["user", "order", "product", "session", "email", "file", "db", "s3", "dynamo", "api", "config"];
|
|
1424
|
+
for (const domain of domainPatterns) {
|
|
1425
|
+
if (name.includes(domain)) {
|
|
1426
|
+
domains.add(domain);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (stems.size === 1 && exportNames.length >= 2) return true;
|
|
1431
|
+
if (domains.size === 1 && exportNames.length >= 2) return true;
|
|
1432
|
+
const prefixes = exportNames.map((name) => {
|
|
1433
|
+
const match = name.match(/^([a-z]+)/);
|
|
1434
|
+
return match ? match[1] : "";
|
|
1435
|
+
}).filter((p) => p.length >= 3);
|
|
1436
|
+
if (prefixes.length >= 2) {
|
|
1437
|
+
const uniquePrefixes = new Set(prefixes);
|
|
1438
|
+
if (uniquePrefixes.size === 1) return true;
|
|
1439
|
+
}
|
|
1440
|
+
const nounSets = exportNames.map((name) => {
|
|
1441
|
+
const tokens = name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1442
|
+
const skip = /* @__PURE__ */ new Set([
|
|
1443
|
+
"get",
|
|
1444
|
+
"set",
|
|
1445
|
+
"create",
|
|
1446
|
+
"update",
|
|
1447
|
+
"delete",
|
|
1448
|
+
"fetch",
|
|
1449
|
+
"save",
|
|
1450
|
+
"load",
|
|
1451
|
+
"parse",
|
|
1452
|
+
"format",
|
|
1453
|
+
"validate",
|
|
1454
|
+
"convert",
|
|
1455
|
+
"transform",
|
|
1456
|
+
"build",
|
|
1457
|
+
"generate",
|
|
1458
|
+
"render",
|
|
1459
|
+
"send",
|
|
1460
|
+
"receive",
|
|
1461
|
+
"find",
|
|
1462
|
+
"list",
|
|
1463
|
+
"add",
|
|
1464
|
+
"remove",
|
|
1465
|
+
"insert",
|
|
1466
|
+
"upsert",
|
|
1467
|
+
"put",
|
|
1468
|
+
"read",
|
|
1469
|
+
"write",
|
|
1470
|
+
"check",
|
|
1471
|
+
"handle",
|
|
1472
|
+
"process",
|
|
1473
|
+
"pending",
|
|
1474
|
+
"active",
|
|
1475
|
+
"current",
|
|
1476
|
+
"new",
|
|
1477
|
+
"old",
|
|
1478
|
+
"all"
|
|
1479
|
+
]);
|
|
1480
|
+
const singularize2 = (w) => w.endsWith("s") && w.length > 3 ? w.slice(0, -1) : w;
|
|
1481
|
+
return new Set(tokens.filter((t) => !skip.has(t) && t.length > 2).map(singularize2));
|
|
1482
|
+
});
|
|
1483
|
+
if (nounSets.length >= 2 && nounSets.every((s) => s.size > 0)) {
|
|
1484
|
+
const [first, ...rest] = nounSets;
|
|
1485
|
+
const commonNouns = Array.from(first).filter((n) => rest.every((s) => s.has(n)));
|
|
1486
|
+
if (commonNouns.length > 0) return true;
|
|
1487
|
+
}
|
|
1488
|
+
return false;
|
|
1075
1489
|
}
|
|
1076
1490
|
function adjustFragmentationForClassification(baseFragmentation, classification) {
|
|
1077
1491
|
switch (classification) {
|
|
@@ -1079,6 +1493,13 @@ function adjustFragmentationForClassification(baseFragmentation, classification)
|
|
|
1079
1493
|
return 0;
|
|
1080
1494
|
case "type-definition":
|
|
1081
1495
|
return 0;
|
|
1496
|
+
case "utility-module":
|
|
1497
|
+
case "service-file":
|
|
1498
|
+
case "lambda-handler":
|
|
1499
|
+
case "email-template":
|
|
1500
|
+
case "parser-file":
|
|
1501
|
+
case "nextjs-page":
|
|
1502
|
+
return baseFragmentation * 0.2;
|
|
1082
1503
|
case "cohesive-module":
|
|
1083
1504
|
return baseFragmentation * 0.3;
|
|
1084
1505
|
case "mixed-concerns":
|
|
@@ -1104,6 +1525,36 @@ function getClassificationRecommendations(classification, file, issues) {
|
|
|
1104
1525
|
"Module has good cohesion despite its size",
|
|
1105
1526
|
"Consider documenting the module boundaries for AI assistants"
|
|
1106
1527
|
];
|
|
1528
|
+
case "utility-module":
|
|
1529
|
+
return [
|
|
1530
|
+
"Utility module detected - multiple domains are acceptable here",
|
|
1531
|
+
"Consider grouping related utilities by prefix or domain for better discoverability"
|
|
1532
|
+
];
|
|
1533
|
+
case "service-file":
|
|
1534
|
+
return [
|
|
1535
|
+
"Service file detected - orchestration of multiple dependencies is expected",
|
|
1536
|
+
"Consider documenting service boundaries and dependencies"
|
|
1537
|
+
];
|
|
1538
|
+
case "lambda-handler":
|
|
1539
|
+
return [
|
|
1540
|
+
"Lambda handler detected - coordination of services is expected",
|
|
1541
|
+
"Ensure handler has clear single responsibility"
|
|
1542
|
+
];
|
|
1543
|
+
case "email-template":
|
|
1544
|
+
return [
|
|
1545
|
+
"Email template detected - references multiple domains for rendering",
|
|
1546
|
+
"Template structure is cohesive by design"
|
|
1547
|
+
];
|
|
1548
|
+
case "parser-file":
|
|
1549
|
+
return [
|
|
1550
|
+
"Parser/transformer file detected - handles multiple data sources",
|
|
1551
|
+
"Consider documenting input/output schemas"
|
|
1552
|
+
];
|
|
1553
|
+
case "nextjs-page":
|
|
1554
|
+
return [
|
|
1555
|
+
"Next.js App Router page detected - metadata/JSON-LD/component pattern is cohesive",
|
|
1556
|
+
"Multiple exports (metadata, faqJsonLd, default) serve single page purpose"
|
|
1557
|
+
];
|
|
1107
1558
|
case "mixed-concerns":
|
|
1108
1559
|
return [
|
|
1109
1560
|
"Consider splitting this file by domain",
|
|
@@ -1116,7 +1567,8 @@ function getClassificationRecommendations(classification, file, issues) {
|
|
|
1116
1567
|
}
|
|
1117
1568
|
|
|
1118
1569
|
// src/scoring.ts
|
|
1119
|
-
|
|
1570
|
+
var import_core2 = require("@aiready/core");
|
|
1571
|
+
function calculateContextScore(summary, costConfig) {
|
|
1120
1572
|
const {
|
|
1121
1573
|
avgContextBudget,
|
|
1122
1574
|
maxContextBudget,
|
|
@@ -1205,6 +1657,14 @@ function calculateContextScore(summary) {
|
|
|
1205
1657
|
priority: "high"
|
|
1206
1658
|
});
|
|
1207
1659
|
}
|
|
1660
|
+
const cfg = { ...import_core2.DEFAULT_COST_CONFIG, ...costConfig };
|
|
1661
|
+
const totalContextBudget = avgContextBudget * summary.totalFiles;
|
|
1662
|
+
const estimatedMonthlyCost = (0, import_core2.calculateMonthlyCost)(totalContextBudget, cfg);
|
|
1663
|
+
const issues = [
|
|
1664
|
+
...Array(criticalIssues).fill({ severity: "critical" }),
|
|
1665
|
+
...Array(majorIssues).fill({ severity: "major" })
|
|
1666
|
+
];
|
|
1667
|
+
const productivityImpact = (0, import_core2.calculateProductivityImpact)(issues);
|
|
1208
1668
|
return {
|
|
1209
1669
|
toolName: "context-analyzer",
|
|
1210
1670
|
score,
|
|
@@ -1215,7 +1675,10 @@ function calculateContextScore(summary) {
|
|
|
1215
1675
|
maxImportDepth,
|
|
1216
1676
|
avgFragmentation: Math.round(avgFragmentation * 100) / 100,
|
|
1217
1677
|
criticalIssues,
|
|
1218
|
-
majorIssues
|
|
1678
|
+
majorIssues,
|
|
1679
|
+
// Business value metrics
|
|
1680
|
+
estimatedMonthlyCost,
|
|
1681
|
+
estimatedDeveloperHours: productivityImpact.totalHours
|
|
1219
1682
|
},
|
|
1220
1683
|
factors,
|
|
1221
1684
|
recommendations
|
|
@@ -1224,7 +1687,7 @@ function calculateContextScore(summary) {
|
|
|
1224
1687
|
|
|
1225
1688
|
// src/index.ts
|
|
1226
1689
|
async function getSmartDefaults(directory, userOptions) {
|
|
1227
|
-
const files = await (0,
|
|
1690
|
+
const files = await (0, import_core4.scanFiles)({
|
|
1228
1691
|
rootDir: directory,
|
|
1229
1692
|
include: userOptions.include,
|
|
1230
1693
|
exclude: userOptions.exclude
|
|
@@ -1277,7 +1740,7 @@ async function analyzeContext(options) {
|
|
|
1277
1740
|
includeNodeModules = false,
|
|
1278
1741
|
...scanOptions
|
|
1279
1742
|
} = options;
|
|
1280
|
-
const files = await (0,
|
|
1743
|
+
const files = await (0, import_core4.scanFiles)({
|
|
1281
1744
|
...scanOptions,
|
|
1282
1745
|
// Only add node_modules to exclude if includeNodeModules is false
|
|
1283
1746
|
// The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
|
|
@@ -1289,7 +1752,7 @@ async function analyzeContext(options) {
|
|
|
1289
1752
|
const fileContents = await Promise.all(
|
|
1290
1753
|
files.map(async (file) => ({
|
|
1291
1754
|
file,
|
|
1292
|
-
content: await (0,
|
|
1755
|
+
content: await (0, import_core4.readFileContent)(file)
|
|
1293
1756
|
}))
|
|
1294
1757
|
);
|
|
1295
1758
|
const graph = buildDependencyGraph(fileContents.filter((f) => !f.file.toLowerCase().endsWith(".py")));
|
|
@@ -1352,7 +1815,7 @@ async function analyzeContext(options) {
|
|
|
1352
1815
|
const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
|
|
1353
1816
|
const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
|
|
1354
1817
|
const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
1355
|
-
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
|
|
1818
|
+
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file, { coUsageMatrix: graph.coUsageMatrix }) : 1;
|
|
1356
1819
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
1357
1820
|
const relatedFiles = [];
|
|
1358
1821
|
for (const cluster of clusters) {
|
|
@@ -1377,6 +1840,11 @@ async function analyzeContext(options) {
|
|
|
1377
1840
|
...new Set(node.exports.map((e) => e.inferredDomain || "unknown"))
|
|
1378
1841
|
];
|
|
1379
1842
|
const fileClassification = classifyFile(node, cohesionScore, domains);
|
|
1843
|
+
const adjustedCohesionScore = adjustCohesionForClassification(
|
|
1844
|
+
cohesionScore,
|
|
1845
|
+
fileClassification,
|
|
1846
|
+
node
|
|
1847
|
+
);
|
|
1380
1848
|
const adjustedFragmentationScore = adjustFragmentationForClassification(
|
|
1381
1849
|
fragmentationScore,
|
|
1382
1850
|
fileClassification
|
|
@@ -1395,7 +1863,8 @@ async function analyzeContext(options) {
|
|
|
1395
1863
|
file,
|
|
1396
1864
|
importDepth,
|
|
1397
1865
|
contextBudget,
|
|
1398
|
-
cohesionScore,
|
|
1866
|
+
cohesionScore: adjustedCohesionScore,
|
|
1867
|
+
// Use adjusted cohesion
|
|
1399
1868
|
fragmentationScore: adjustedFragmentationScore,
|
|
1400
1869
|
maxDepth,
|
|
1401
1870
|
maxContextBudget,
|
|
@@ -1411,7 +1880,8 @@ async function analyzeContext(options) {
|
|
|
1411
1880
|
dependencyCount: dependencyList.length,
|
|
1412
1881
|
dependencyList,
|
|
1413
1882
|
circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
|
|
1414
|
-
cohesionScore,
|
|
1883
|
+
cohesionScore: adjustedCohesionScore,
|
|
1884
|
+
// Report adjusted cohesion
|
|
1415
1885
|
domains,
|
|
1416
1886
|
exportCount: node.exports.length,
|
|
1417
1887
|
contextBudget,
|