@aiready/context-analyzer 0.9.26 → 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);
@@ -979,6 +979,9 @@ function classifyFile(node, cohesionScore, domains) {
979
979
  if (isLambdaHandler(node)) {
980
980
  return "lambda-handler";
981
981
  }
982
+ if (isDataAccessFile(node)) {
983
+ return "cohesive-module";
984
+ }
982
985
  if (isEmailTemplate(node)) {
983
986
  return "email-template";
984
987
  }
@@ -997,11 +1000,17 @@ function classifyFile(node, cohesionScore, domains) {
997
1000
  if (isUtilityFile(node)) {
998
1001
  return "utility-module";
999
1002
  }
1003
+ if (file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/")) {
1004
+ return "utility-module";
1005
+ }
1000
1006
  const uniqueDomains = domains.filter((d) => d !== "unknown");
1001
1007
  const hasSingleDomain = uniqueDomains.length <= 1;
1002
1008
  if (hasSingleDomain) {
1003
1009
  return "cohesive-module";
1004
1010
  }
1011
+ if (allExportsShareEntityNoun(exports2)) {
1012
+ return "cohesive-module";
1013
+ }
1005
1014
  const hasMultipleDomains = uniqueDomains.length > 1;
1006
1015
  const hasLowCohesion = cohesionScore < 0.4;
1007
1016
  if (hasMultipleDomains && hasLowCohesion) {
@@ -1093,6 +1102,107 @@ function isUtilityFile(node) {
1093
1102
  const hasManySmallExportsInUtilityContext = exports2.length >= 3 && exports2.every((e) => e.type === "function" || e.type === "const") && (isUtilityName || isUtilityPath);
1094
1103
  return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
1095
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
+ }
1096
1206
  function isLambdaHandler(node) {
1097
1207
  const { file, exports: exports2 } = node;
1098
1208
  const fileName = file.split("/").pop()?.toLowerCase();
@@ -1107,7 +1217,7 @@ function isLambdaHandler(node) {
1107
1217
  const isHandlerName = handlerPatterns.some(
1108
1218
  (pattern) => fileName?.includes(pattern)
1109
1219
  );
1110
- const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/functions/");
1220
+ const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/lambda/") || file.toLowerCase().includes("/functions/");
1111
1221
  const hasHandlerExport = exports2.some(
1112
1222
  (e) => e.name.toLowerCase() === "handler" || e.name.toLowerCase() === "main" || e.name.toLowerCase() === "lambdahandler" || e.name.toLowerCase().endsWith("handler")
1113
1223
  );
@@ -1247,25 +1357,25 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
1247
1357
  const exportNames = node.exports.map((e) => e.name.toLowerCase());
1248
1358
  const hasRelatedNames = hasRelatedExportNames(exportNames);
1249
1359
  if (hasRelatedNames) {
1250
- return Math.min(1, baseCohesion + 0.45);
1360
+ return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
1251
1361
  }
1252
1362
  }
1253
- return Math.min(1, baseCohesion + 0.35);
1363
+ return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
1254
1364
  }
1255
1365
  case "service-file": {
1256
1366
  if (node?.exports.some((e) => e.type === "class")) {
1257
- return Math.min(1, baseCohesion + 0.4);
1367
+ return Math.max(0.78, Math.min(1, baseCohesion + 0.4));
1258
1368
  }
1259
- return Math.min(1, baseCohesion + 0.3);
1369
+ return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
1260
1370
  }
1261
1371
  case "lambda-handler": {
1262
1372
  if (node) {
1263
1373
  const hasSingleEntry = node.exports.length === 1 || node.exports.some((e) => e.name.toLowerCase() === "handler");
1264
1374
  if (hasSingleEntry) {
1265
- return Math.min(1, baseCohesion + 0.45);
1375
+ return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
1266
1376
  }
1267
1377
  }
1268
- return Math.min(1, baseCohesion + 0.35);
1378
+ return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
1269
1379
  }
1270
1380
  case "email-template": {
1271
1381
  if (node) {
@@ -1273,10 +1383,10 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
1273
1383
  (e) => e.name.toLowerCase().includes("render") || e.name.toLowerCase().includes("generate") || e.name.toLowerCase().includes("template")
1274
1384
  );
1275
1385
  if (hasTemplateFunc) {
1276
- return Math.min(1, baseCohesion + 0.4);
1386
+ return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
1277
1387
  }
1278
1388
  }
1279
- return Math.min(1, baseCohesion + 0.3);
1389
+ return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
1280
1390
  }
1281
1391
  case "parser-file": {
1282
1392
  if (node) {
@@ -1284,10 +1394,10 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
1284
1394
  (e) => e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert")
1285
1395
  );
1286
1396
  if (hasParseFunc) {
1287
- return Math.min(1, baseCohesion + 0.4);
1397
+ return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
1288
1398
  }
1289
1399
  }
1290
- return Math.min(1, baseCohesion + 0.3);
1400
+ return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
1291
1401
  }
1292
1402
  case "nextjs-page":
1293
1403
  return 1;
@@ -1327,6 +1437,54 @@ function hasRelatedExportNames(exportNames) {
1327
1437
  const uniquePrefixes = new Set(prefixes);
1328
1438
  if (uniquePrefixes.size === 1) return true;
1329
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
+ }
1330
1488
  return false;
1331
1489
  }
1332
1490
  function adjustFragmentationForClassification(baseFragmentation, classification) {
@@ -1409,7 +1567,8 @@ function getClassificationRecommendations(classification, file, issues) {
1409
1567
  }
1410
1568
 
1411
1569
  // src/scoring.ts
1412
- function calculateContextScore(summary) {
1570
+ var import_core2 = require("@aiready/core");
1571
+ function calculateContextScore(summary, costConfig) {
1413
1572
  const {
1414
1573
  avgContextBudget,
1415
1574
  maxContextBudget,
@@ -1498,6 +1657,14 @@ function calculateContextScore(summary) {
1498
1657
  priority: "high"
1499
1658
  });
1500
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);
1501
1668
  return {
1502
1669
  toolName: "context-analyzer",
1503
1670
  score,
@@ -1508,7 +1675,10 @@ function calculateContextScore(summary) {
1508
1675
  maxImportDepth,
1509
1676
  avgFragmentation: Math.round(avgFragmentation * 100) / 100,
1510
1677
  criticalIssues,
1511
- majorIssues
1678
+ majorIssues,
1679
+ // Business value metrics
1680
+ estimatedMonthlyCost,
1681
+ estimatedDeveloperHours: productivityImpact.totalHours
1512
1682
  },
1513
1683
  factors,
1514
1684
  recommendations
@@ -1517,7 +1687,7 @@ function calculateContextScore(summary) {
1517
1687
 
1518
1688
  // src/index.ts
1519
1689
  async function getSmartDefaults(directory, userOptions) {
1520
- const files = await (0, import_core3.scanFiles)({
1690
+ const files = await (0, import_core4.scanFiles)({
1521
1691
  rootDir: directory,
1522
1692
  include: userOptions.include,
1523
1693
  exclude: userOptions.exclude
@@ -1570,7 +1740,7 @@ async function analyzeContext(options) {
1570
1740
  includeNodeModules = false,
1571
1741
  ...scanOptions
1572
1742
  } = options;
1573
- const files = await (0, import_core3.scanFiles)({
1743
+ const files = await (0, import_core4.scanFiles)({
1574
1744
  ...scanOptions,
1575
1745
  // Only add node_modules to exclude if includeNodeModules is false
1576
1746
  // The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
@@ -1582,7 +1752,7 @@ async function analyzeContext(options) {
1582
1752
  const fileContents = await Promise.all(
1583
1753
  files.map(async (file) => ({
1584
1754
  file,
1585
- content: await (0, import_core3.readFileContent)(file)
1755
+ content: await (0, import_core4.readFileContent)(file)
1586
1756
  }))
1587
1757
  );
1588
1758
  const graph = buildDependencyGraph(fileContents.filter((f) => !f.file.toLowerCase().endsWith(".py")));
@@ -1645,7 +1815,7 @@ async function analyzeContext(options) {
1645
1815
  const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
1646
1816
  const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
1647
1817
  const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
1648
- 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;
1649
1819
  const fragmentationScore = fragmentationMap.get(file) || 0;
1650
1820
  const relatedFiles = [];
1651
1821
  for (const cluster of clusters) {
package/dist/index.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  getCoUsageData,
13
13
  getSmartDefaults,
14
14
  inferDomainFromSemantics
15
- } from "./chunk-PJD4VCIH.mjs";
15
+ } from "./chunk-M64RHH4D.mjs";
16
16
  import "./chunk-Y6FXYEAI.mjs";
17
17
  export {
18
18
  adjustFragmentationForClassification,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/context-analyzer",
3
- "version": "0.9.26",
3
+ "version": "0.9.28",
4
4
  "description": "AI context window cost analysis - detect fragmented code, deep import chains, and expensive context budgets",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -49,7 +49,7 @@
49
49
  "commander": "^14.0.0",
50
50
  "chalk": "^5.3.0",
51
51
  "prompts": "^2.4.2",
52
- "@aiready/core": "0.9.22"
52
+ "@aiready/core": "0.9.25"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^24.0.0",