@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/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, import_core2.getParser)("dummy.py");
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, import_core2.getParser)("dummy.py");
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, import_core2.estimateTokens)(code);
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 import_core2, import_path;
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
- import_core2 = require("@aiready/core");
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 import_core3 = require("@aiready/core");
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 c of counts) {
574
- const p = c / total;
575
- entropy -= p * Math.log2(p);
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 p of probs) {
844
- entropy -= p * Math.log2(p);
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 count of domainCounts.values()) {
885
- const p = count / total;
886
- if (p > 0) {
887
- entropy -= p * Math.log2(p);
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 (isUtilityFile(node)) {
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
- return isTypesFile || mostlyTypes;
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("/lib/") || file.toLowerCase().includes("/common/");
996
- const hasManySmallExports = exports2.length >= 3 && exports2.every(
997
- (e) => e.type === "function" || e.type === "const"
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 isUtilityName || isUtilityPath || hasManySmallExports;
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, import_core3.scanFiles)({
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, import_core3.readFileContent)(file)
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 import_core4 = require("@aiready/core");
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, import_core4.loadMergedConfig)(directory, defaults, {
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, import_core4.getElapsedTime)(startTime);
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, import_core4.resolveOutputPath)(
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, import_core4.handleJSONOutput)(jsonOutput, outputPath, `
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, import_core4.resolveOutputPath)(
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, import_core4.handleCLIError)(error, "Analysis");
1975
+ (0, import_core5.handleCLIError)(error, "Analysis");
1515
1976
  }
1516
1977
  });
1517
1978
  program.parse();