@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/cli.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
|
});
|
|
@@ -230,7 +230,7 @@ var init_python_context = __esm({
|
|
|
230
230
|
var import_commander = require("commander");
|
|
231
231
|
|
|
232
232
|
// src/index.ts
|
|
233
|
-
var
|
|
233
|
+
var import_core4 = require("@aiready/core");
|
|
234
234
|
|
|
235
235
|
// src/analyzer.ts
|
|
236
236
|
var import_core = require("@aiready/core");
|
|
@@ -570,9 +570,9 @@ function calculatePathEntropy(files) {
|
|
|
570
570
|
if (counts.length <= 1) return 0;
|
|
571
571
|
const total = counts.reduce((s, v) => s + v, 0);
|
|
572
572
|
let entropy = 0;
|
|
573
|
-
for (const
|
|
574
|
-
const
|
|
575
|
-
entropy -=
|
|
573
|
+
for (const count of counts) {
|
|
574
|
+
const prob = count / total;
|
|
575
|
+
entropy -= prob * Math.log2(prob);
|
|
576
576
|
}
|
|
577
577
|
const maxEntropy = Math.log2(counts.length);
|
|
578
578
|
return maxEntropy > 0 ? entropy / maxEntropy : 0;
|
|
@@ -840,8 +840,8 @@ function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
|
|
|
840
840
|
}
|
|
841
841
|
if (probs.length <= 1) return 1;
|
|
842
842
|
let entropy = 0;
|
|
843
|
-
for (const
|
|
844
|
-
entropy -=
|
|
843
|
+
for (const prob of probs) {
|
|
844
|
+
entropy -= prob * Math.log2(prob);
|
|
845
845
|
}
|
|
846
846
|
const maxEntropy = Math.log2(probs.length);
|
|
847
847
|
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
@@ -881,10 +881,10 @@ function calculateDomainCohesion(exports2) {
|
|
|
881
881
|
}
|
|
882
882
|
const total = domains.length;
|
|
883
883
|
let entropy = 0;
|
|
884
|
-
for (const
|
|
885
|
-
const
|
|
886
|
-
if (
|
|
887
|
-
entropy -=
|
|
884
|
+
for (const domainCount of domainCounts.values()) {
|
|
885
|
+
const prob = domainCount / total;
|
|
886
|
+
if (prob > 0) {
|
|
887
|
+
entropy -= prob * Math.log2(prob);
|
|
888
888
|
}
|
|
889
889
|
}
|
|
890
890
|
const maxEntropy = Math.log2(total);
|
|
@@ -901,13 +901,39 @@ function classifyFile(node, cohesionScore, domains) {
|
|
|
901
901
|
if (isConfigOrSchemaFile(node)) {
|
|
902
902
|
return "cohesive-module";
|
|
903
903
|
}
|
|
904
|
+
if (isLambdaHandler(node)) {
|
|
905
|
+
return "lambda-handler";
|
|
906
|
+
}
|
|
907
|
+
if (isDataAccessFile(node)) {
|
|
908
|
+
return "cohesive-module";
|
|
909
|
+
}
|
|
910
|
+
if (isEmailTemplate(node)) {
|
|
911
|
+
return "email-template";
|
|
912
|
+
}
|
|
913
|
+
if (isParserFile(node)) {
|
|
914
|
+
return "parser-file";
|
|
915
|
+
}
|
|
916
|
+
if (isServiceFile(node)) {
|
|
917
|
+
return "service-file";
|
|
918
|
+
}
|
|
919
|
+
if (isSessionFile(node)) {
|
|
920
|
+
return "cohesive-module";
|
|
921
|
+
}
|
|
922
|
+
if (isNextJsPage(node)) {
|
|
923
|
+
return "nextjs-page";
|
|
924
|
+
}
|
|
925
|
+
if (isUtilityFile(node)) {
|
|
926
|
+
return "utility-module";
|
|
927
|
+
}
|
|
928
|
+
if (file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/")) {
|
|
929
|
+
return "utility-module";
|
|
930
|
+
}
|
|
904
931
|
const uniqueDomains = domains.filter((d) => d !== "unknown");
|
|
905
932
|
const hasSingleDomain = uniqueDomains.length <= 1;
|
|
906
|
-
const hasReasonableCohesion = cohesionScore >= 0.5;
|
|
907
933
|
if (hasSingleDomain) {
|
|
908
934
|
return "cohesive-module";
|
|
909
935
|
}
|
|
910
|
-
if (
|
|
936
|
+
if (allExportsShareEntityNoun(exports2)) {
|
|
911
937
|
return "cohesive-module";
|
|
912
938
|
}
|
|
913
939
|
const hasMultipleDomains = uniqueDomains.length > 1;
|
|
@@ -942,10 +968,14 @@ function isTypeDefinitionFile(node) {
|
|
|
942
968
|
const { file, exports: exports2 } = node;
|
|
943
969
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
944
970
|
const isTypesFile = fileName?.includes("types") || fileName?.includes(".d.ts") || fileName === "types.ts" || fileName === "interfaces.ts";
|
|
971
|
+
const lowerPath = file.toLowerCase();
|
|
972
|
+
const isTypesPath = lowerPath.includes("/types/") || lowerPath.includes("/typings/") || lowerPath.includes("/@types/") || lowerPath.startsWith("types/") || lowerPath.startsWith("typings/");
|
|
945
973
|
const typeExports = exports2.filter((e) => e.type === "type" || e.type === "interface");
|
|
946
974
|
const runtimeExports = exports2.filter((e) => e.type === "function" || e.type === "class" || e.type === "const");
|
|
947
975
|
const mostlyTypes = exports2.length > 0 && typeExports.length > runtimeExports.length && typeExports.length / exports2.length > 0.7;
|
|
948
|
-
|
|
976
|
+
const pureTypeFile = exports2.length > 0 && typeExports.length === exports2.length;
|
|
977
|
+
const emptyOrReExportInTypesDir = isTypesPath && exports2.length === 0;
|
|
978
|
+
return isTypesFile || isTypesPath || mostlyTypes || pureTypeFile || emptyOrReExportInTypesDir;
|
|
949
979
|
}
|
|
950
980
|
function isConfigOrSchemaFile(node) {
|
|
951
981
|
const { file, exports: exports2 } = node;
|
|
@@ -982,21 +1012,405 @@ function isUtilityFile(node) {
|
|
|
982
1012
|
"helpers",
|
|
983
1013
|
"common",
|
|
984
1014
|
"shared",
|
|
985
|
-
"lib",
|
|
986
1015
|
"toolbox",
|
|
987
1016
|
"toolkit",
|
|
988
1017
|
".util.",
|
|
989
1018
|
"-util.",
|
|
990
|
-
"_util."
|
|
1019
|
+
"_util.",
|
|
1020
|
+
"-utils.",
|
|
1021
|
+
".utils."
|
|
991
1022
|
];
|
|
992
1023
|
const isUtilityName = utilityPatterns.some(
|
|
993
1024
|
(pattern) => fileName?.includes(pattern)
|
|
994
1025
|
);
|
|
995
|
-
const isUtilityPath = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/") || file.toLowerCase().includes("/
|
|
996
|
-
const
|
|
997
|
-
|
|
1026
|
+
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");
|
|
1027
|
+
const hasManySmallExportsInUtilityContext = exports2.length >= 3 && exports2.every((e) => e.type === "function" || e.type === "const") && (isUtilityName || isUtilityPath);
|
|
1028
|
+
return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
|
|
1029
|
+
}
|
|
1030
|
+
function splitCamelCase(name) {
|
|
1031
|
+
return name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1032
|
+
}
|
|
1033
|
+
var SKIP_WORDS = /* @__PURE__ */ new Set([
|
|
1034
|
+
"get",
|
|
1035
|
+
"set",
|
|
1036
|
+
"create",
|
|
1037
|
+
"update",
|
|
1038
|
+
"delete",
|
|
1039
|
+
"fetch",
|
|
1040
|
+
"save",
|
|
1041
|
+
"load",
|
|
1042
|
+
"parse",
|
|
1043
|
+
"format",
|
|
1044
|
+
"validate",
|
|
1045
|
+
"convert",
|
|
1046
|
+
"transform",
|
|
1047
|
+
"build",
|
|
1048
|
+
"generate",
|
|
1049
|
+
"render",
|
|
1050
|
+
"send",
|
|
1051
|
+
"receive",
|
|
1052
|
+
"find",
|
|
1053
|
+
"list",
|
|
1054
|
+
"add",
|
|
1055
|
+
"remove",
|
|
1056
|
+
"insert",
|
|
1057
|
+
"upsert",
|
|
1058
|
+
"put",
|
|
1059
|
+
"read",
|
|
1060
|
+
"write",
|
|
1061
|
+
"check",
|
|
1062
|
+
"handle",
|
|
1063
|
+
"process",
|
|
1064
|
+
"compute",
|
|
1065
|
+
"calculate",
|
|
1066
|
+
"init",
|
|
1067
|
+
"reset",
|
|
1068
|
+
"clear",
|
|
1069
|
+
"pending",
|
|
1070
|
+
"active",
|
|
1071
|
+
"current",
|
|
1072
|
+
"new",
|
|
1073
|
+
"old",
|
|
1074
|
+
"all",
|
|
1075
|
+
"by",
|
|
1076
|
+
"with",
|
|
1077
|
+
"from",
|
|
1078
|
+
"to",
|
|
1079
|
+
"and",
|
|
1080
|
+
"or",
|
|
1081
|
+
"is",
|
|
1082
|
+
"has",
|
|
1083
|
+
"in",
|
|
1084
|
+
"on",
|
|
1085
|
+
"of",
|
|
1086
|
+
"the"
|
|
1087
|
+
]);
|
|
1088
|
+
function simpleSingularize(word) {
|
|
1089
|
+
if (word.endsWith("ies") && word.length > 3) return word.slice(0, -3) + "y";
|
|
1090
|
+
if (word.endsWith("ses") && word.length > 4) return word.slice(0, -2);
|
|
1091
|
+
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
1092
|
+
return word;
|
|
1093
|
+
}
|
|
1094
|
+
function extractEntityNouns(name) {
|
|
1095
|
+
return splitCamelCase(name).filter((token) => !SKIP_WORDS.has(token) && token.length > 2).map(simpleSingularize);
|
|
1096
|
+
}
|
|
1097
|
+
function allExportsShareEntityNoun(exports2) {
|
|
1098
|
+
if (exports2.length < 2 || exports2.length > 30) return false;
|
|
1099
|
+
const nounSets = exports2.map((e) => new Set(extractEntityNouns(e.name)));
|
|
1100
|
+
if (nounSets.some((s) => s.size === 0)) return false;
|
|
1101
|
+
const [first, ...rest] = nounSets;
|
|
1102
|
+
const commonNouns = Array.from(first).filter(
|
|
1103
|
+
(noun) => rest.every((s) => s.has(noun))
|
|
1104
|
+
);
|
|
1105
|
+
return commonNouns.length > 0;
|
|
1106
|
+
}
|
|
1107
|
+
function isDataAccessFile(node) {
|
|
1108
|
+
const { file, exports: exports2 } = node;
|
|
1109
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1110
|
+
const dalPatterns = [
|
|
1111
|
+
"dynamo",
|
|
1112
|
+
"database",
|
|
1113
|
+
"repository",
|
|
1114
|
+
"repo",
|
|
1115
|
+
"dao",
|
|
1116
|
+
"firestore",
|
|
1117
|
+
"postgres",
|
|
1118
|
+
"mysql",
|
|
1119
|
+
"mongo",
|
|
1120
|
+
"redis",
|
|
1121
|
+
"sqlite",
|
|
1122
|
+
"supabase",
|
|
1123
|
+
"prisma"
|
|
1124
|
+
];
|
|
1125
|
+
const isDalName = dalPatterns.some((p) => fileName?.includes(p));
|
|
1126
|
+
const isDalPath = file.toLowerCase().includes("/repositories/") || file.toLowerCase().includes("/dao/") || file.toLowerCase().includes("/data/");
|
|
1127
|
+
const hasDalExportPattern = exports2.length >= 1 && exports2.length <= 10 && allExportsShareEntityNoun(exports2);
|
|
1128
|
+
const isUtilityPathLocal = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/");
|
|
1129
|
+
return isDalPath || isDalName && hasDalExportPattern && !isUtilityPathLocal;
|
|
1130
|
+
}
|
|
1131
|
+
function isLambdaHandler(node) {
|
|
1132
|
+
const { file, exports: exports2 } = node;
|
|
1133
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1134
|
+
const handlerPatterns = [
|
|
1135
|
+
"handler",
|
|
1136
|
+
".handler.",
|
|
1137
|
+
"-handler.",
|
|
1138
|
+
"lambda",
|
|
1139
|
+
".lambda.",
|
|
1140
|
+
"-lambda."
|
|
1141
|
+
];
|
|
1142
|
+
const isHandlerName = handlerPatterns.some(
|
|
1143
|
+
(pattern) => fileName?.includes(pattern)
|
|
1144
|
+
);
|
|
1145
|
+
const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/lambda/") || file.toLowerCase().includes("/functions/");
|
|
1146
|
+
const hasHandlerExport = exports2.some(
|
|
1147
|
+
(e) => e.name.toLowerCase() === "handler" || e.name.toLowerCase() === "main" || e.name.toLowerCase() === "lambdahandler" || e.name.toLowerCase().endsWith("handler")
|
|
1148
|
+
);
|
|
1149
|
+
const hasSingleEntryInHandlerContext = exports2.length === 1 && (exports2[0].type === "function" || exports2[0].name === "default") && (isHandlerPath || isHandlerName);
|
|
1150
|
+
return isHandlerName || isHandlerPath || hasHandlerExport || hasSingleEntryInHandlerContext;
|
|
1151
|
+
}
|
|
1152
|
+
function isServiceFile(node) {
|
|
1153
|
+
const { file, exports: exports2 } = node;
|
|
1154
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1155
|
+
const servicePatterns = [
|
|
1156
|
+
"service",
|
|
1157
|
+
".service.",
|
|
1158
|
+
"-service.",
|
|
1159
|
+
"_service."
|
|
1160
|
+
];
|
|
1161
|
+
const isServiceName = servicePatterns.some(
|
|
1162
|
+
(pattern) => fileName?.includes(pattern)
|
|
1163
|
+
);
|
|
1164
|
+
const isServicePath = file.toLowerCase().includes("/services/");
|
|
1165
|
+
const hasServiceNamedExport = exports2.some(
|
|
1166
|
+
(e) => e.name.toLowerCase().includes("service") || e.name.toLowerCase().endsWith("service")
|
|
1167
|
+
);
|
|
1168
|
+
const hasClassExport = exports2.some((e) => e.type === "class");
|
|
1169
|
+
return isServiceName || isServicePath || hasServiceNamedExport && hasClassExport;
|
|
1170
|
+
}
|
|
1171
|
+
function isEmailTemplate(node) {
|
|
1172
|
+
const { file, exports: exports2 } = node;
|
|
1173
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1174
|
+
const emailTemplatePatterns = [
|
|
1175
|
+
"-email-",
|
|
1176
|
+
".email.",
|
|
1177
|
+
"_email_",
|
|
1178
|
+
"-template",
|
|
1179
|
+
".template.",
|
|
1180
|
+
"_template",
|
|
1181
|
+
"-mail.",
|
|
1182
|
+
".mail."
|
|
1183
|
+
];
|
|
1184
|
+
const isEmailTemplateName = emailTemplatePatterns.some(
|
|
1185
|
+
(pattern) => fileName?.includes(pattern)
|
|
1186
|
+
);
|
|
1187
|
+
const isSpecificTemplateName = fileName?.includes("receipt") || fileName?.includes("invoice-email") || fileName?.includes("welcome-email") || fileName?.includes("notification-email") || fileName?.includes("writer") && fileName.includes("receipt");
|
|
1188
|
+
const isEmailPath = file.toLowerCase().includes("/emails/") || file.toLowerCase().includes("/mail/") || file.toLowerCase().includes("/notifications/");
|
|
1189
|
+
const hasTemplateFunction = exports2.some(
|
|
1190
|
+
(e) => e.type === "function" && (e.name.toLowerCase().startsWith("render") || e.name.toLowerCase().startsWith("generate") || e.name.toLowerCase().includes("template") && e.name.toLowerCase().includes("email"))
|
|
1191
|
+
);
|
|
1192
|
+
const hasEmailExport = exports2.some(
|
|
1193
|
+
(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"
|
|
1194
|
+
);
|
|
1195
|
+
return isEmailPath || isEmailTemplateName || isSpecificTemplateName || hasTemplateFunction && hasEmailExport;
|
|
1196
|
+
}
|
|
1197
|
+
function isParserFile(node) {
|
|
1198
|
+
const { file, exports: exports2 } = node;
|
|
1199
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1200
|
+
const parserPatterns = [
|
|
1201
|
+
"parser",
|
|
1202
|
+
".parser.",
|
|
1203
|
+
"-parser.",
|
|
1204
|
+
"_parser.",
|
|
1205
|
+
"transform",
|
|
1206
|
+
".transform.",
|
|
1207
|
+
"-transform.",
|
|
1208
|
+
"converter",
|
|
1209
|
+
".converter.",
|
|
1210
|
+
"-converter.",
|
|
1211
|
+
"mapper",
|
|
1212
|
+
".mapper.",
|
|
1213
|
+
"-mapper.",
|
|
1214
|
+
"serializer",
|
|
1215
|
+
".serializer.",
|
|
1216
|
+
"deterministic"
|
|
1217
|
+
// For base-parser-deterministic.ts pattern
|
|
1218
|
+
];
|
|
1219
|
+
const isParserName = parserPatterns.some(
|
|
1220
|
+
(pattern) => fileName?.includes(pattern)
|
|
1221
|
+
);
|
|
1222
|
+
const isParserPath = file.toLowerCase().includes("/parsers/") || file.toLowerCase().includes("/transformers/") || file.toLowerCase().includes("/converters/") || file.toLowerCase().includes("/mappers/");
|
|
1223
|
+
const hasParserExport = exports2.some(
|
|
1224
|
+
(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")
|
|
1225
|
+
);
|
|
1226
|
+
const hasParseFunction = exports2.some(
|
|
1227
|
+
(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"))
|
|
1228
|
+
);
|
|
1229
|
+
return isParserName || isParserPath || hasParserExport || hasParseFunction;
|
|
1230
|
+
}
|
|
1231
|
+
function isSessionFile(node) {
|
|
1232
|
+
const { file, exports: exports2 } = node;
|
|
1233
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1234
|
+
const sessionPatterns = [
|
|
1235
|
+
"session",
|
|
1236
|
+
".session.",
|
|
1237
|
+
"-session.",
|
|
1238
|
+
"state",
|
|
1239
|
+
".state.",
|
|
1240
|
+
"-state.",
|
|
1241
|
+
"context",
|
|
1242
|
+
".context.",
|
|
1243
|
+
"-context.",
|
|
1244
|
+
"store",
|
|
1245
|
+
".store.",
|
|
1246
|
+
"-store."
|
|
1247
|
+
];
|
|
1248
|
+
const isSessionName = sessionPatterns.some(
|
|
1249
|
+
(pattern) => fileName?.includes(pattern)
|
|
1250
|
+
);
|
|
1251
|
+
const isSessionPath = file.toLowerCase().includes("/sessions/") || file.toLowerCase().includes("/state/") || file.toLowerCase().includes("/context/") || file.toLowerCase().includes("/store/");
|
|
1252
|
+
const hasSessionExport = exports2.some(
|
|
1253
|
+
(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")
|
|
1254
|
+
);
|
|
1255
|
+
return isSessionName || isSessionPath || hasSessionExport;
|
|
1256
|
+
}
|
|
1257
|
+
function isNextJsPage(node) {
|
|
1258
|
+
const { file, exports: exports2 } = node;
|
|
1259
|
+
const lowerPath = file.toLowerCase();
|
|
1260
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1261
|
+
const isInAppDir = lowerPath.includes("/app/") || lowerPath.startsWith("app/");
|
|
1262
|
+
const isPageFile = fileName === "page.tsx" || fileName === "page.ts";
|
|
1263
|
+
if (!isInAppDir || !isPageFile) {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
const exportNames = exports2.map((e) => e.name.toLowerCase());
|
|
1267
|
+
const hasDefaultExport = exports2.some((e) => e.type === "default");
|
|
1268
|
+
const nextJsExports = ["metadata", "generatemetadata", "faqjsonld", "jsonld", "icon", "viewport", "dynamic"];
|
|
1269
|
+
const hasNextJsExports = exportNames.some(
|
|
1270
|
+
(name) => nextJsExports.includes(name) || name.includes("jsonld")
|
|
998
1271
|
);
|
|
999
|
-
return
|
|
1272
|
+
return hasDefaultExport || hasNextJsExports;
|
|
1273
|
+
}
|
|
1274
|
+
function adjustCohesionForClassification(baseCohesion, classification, node) {
|
|
1275
|
+
switch (classification) {
|
|
1276
|
+
case "barrel-export":
|
|
1277
|
+
return 1;
|
|
1278
|
+
case "type-definition":
|
|
1279
|
+
return 1;
|
|
1280
|
+
case "utility-module": {
|
|
1281
|
+
if (node) {
|
|
1282
|
+
const exportNames = node.exports.map((e) => e.name.toLowerCase());
|
|
1283
|
+
const hasRelatedNames = hasRelatedExportNames(exportNames);
|
|
1284
|
+
if (hasRelatedNames) {
|
|
1285
|
+
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1289
|
+
}
|
|
1290
|
+
case "service-file": {
|
|
1291
|
+
if (node?.exports.some((e) => e.type === "class")) {
|
|
1292
|
+
return Math.max(0.78, Math.min(1, baseCohesion + 0.4));
|
|
1293
|
+
}
|
|
1294
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1295
|
+
}
|
|
1296
|
+
case "lambda-handler": {
|
|
1297
|
+
if (node) {
|
|
1298
|
+
const hasSingleEntry = node.exports.length === 1 || node.exports.some((e) => e.name.toLowerCase() === "handler");
|
|
1299
|
+
if (hasSingleEntry) {
|
|
1300
|
+
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1304
|
+
}
|
|
1305
|
+
case "email-template": {
|
|
1306
|
+
if (node) {
|
|
1307
|
+
const hasTemplateFunc = node.exports.some(
|
|
1308
|
+
(e) => e.name.toLowerCase().includes("render") || e.name.toLowerCase().includes("generate") || e.name.toLowerCase().includes("template")
|
|
1309
|
+
);
|
|
1310
|
+
if (hasTemplateFunc) {
|
|
1311
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1315
|
+
}
|
|
1316
|
+
case "parser-file": {
|
|
1317
|
+
if (node) {
|
|
1318
|
+
const hasParseFunc = node.exports.some(
|
|
1319
|
+
(e) => e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert")
|
|
1320
|
+
);
|
|
1321
|
+
if (hasParseFunc) {
|
|
1322
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
|
|
1326
|
+
}
|
|
1327
|
+
case "nextjs-page":
|
|
1328
|
+
return 1;
|
|
1329
|
+
case "cohesive-module":
|
|
1330
|
+
return Math.max(baseCohesion, 0.7);
|
|
1331
|
+
case "mixed-concerns":
|
|
1332
|
+
return baseCohesion;
|
|
1333
|
+
default:
|
|
1334
|
+
return Math.min(1, baseCohesion + 0.1);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
function hasRelatedExportNames(exportNames) {
|
|
1338
|
+
if (exportNames.length < 2) return true;
|
|
1339
|
+
const stems = /* @__PURE__ */ new Set();
|
|
1340
|
+
const domains = /* @__PURE__ */ new Set();
|
|
1341
|
+
for (const name of exportNames) {
|
|
1342
|
+
const verbs = ["get", "set", "create", "update", "delete", "fetch", "save", "load", "parse", "format", "validate", "convert", "transform", "build", "generate", "render", "send", "receive"];
|
|
1343
|
+
for (const verb of verbs) {
|
|
1344
|
+
if (name.startsWith(verb) && name.length > verb.length) {
|
|
1345
|
+
stems.add(name.slice(verb.length).toLowerCase());
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const domainPatterns = ["user", "order", "product", "session", "email", "file", "db", "s3", "dynamo", "api", "config"];
|
|
1349
|
+
for (const domain of domainPatterns) {
|
|
1350
|
+
if (name.includes(domain)) {
|
|
1351
|
+
domains.add(domain);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (stems.size === 1 && exportNames.length >= 2) return true;
|
|
1356
|
+
if (domains.size === 1 && exportNames.length >= 2) return true;
|
|
1357
|
+
const prefixes = exportNames.map((name) => {
|
|
1358
|
+
const match = name.match(/^([a-z]+)/);
|
|
1359
|
+
return match ? match[1] : "";
|
|
1360
|
+
}).filter((p) => p.length >= 3);
|
|
1361
|
+
if (prefixes.length >= 2) {
|
|
1362
|
+
const uniquePrefixes = new Set(prefixes);
|
|
1363
|
+
if (uniquePrefixes.size === 1) return true;
|
|
1364
|
+
}
|
|
1365
|
+
const nounSets = exportNames.map((name) => {
|
|
1366
|
+
const tokens = name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1367
|
+
const skip = /* @__PURE__ */ new Set([
|
|
1368
|
+
"get",
|
|
1369
|
+
"set",
|
|
1370
|
+
"create",
|
|
1371
|
+
"update",
|
|
1372
|
+
"delete",
|
|
1373
|
+
"fetch",
|
|
1374
|
+
"save",
|
|
1375
|
+
"load",
|
|
1376
|
+
"parse",
|
|
1377
|
+
"format",
|
|
1378
|
+
"validate",
|
|
1379
|
+
"convert",
|
|
1380
|
+
"transform",
|
|
1381
|
+
"build",
|
|
1382
|
+
"generate",
|
|
1383
|
+
"render",
|
|
1384
|
+
"send",
|
|
1385
|
+
"receive",
|
|
1386
|
+
"find",
|
|
1387
|
+
"list",
|
|
1388
|
+
"add",
|
|
1389
|
+
"remove",
|
|
1390
|
+
"insert",
|
|
1391
|
+
"upsert",
|
|
1392
|
+
"put",
|
|
1393
|
+
"read",
|
|
1394
|
+
"write",
|
|
1395
|
+
"check",
|
|
1396
|
+
"handle",
|
|
1397
|
+
"process",
|
|
1398
|
+
"pending",
|
|
1399
|
+
"active",
|
|
1400
|
+
"current",
|
|
1401
|
+
"new",
|
|
1402
|
+
"old",
|
|
1403
|
+
"all"
|
|
1404
|
+
]);
|
|
1405
|
+
const singularize2 = (w) => w.endsWith("s") && w.length > 3 ? w.slice(0, -1) : w;
|
|
1406
|
+
return new Set(tokens.filter((t) => !skip.has(t) && t.length > 2).map(singularize2));
|
|
1407
|
+
});
|
|
1408
|
+
if (nounSets.length >= 2 && nounSets.every((s) => s.size > 0)) {
|
|
1409
|
+
const [first, ...rest] = nounSets;
|
|
1410
|
+
const commonNouns = Array.from(first).filter((n) => rest.every((s) => s.has(n)));
|
|
1411
|
+
if (commonNouns.length > 0) return true;
|
|
1412
|
+
}
|
|
1413
|
+
return false;
|
|
1000
1414
|
}
|
|
1001
1415
|
function adjustFragmentationForClassification(baseFragmentation, classification) {
|
|
1002
1416
|
switch (classification) {
|
|
@@ -1004,6 +1418,13 @@ function adjustFragmentationForClassification(baseFragmentation, classification)
|
|
|
1004
1418
|
return 0;
|
|
1005
1419
|
case "type-definition":
|
|
1006
1420
|
return 0;
|
|
1421
|
+
case "utility-module":
|
|
1422
|
+
case "service-file":
|
|
1423
|
+
case "lambda-handler":
|
|
1424
|
+
case "email-template":
|
|
1425
|
+
case "parser-file":
|
|
1426
|
+
case "nextjs-page":
|
|
1427
|
+
return baseFragmentation * 0.2;
|
|
1007
1428
|
case "cohesive-module":
|
|
1008
1429
|
return baseFragmentation * 0.3;
|
|
1009
1430
|
case "mixed-concerns":
|
|
@@ -1029,6 +1450,36 @@ function getClassificationRecommendations(classification, file, issues) {
|
|
|
1029
1450
|
"Module has good cohesion despite its size",
|
|
1030
1451
|
"Consider documenting the module boundaries for AI assistants"
|
|
1031
1452
|
];
|
|
1453
|
+
case "utility-module":
|
|
1454
|
+
return [
|
|
1455
|
+
"Utility module detected - multiple domains are acceptable here",
|
|
1456
|
+
"Consider grouping related utilities by prefix or domain for better discoverability"
|
|
1457
|
+
];
|
|
1458
|
+
case "service-file":
|
|
1459
|
+
return [
|
|
1460
|
+
"Service file detected - orchestration of multiple dependencies is expected",
|
|
1461
|
+
"Consider documenting service boundaries and dependencies"
|
|
1462
|
+
];
|
|
1463
|
+
case "lambda-handler":
|
|
1464
|
+
return [
|
|
1465
|
+
"Lambda handler detected - coordination of services is expected",
|
|
1466
|
+
"Ensure handler has clear single responsibility"
|
|
1467
|
+
];
|
|
1468
|
+
case "email-template":
|
|
1469
|
+
return [
|
|
1470
|
+
"Email template detected - references multiple domains for rendering",
|
|
1471
|
+
"Template structure is cohesive by design"
|
|
1472
|
+
];
|
|
1473
|
+
case "parser-file":
|
|
1474
|
+
return [
|
|
1475
|
+
"Parser/transformer file detected - handles multiple data sources",
|
|
1476
|
+
"Consider documenting input/output schemas"
|
|
1477
|
+
];
|
|
1478
|
+
case "nextjs-page":
|
|
1479
|
+
return [
|
|
1480
|
+
"Next.js App Router page detected - metadata/JSON-LD/component pattern is cohesive",
|
|
1481
|
+
"Multiple exports (metadata, faqJsonLd, default) serve single page purpose"
|
|
1482
|
+
];
|
|
1032
1483
|
case "mixed-concerns":
|
|
1033
1484
|
return [
|
|
1034
1485
|
"Consider splitting this file by domain",
|
|
@@ -1040,6 +1491,9 @@ function getClassificationRecommendations(classification, file, issues) {
|
|
|
1040
1491
|
}
|
|
1041
1492
|
}
|
|
1042
1493
|
|
|
1494
|
+
// src/scoring.ts
|
|
1495
|
+
var import_core2 = require("@aiready/core");
|
|
1496
|
+
|
|
1043
1497
|
// src/index.ts
|
|
1044
1498
|
async function analyzeContext(options) {
|
|
1045
1499
|
const {
|
|
@@ -1051,7 +1505,7 @@ async function analyzeContext(options) {
|
|
|
1051
1505
|
includeNodeModules = false,
|
|
1052
1506
|
...scanOptions
|
|
1053
1507
|
} = options;
|
|
1054
|
-
const files = await (0,
|
|
1508
|
+
const files = await (0, import_core4.scanFiles)({
|
|
1055
1509
|
...scanOptions,
|
|
1056
1510
|
// Only add node_modules to exclude if includeNodeModules is false
|
|
1057
1511
|
// The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
|
|
@@ -1063,7 +1517,7 @@ async function analyzeContext(options) {
|
|
|
1063
1517
|
const fileContents = await Promise.all(
|
|
1064
1518
|
files.map(async (file) => ({
|
|
1065
1519
|
file,
|
|
1066
|
-
content: await (0,
|
|
1520
|
+
content: await (0, import_core4.readFileContent)(file)
|
|
1067
1521
|
}))
|
|
1068
1522
|
);
|
|
1069
1523
|
const graph = buildDependencyGraph(fileContents.filter((f) => !f.file.toLowerCase().endsWith(".py")));
|
|
@@ -1126,7 +1580,7 @@ async function analyzeContext(options) {
|
|
|
1126
1580
|
const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
|
|
1127
1581
|
const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
|
|
1128
1582
|
const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
1129
|
-
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
|
|
1583
|
+
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file, { coUsageMatrix: graph.coUsageMatrix }) : 1;
|
|
1130
1584
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
1131
1585
|
const relatedFiles = [];
|
|
1132
1586
|
for (const cluster of clusters) {
|
|
@@ -1151,6 +1605,11 @@ async function analyzeContext(options) {
|
|
|
1151
1605
|
...new Set(node.exports.map((e) => e.inferredDomain || "unknown"))
|
|
1152
1606
|
];
|
|
1153
1607
|
const fileClassification = classifyFile(node, cohesionScore, domains);
|
|
1608
|
+
const adjustedCohesionScore = adjustCohesionForClassification(
|
|
1609
|
+
cohesionScore,
|
|
1610
|
+
fileClassification,
|
|
1611
|
+
node
|
|
1612
|
+
);
|
|
1154
1613
|
const adjustedFragmentationScore = adjustFragmentationForClassification(
|
|
1155
1614
|
fragmentationScore,
|
|
1156
1615
|
fileClassification
|
|
@@ -1169,7 +1628,8 @@ async function analyzeContext(options) {
|
|
|
1169
1628
|
file,
|
|
1170
1629
|
importDepth,
|
|
1171
1630
|
contextBudget,
|
|
1172
|
-
cohesionScore,
|
|
1631
|
+
cohesionScore: adjustedCohesionScore,
|
|
1632
|
+
// Use adjusted cohesion
|
|
1173
1633
|
fragmentationScore: adjustedFragmentationScore,
|
|
1174
1634
|
maxDepth,
|
|
1175
1635
|
maxContextBudget,
|
|
@@ -1185,7 +1645,8 @@ async function analyzeContext(options) {
|
|
|
1185
1645
|
dependencyCount: dependencyList.length,
|
|
1186
1646
|
dependencyList,
|
|
1187
1647
|
circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
|
|
1188
|
-
cohesionScore,
|
|
1648
|
+
cohesionScore: adjustedCohesionScore,
|
|
1649
|
+
// Report adjusted cohesion
|
|
1189
1650
|
domains,
|
|
1190
1651
|
exportCount: node.exports.length,
|
|
1191
1652
|
contextBudget,
|
|
@@ -1428,7 +1889,7 @@ function downgradeSeverity(s) {
|
|
|
1428
1889
|
var import_chalk = __toESM(require("chalk"));
|
|
1429
1890
|
var import_fs = require("fs");
|
|
1430
1891
|
var import_path2 = require("path");
|
|
1431
|
-
var
|
|
1892
|
+
var import_core5 = require("@aiready/core");
|
|
1432
1893
|
var import_prompts = __toESM(require("prompts"));
|
|
1433
1894
|
var program = new import_commander.Command();
|
|
1434
1895
|
program.name("aiready-context").description("Analyze AI context window cost and code structure").version("0.1.0").addHelpText("after", "\nCONFIGURATION:\n Supports config files: aiready.json, aiready.config.json, .aiready.json, .aireadyrc.json, aiready.config.js, .aireadyrc.js\n CLI options override config file settings").argument("<directory>", "Directory to analyze").option("--max-depth <number>", "Maximum acceptable import depth").option(
|
|
@@ -1459,7 +1920,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
1459
1920
|
exclude: void 0,
|
|
1460
1921
|
maxResults: 10
|
|
1461
1922
|
};
|
|
1462
|
-
let finalOptions = await (0,
|
|
1923
|
+
let finalOptions = await (0, import_core5.loadMergedConfig)(directory, defaults, {
|
|
1463
1924
|
maxDepth: options.maxDepth ? parseInt(options.maxDepth) : void 0,
|
|
1464
1925
|
maxContextBudget: options.maxContext ? parseInt(options.maxContext) : void 0,
|
|
1465
1926
|
minCohesion: options.minCohesion ? parseFloat(options.minCohesion) : void 0,
|
|
@@ -1474,7 +1935,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
1474
1935
|
finalOptions = await runInteractiveSetup(directory, finalOptions);
|
|
1475
1936
|
}
|
|
1476
1937
|
const results = await analyzeContext(finalOptions);
|
|
1477
|
-
const elapsedTime = (0,
|
|
1938
|
+
const elapsedTime = (0, import_core5.getElapsedTime)(startTime);
|
|
1478
1939
|
const summary = generateSummary(results);
|
|
1479
1940
|
if (options.output === "json") {
|
|
1480
1941
|
const jsonOutput = {
|
|
@@ -1483,18 +1944,18 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
1483
1944
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1484
1945
|
analysisTime: elapsedTime
|
|
1485
1946
|
};
|
|
1486
|
-
const outputPath = (0,
|
|
1947
|
+
const outputPath = (0, import_core5.resolveOutputPath)(
|
|
1487
1948
|
options.outputFile,
|
|
1488
1949
|
`context-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
|
|
1489
1950
|
directory
|
|
1490
1951
|
);
|
|
1491
|
-
(0,
|
|
1952
|
+
(0, import_core5.handleJSONOutput)(jsonOutput, outputPath, `
|
|
1492
1953
|
\u2713 JSON report saved to ${outputPath}`);
|
|
1493
1954
|
return;
|
|
1494
1955
|
}
|
|
1495
1956
|
if (options.output === "html") {
|
|
1496
1957
|
const html = generateHTMLReport(summary, results);
|
|
1497
|
-
const outputPath = (0,
|
|
1958
|
+
const outputPath = (0, import_core5.resolveOutputPath)(
|
|
1498
1959
|
options.outputFile,
|
|
1499
1960
|
`context-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.html`,
|
|
1500
1961
|
directory
|
|
@@ -1511,7 +1972,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
1511
1972
|
displayConsoleReport(summary, results, elapsedTime, finalOptions.maxResults);
|
|
1512
1973
|
displayTuningGuidance(results, finalOptions);
|
|
1513
1974
|
} catch (error) {
|
|
1514
|
-
(0,
|
|
1975
|
+
(0, import_core5.handleCLIError)(error, "Analysis");
|
|
1515
1976
|
}
|
|
1516
1977
|
});
|
|
1517
1978
|
program.parse();
|