@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/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, 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
  });
@@ -244,7 +244,7 @@ __export(index_exports, {
244
244
  inferDomainFromSemantics: () => inferDomainFromSemantics
245
245
  });
246
246
  module.exports = __toCommonJS(index_exports);
247
- var import_core3 = require("@aiready/core");
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 c of counts) {
649
- const p = c / total;
650
- entropy -= p * Math.log2(p);
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 p of probs) {
919
- entropy -= p * Math.log2(p);
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 count of domainCounts.values()) {
960
- const p = count / total;
961
- if (p > 0) {
962
- entropy -= p * Math.log2(p);
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 (isUtilityFile(node)) {
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
- return isTypesFile || mostlyTypes;
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("/lib/") || file.toLowerCase().includes("/common/");
1071
- const hasManySmallExports = exports2.length >= 3 && exports2.every(
1072
- (e) => e.type === "function" || e.type === "const"
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 isUtilityName || isUtilityPath || hasManySmallExports;
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
- function calculateContextScore(summary) {
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, import_core3.scanFiles)({
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, import_core3.scanFiles)({
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, import_core3.readFileContent)(file)
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,