@aiready/context-analyzer 0.21.10 → 0.21.12

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
@@ -336,7 +336,7 @@ var import_core11 = require("@aiready/core");
336
336
  // src/provider.ts
337
337
  var import_core9 = require("@aiready/core");
338
338
 
339
- // src/analyzer.ts
339
+ // src/orchestrator.ts
340
340
  var import_core6 = require("@aiready/core");
341
341
 
342
342
  // src/metrics.ts
@@ -649,16 +649,17 @@ function calculateEnhancedCohesion(exports2, filePath, options) {
649
649
  if (filePath && isTestFile(filePath)) return 1;
650
650
  const domains = exports2.map((e) => e.inferredDomain || "unknown");
651
651
  const domainCounts = /* @__PURE__ */ new Map();
652
- for (const d of domains) domainCounts.set(d, (domainCounts.get(d) || 0) + 1);
652
+ for (const domain of domains)
653
+ domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
653
654
  if (domainCounts.size === 1 && domains[0] !== "unknown") {
654
655
  if (!options?.weights) return 1;
655
656
  }
656
657
  const probs = Array.from(domainCounts.values()).map(
657
- (c) => c / exports2.length
658
+ (count) => count / exports2.length
658
659
  );
659
660
  let domainEntropy = 0;
660
- for (const p of probs) {
661
- if (p > 0) domainEntropy -= p * Math.log2(p);
661
+ for (const prob of probs) {
662
+ if (prob > 0) domainEntropy -= prob * Math.log2(prob);
662
663
  }
663
664
  const maxEntropy = Math.log2(Math.max(2, domainCounts.size));
664
665
  const domainScore = 1 - domainEntropy / maxEntropy;
@@ -719,7 +720,7 @@ function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
719
720
  function calculateFragmentation(files, domain, options) {
720
721
  if (files.length <= 1) return 0;
721
722
  const directories = new Set(
722
- files.map((f) => f.split("/").slice(0, -1).join("/"))
723
+ files.map((file) => file.split("/").slice(0, -1).join("/"))
723
724
  );
724
725
  const uniqueDirs = directories.size;
725
726
  let score = options?.useLogScale ? uniqueDirs <= 1 ? 0 : Math.log(uniqueDirs) / Math.log(options.logBase || Math.E) / (Math.log(files.length) / Math.log(options.logBase || Math.E)) : (uniqueDirs - 1) / (files.length - 1);
@@ -732,13 +733,13 @@ function calculateFragmentation(files, domain, options) {
732
733
  function calculatePathEntropy(files) {
733
734
  if (!files || files.length === 0) return 0;
734
735
  const dirCounts = /* @__PURE__ */ new Map();
735
- for (const f of files) {
736
- const dir = f.split("/").slice(0, -1).join("/") || ".";
736
+ for (const file of files) {
737
+ const dir = file.split("/").slice(0, -1).join("/") || ".";
737
738
  dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
738
739
  }
739
740
  const counts = Array.from(dirCounts.values());
740
741
  if (counts.length <= 1) return 0;
741
- const total = counts.reduce((s, v) => s + v, 0);
742
+ const total = counts.reduce((sum, value) => sum + value, 0);
742
743
  let entropy = 0;
743
744
  for (const count of counts) {
744
745
  const prob = count / total;
@@ -749,11 +750,11 @@ function calculatePathEntropy(files) {
749
750
  }
750
751
  function calculateDirectoryDistance(files) {
751
752
  if (!files || files.length <= 1) return 0;
752
- const pathSegments = (p) => p.split("/").filter(Boolean);
753
- const commonAncestorDepth = (a, b) => {
754
- const minLen = Math.min(a.length, b.length);
753
+ const pathSegments = (pathStr) => pathStr.split("/").filter(Boolean);
754
+ const commonAncestorDepth = (pathA, pathB) => {
755
+ const minLen = Math.min(pathA.length, pathB.length);
755
756
  let i = 0;
756
- while (i < minLen && a[i] === b[i]) i++;
757
+ while (i < minLen && pathA[i] === pathB[i]) i++;
757
758
  return i;
758
759
  };
759
760
  let totalNormalized = 0;
@@ -1002,86 +1003,67 @@ function detectCircularDependencies(graph) {
1002
1003
  return detectGraphCycles(graph.edges);
1003
1004
  }
1004
1005
 
1005
- // src/cluster-detector.ts
1006
- function detectModuleClusters(graph, options) {
1007
- const domainMap = /* @__PURE__ */ new Map();
1008
- for (const [file, node] of graph.nodes.entries()) {
1009
- const primaryDomain = node.exports[0]?.inferredDomain || "unknown";
1010
- if (!domainMap.has(primaryDomain)) {
1011
- domainMap.set(primaryDomain, []);
1012
- }
1013
- domainMap.get(primaryDomain).push(file);
1014
- }
1015
- const clusters = [];
1016
- const generateSuggestedStructure = (files, tokens, fragmentation) => {
1017
- const targetFiles = Math.max(1, Math.ceil(tokens / 1e4));
1018
- const plan = [];
1019
- if (fragmentation > 0.5) {
1020
- plan.push(
1021
- `Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
1022
- );
1023
- }
1024
- if (tokens > 2e4) {
1025
- plan.push(
1026
- `Domain logic is very large (${Math.round(tokens / 1e3)}k tokens). Ensure clear sub-domain boundaries.`
1027
- );
1028
- }
1029
- return { targetFiles, consolidationPlan: plan };
1030
- };
1031
- for (const [domain, files] of domainMap.entries()) {
1032
- if (files.length < 2 || domain === "unknown") continue;
1033
- const totalTokens = files.reduce((sum, file) => {
1034
- const node = graph.nodes.get(file);
1035
- return sum + (node?.tokenCost || 0);
1036
- }, 0);
1037
- let sharedImportRatio = 0;
1038
- if (files.length >= 2) {
1039
- const allImportSets = files.map(
1040
- (f) => new Set(graph.nodes.get(f)?.imports || [])
1041
- );
1042
- let intersection = new Set(allImportSets[0]);
1043
- const union = new Set(allImportSets[0]);
1044
- for (let i = 1; i < allImportSets.length; i++) {
1045
- const nextSet = allImportSets[i];
1046
- intersection = new Set([...intersection].filter((x) => nextSet.has(x)));
1047
- for (const x of nextSet) union.add(x);
1048
- }
1049
- sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
1050
- }
1051
- const fragmentation = calculateFragmentation(files, domain, {
1052
- ...options,
1053
- sharedImportRatio
1054
- });
1055
- let totalCohesion = 0;
1056
- files.forEach((f) => {
1057
- const node = graph.nodes.get(f);
1058
- if (node) totalCohesion += calculateEnhancedCohesion(node.exports);
1059
- });
1060
- const avgCohesion = totalCohesion / files.length;
1061
- clusters.push({
1062
- domain,
1063
- files,
1064
- totalTokens,
1065
- fragmentationScore: fragmentation,
1066
- avgCohesion,
1067
- suggestedStructure: generateSuggestedStructure(
1068
- files,
1069
- totalTokens,
1070
- fragmentation
1071
- )
1072
- });
1073
- }
1074
- return clusters;
1075
- }
1076
-
1077
1006
  // src/heuristics.ts
1007
+ var BARREL_EXPORT_MIN_EXPORTS = 5;
1008
+ var BARREL_EXPORT_TOKEN_LIMIT = 1e3;
1009
+ var HANDLER_NAME_PATTERNS = [
1010
+ "handler",
1011
+ ".handler.",
1012
+ "-handler.",
1013
+ "lambda",
1014
+ ".lambda.",
1015
+ "-lambda."
1016
+ ];
1017
+ var SERVICE_NAME_PATTERNS = [
1018
+ "service",
1019
+ ".service.",
1020
+ "-service.",
1021
+ "_service."
1022
+ ];
1023
+ var EMAIL_NAME_PATTERNS = [
1024
+ "-email-",
1025
+ ".email.",
1026
+ "_email_",
1027
+ "-template",
1028
+ ".template.",
1029
+ "_template",
1030
+ "-mail.",
1031
+ ".mail."
1032
+ ];
1033
+ var PARSER_NAME_PATTERNS = [
1034
+ "parser",
1035
+ ".parser.",
1036
+ "-parser.",
1037
+ "_parser.",
1038
+ "transform",
1039
+ "converter",
1040
+ "mapper",
1041
+ "serializer"
1042
+ ];
1043
+ var SESSION_NAME_PATTERNS = ["session", "state", "context", "store"];
1044
+ var NEXTJS_METADATA_EXPORTS = [
1045
+ "metadata",
1046
+ "generatemetadata",
1047
+ "faqjsonld",
1048
+ "jsonld",
1049
+ "icon"
1050
+ ];
1051
+ var CONFIG_NAME_PATTERNS = [
1052
+ ".config.",
1053
+ "tsconfig",
1054
+ "jest.config",
1055
+ "package.json",
1056
+ "aiready.json",
1057
+ "next.config",
1058
+ "sst.config"
1059
+ ];
1078
1060
  function isBarrelExport(node) {
1079
1061
  const { file, exports: exports2 } = node;
1080
1062
  const fileName = file.split("/").pop()?.toLowerCase();
1081
1063
  const isIndexFile = fileName === "index.ts" || fileName === "index.js";
1082
- const isSmallAndManyExports = node.tokenCost < 1e3 && (exports2 || []).length > 5;
1083
- const isReexportPattern = (exports2 || []).length >= 5 && (exports2 || []).every(
1084
- (e) => ["const", "function", "type", "interface"].includes(e.type)
1064
+ const isSmallAndManyExports = node.tokenCost < BARREL_EXPORT_TOKEN_LIMIT && (exports2 || []).length > BARREL_EXPORT_MIN_EXPORTS;
1065
+ const isReexportPattern = (exports2 || []).length >= BARREL_EXPORT_MIN_EXPORTS && (exports2 || []).every(
1066
+ (exp) => ["const", "function", "type", "interface"].includes(exp.type)
1085
1067
  );
1086
1068
  return !!isIndexFile || !!isSmallAndManyExports || !!isReexportPattern;
1087
1069
  }
@@ -1090,7 +1072,9 @@ function isTypeDefinition(node) {
1090
1072
  if (file.endsWith(".d.ts")) return true;
1091
1073
  const nodeExports = node.exports || [];
1092
1074
  const hasExports = nodeExports.length > 0;
1093
- const areAllTypes = hasExports && nodeExports.every((e) => e.type === "type" || e.type === "interface");
1075
+ const areAllTypes = hasExports && nodeExports.every(
1076
+ (exp) => exp.type === "type" || exp.type === "interface"
1077
+ );
1094
1078
  const isTypePath = /\/(types|interfaces|models)\//i.test(file);
1095
1079
  return !!areAllTypes || isTypePath && hasExports;
1096
1080
  }
@@ -1104,81 +1088,65 @@ function isUtilityModule(node) {
1104
1088
  function isLambdaHandler(node) {
1105
1089
  const { file, exports: exports2 } = node;
1106
1090
  const fileName = file.split("/").pop()?.toLowerCase() || "";
1107
- const handlerPatterns = [
1108
- "handler",
1109
- ".handler.",
1110
- "-handler.",
1111
- "lambda",
1112
- ".lambda.",
1113
- "-lambda."
1114
- ];
1115
- const isHandlerName = handlerPatterns.some((p) => fileName.includes(p));
1091
+ const isHandlerName = HANDLER_NAME_PATTERNS.some(
1092
+ (pattern) => fileName.includes(pattern)
1093
+ );
1116
1094
  const isHandlerPath = /\/(handlers|lambdas|lambda|functions)\//i.test(file);
1117
1095
  const hasHandlerExport = (exports2 || []).some(
1118
- (e) => ["handler", "main", "lambdahandler"].includes(e.name.toLowerCase()) || e.name.toLowerCase().endsWith("handler")
1096
+ (exp) => ["handler", "main", "lambdahandler"].includes(exp.name.toLowerCase()) || exp.name.toLowerCase().endsWith("handler")
1119
1097
  );
1120
1098
  return isHandlerName || isHandlerPath || hasHandlerExport;
1121
1099
  }
1122
1100
  function isServiceFile(node) {
1123
1101
  const { file, exports: exports2 } = node;
1124
1102
  const fileName = file.split("/").pop()?.toLowerCase() || "";
1125
- const servicePatterns = ["service", ".service.", "-service.", "_service."];
1126
- const isServiceName = servicePatterns.some((p) => fileName.includes(p));
1103
+ const isServiceName = SERVICE_NAME_PATTERNS.some(
1104
+ (pattern) => fileName.includes(pattern)
1105
+ );
1127
1106
  const isServicePath = file.toLowerCase().includes("/services/");
1128
1107
  const hasServiceNamedExport = (exports2 || []).some(
1129
- (e) => e.name.toLowerCase().includes("service")
1108
+ (exp) => exp.name.toLowerCase().includes("service")
1109
+ );
1110
+ const hasClassExport = (exports2 || []).some(
1111
+ (exp) => exp.type === "class"
1130
1112
  );
1131
- const hasClassExport = (exports2 || []).some((e) => e.type === "class");
1132
1113
  return isServiceName || isServicePath || hasServiceNamedExport && hasClassExport;
1133
1114
  }
1134
1115
  function isEmailTemplate(node) {
1135
1116
  const { file, exports: exports2 } = node;
1136
1117
  const fileName = file.split("/").pop()?.toLowerCase() || "";
1137
- const emailPatterns = [
1138
- "-email-",
1139
- ".email.",
1140
- "_email_",
1141
- "-template",
1142
- ".template.",
1143
- "_template",
1144
- "-mail.",
1145
- ".mail."
1146
- ];
1147
- const isEmailName = emailPatterns.some((p) => fileName.includes(p));
1118
+ const isEmailName = EMAIL_NAME_PATTERNS.some(
1119
+ (pattern) => fileName.includes(pattern)
1120
+ );
1148
1121
  const isEmailPath = /\/(emails|mail|notifications)\//i.test(file);
1149
1122
  const hasTemplateFunction = (exports2 || []).some(
1150
- (e) => e.type === "function" && (e.name.toLowerCase().startsWith("render") || e.name.toLowerCase().startsWith("generate"))
1123
+ (exp) => exp.type === "function" && (exp.name.toLowerCase().startsWith("render") || exp.name.toLowerCase().startsWith("generate"))
1151
1124
  );
1152
1125
  return isEmailPath || isEmailName || hasTemplateFunction;
1153
1126
  }
1154
1127
  function isParserFile(node) {
1155
1128
  const { file, exports: exports2 } = node;
1156
1129
  const fileName = file.split("/").pop()?.toLowerCase() || "";
1157
- const parserPatterns = [
1158
- "parser",
1159
- ".parser.",
1160
- "-parser.",
1161
- "_parser.",
1162
- "transform",
1163
- "converter",
1164
- "mapper",
1165
- "serializer"
1166
- ];
1167
- const isParserName = parserPatterns.some((p) => fileName.includes(p));
1130
+ const isParserName = PARSER_NAME_PATTERNS.some(
1131
+ (pattern) => fileName.includes(pattern)
1132
+ );
1168
1133
  const isParserPath = /\/(parsers|transformers)\//i.test(file);
1169
1134
  const hasParseFunction = (exports2 || []).some(
1170
- (e) => e.type === "function" && (e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform"))
1135
+ (exp) => exp.type === "function" && (exp.name.toLowerCase().startsWith("parse") || exp.name.toLowerCase().startsWith("transform"))
1171
1136
  );
1172
1137
  return isParserName || isParserPath || hasParseFunction;
1173
1138
  }
1174
1139
  function isSessionFile(node) {
1175
1140
  const { file, exports: exports2 } = node;
1176
1141
  const fileName = file.split("/").pop()?.toLowerCase() || "";
1177
- const sessionPatterns = ["session", "state", "context", "store"];
1178
- const isSessionName = sessionPatterns.some((p) => fileName.includes(p));
1142
+ const isSessionName = SESSION_NAME_PATTERNS.some(
1143
+ (pattern) => fileName.includes(pattern)
1144
+ );
1179
1145
  const isSessionPath = /\/(sessions|state)\//i.test(file);
1180
1146
  const hasSessionExport = (exports2 || []).some(
1181
- (e) => ["session", "state", "store"].some((p) => e.name.toLowerCase().includes(p))
1147
+ (exp) => ["session", "state", "store"].some(
1148
+ (pattern) => exp.name.toLowerCase().includes(pattern)
1149
+ )
1182
1150
  );
1183
1151
  return isSessionName || isSessionPath || hasSessionExport;
1184
1152
  }
@@ -1189,16 +1157,11 @@ function isNextJsPage(node) {
1189
1157
  const isInAppDir = lowerPath.includes("/app/") || lowerPath.startsWith("app/");
1190
1158
  if (!isInAppDir || fileName !== "page.tsx" && fileName !== "page.ts")
1191
1159
  return false;
1192
- const hasDefaultExport = (exports2 || []).some((e) => e.type === "default");
1193
- const nextJsExports = [
1194
- "metadata",
1195
- "generatemetadata",
1196
- "faqjsonld",
1197
- "jsonld",
1198
- "icon"
1199
- ];
1160
+ const hasDefaultExport = (exports2 || []).some(
1161
+ (exp) => exp.type === "default"
1162
+ );
1200
1163
  const hasNextJsExport = (exports2 || []).some(
1201
- (e) => nextJsExports.includes(e.name.toLowerCase())
1164
+ (exp) => NEXTJS_METADATA_EXPORTS.includes(exp.name.toLowerCase())
1202
1165
  );
1203
1166
  return hasDefaultExport || hasNextJsExport;
1204
1167
  }
@@ -1206,24 +1169,21 @@ function isConfigFile(node) {
1206
1169
  const { file, exports: exports2 } = node;
1207
1170
  const lowerPath = file.toLowerCase();
1208
1171
  const fileName = file.split("/").pop()?.toLowerCase() || "";
1209
- const configPatterns = [
1210
- ".config.",
1211
- "tsconfig",
1212
- "jest.config",
1213
- "package.json",
1214
- "aiready.json",
1215
- "next.config",
1216
- "sst.config"
1217
- ];
1218
- const isConfigName = configPatterns.some((p) => fileName.includes(p));
1172
+ const isConfigName = CONFIG_NAME_PATTERNS.some(
1173
+ (pattern) => fileName.includes(pattern)
1174
+ );
1219
1175
  const isConfigPath = /\/(config|settings|schemas)\//i.test(lowerPath);
1220
1176
  const hasSchemaExport = (exports2 || []).some(
1221
- (e) => ["schema", "config", "setting"].some(
1222
- (p) => e.name.toLowerCase().includes(p)
1177
+ (exp) => ["schema", "config", "setting"].some(
1178
+ (pattern) => exp.name.toLowerCase().includes(pattern)
1223
1179
  )
1224
1180
  );
1225
1181
  return isConfigName || isConfigPath || hasSchemaExport;
1226
1182
  }
1183
+ function isHubAndSpokeFile(node) {
1184
+ const { file } = node;
1185
+ return /\/packages\/[a-zA-Z0-9-]+\/src\//.test(file);
1186
+ }
1227
1187
 
1228
1188
  // src/classifier.ts
1229
1189
  var Classification = {
@@ -1236,6 +1196,7 @@ var Classification = {
1236
1196
  PARSER: "parser-file",
1237
1197
  COHESIVE_MODULE: "cohesive-module",
1238
1198
  UTILITY_MODULE: "utility-module",
1199
+ SPOKE_MODULE: "spoke-module",
1239
1200
  MIXED_CONCERNS: "mixed-concerns",
1240
1201
  UNKNOWN: "unknown"
1241
1202
  };
@@ -1272,6 +1233,9 @@ function classifyFile(node, cohesionScore = 1, domains = []) {
1272
1233
  if (isConfigFile(node)) {
1273
1234
  return Classification.COHESIVE_MODULE;
1274
1235
  }
1236
+ if (isHubAndSpokeFile(node)) {
1237
+ return Classification.SPOKE_MODULE;
1238
+ }
1275
1239
  if (domains.length <= 1 && domains[0] !== "unknown") {
1276
1240
  return Classification.COHESIVE_MODULE;
1277
1241
  }
@@ -1307,6 +1271,8 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
1307
1271
  return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
1308
1272
  case Classification.PARSER:
1309
1273
  return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
1274
+ case Classification.SPOKE_MODULE:
1275
+ return Math.max(baseCohesion, 0.6);
1310
1276
  case Classification.COHESIVE_MODULE:
1311
1277
  return Math.max(baseCohesion, 0.7);
1312
1278
  case Classification.MIXED_CONCERNS:
@@ -1369,6 +1335,9 @@ function adjustFragmentationForClassification(baseFragmentation, classification)
1369
1335
  case Classification.PARSER:
1370
1336
  case Classification.NEXTJS_PAGE:
1371
1337
  return baseFragmentation * 0.2;
1338
+ case Classification.SPOKE_MODULE:
1339
+ return baseFragmentation * 0.15;
1340
+ // Heavily discount intentional monorepo separation
1372
1341
  case Classification.COHESIVE_MODULE:
1373
1342
  return baseFragmentation * 0.3;
1374
1343
  case Classification.MIXED_CONCERNS:
@@ -1378,6 +1347,88 @@ function adjustFragmentationForClassification(baseFragmentation, classification)
1378
1347
  }
1379
1348
  }
1380
1349
 
1350
+ // src/cluster-detector.ts
1351
+ function detectModuleClusters(graph, options) {
1352
+ const domainMap = /* @__PURE__ */ new Map();
1353
+ for (const [file, node] of graph.nodes.entries()) {
1354
+ const primaryDomain = node.exports[0]?.inferredDomain || "unknown";
1355
+ if (!domainMap.has(primaryDomain)) {
1356
+ domainMap.set(primaryDomain, []);
1357
+ }
1358
+ domainMap.get(primaryDomain).push(file);
1359
+ }
1360
+ const clusters = [];
1361
+ const generateSuggestedStructure = (files, tokens, fragmentation) => {
1362
+ const targetFiles = Math.max(1, Math.ceil(tokens / 1e4));
1363
+ const plan = [];
1364
+ if (fragmentation > 0.5) {
1365
+ plan.push(
1366
+ `Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
1367
+ );
1368
+ }
1369
+ if (tokens > 2e4) {
1370
+ plan.push(
1371
+ `Domain logic is very large (${Math.round(tokens / 1e3)}k tokens). Ensure clear sub-domain boundaries.`
1372
+ );
1373
+ }
1374
+ return { targetFiles, consolidationPlan: plan };
1375
+ };
1376
+ for (const [domain, files] of domainMap.entries()) {
1377
+ if (files.length < 2 || domain === "unknown") continue;
1378
+ const totalTokens = files.reduce((sum, file) => {
1379
+ const node = graph.nodes.get(file);
1380
+ return sum + (node?.tokenCost || 0);
1381
+ }, 0);
1382
+ let sharedImportRatio = 0;
1383
+ if (files.length >= 2) {
1384
+ const allImportSets = files.map(
1385
+ (f) => new Set(graph.nodes.get(f)?.imports || [])
1386
+ );
1387
+ let intersection = new Set(allImportSets[0]);
1388
+ const union = new Set(allImportSets[0]);
1389
+ for (let i = 1; i < allImportSets.length; i++) {
1390
+ const nextSet = allImportSets[i];
1391
+ intersection = new Set([...intersection].filter((x) => nextSet.has(x)));
1392
+ for (const x of nextSet) union.add(x);
1393
+ }
1394
+ sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
1395
+ }
1396
+ const rawFragmentation = calculateFragmentation(files, domain, {
1397
+ ...options,
1398
+ sharedImportRatio
1399
+ });
1400
+ let totalCohesion = 0;
1401
+ let totalAdjustedFragmentation = 0;
1402
+ files.forEach((f) => {
1403
+ const node = graph.nodes.get(f);
1404
+ if (node) {
1405
+ const cohesion = calculateEnhancedCohesion(node.exports);
1406
+ totalCohesion += cohesion;
1407
+ const classification = classifyFile(node, cohesion);
1408
+ totalAdjustedFragmentation += adjustFragmentationForClassification(
1409
+ rawFragmentation,
1410
+ classification
1411
+ );
1412
+ }
1413
+ });
1414
+ const avgCohesion = totalCohesion / files.length;
1415
+ const fragmentationScore = totalAdjustedFragmentation / files.length;
1416
+ clusters.push({
1417
+ domain,
1418
+ files,
1419
+ totalTokens,
1420
+ fragmentationScore,
1421
+ avgCohesion,
1422
+ suggestedStructure: generateSuggestedStructure(
1423
+ files,
1424
+ totalTokens,
1425
+ fragmentationScore
1426
+ )
1427
+ });
1428
+ }
1429
+ return clusters;
1430
+ }
1431
+
1381
1432
  // src/remediation.ts
1382
1433
  function getClassificationRecommendations(classification, file, issues) {
1383
1434
  switch (classification) {
@@ -1426,6 +1477,11 @@ function getClassificationRecommendations(classification, file, issues) {
1426
1477
  "Next.js App Router page detected - metadata/JSON-LD/component pattern is cohesive",
1427
1478
  "Multiple exports (metadata, faqJsonLd, default) serve single page purpose"
1428
1479
  ];
1480
+ case "spoke-module":
1481
+ return [
1482
+ "Spoke module detected - intentional monorepo separation is good for modularity",
1483
+ "Ensure this spoke only exports what is necessary for the hub or other spokes"
1484
+ ];
1429
1485
  case "mixed-concerns":
1430
1486
  return [
1431
1487
  "Consider splitting this file by domain",
@@ -1482,7 +1538,79 @@ function getGeneralRecommendations(metrics, thresholds) {
1482
1538
  return { recommendations, issues, severity };
1483
1539
  }
1484
1540
 
1485
- // src/analyzer.ts
1541
+ // src/mapper.ts
1542
+ function mapNodeToResult(node, graph, clusters, allCircularDeps, options) {
1543
+ const file = node.file;
1544
+ const tokenCost = node.tokenCost;
1545
+ const importDepth = calculateImportDepth(file, graph);
1546
+ const transitiveDeps = getTransitiveDependencies(file, graph);
1547
+ const contextBudget = calculateContextBudget(file, graph);
1548
+ const circularDeps = allCircularDeps.filter((cycle) => cycle.includes(file));
1549
+ const cluster = clusters.find((c) => c.files.includes(file));
1550
+ const rawFragmentationScore = cluster ? cluster.fragmentationScore : 0;
1551
+ const rawCohesionScore = calculateEnhancedCohesion(
1552
+ node.exports,
1553
+ file,
1554
+ options
1555
+ );
1556
+ const fileClassification = classifyFile(node, rawCohesionScore);
1557
+ const cohesionScore = adjustCohesionForClassification(
1558
+ rawCohesionScore,
1559
+ fileClassification
1560
+ );
1561
+ const fragmentationScore = adjustFragmentationForClassification(
1562
+ rawFragmentationScore,
1563
+ fileClassification
1564
+ );
1565
+ const { severity, issues, recommendations, potentialSavings } = analyzeIssues(
1566
+ {
1567
+ file,
1568
+ importDepth,
1569
+ contextBudget,
1570
+ cohesionScore,
1571
+ fragmentationScore,
1572
+ maxDepth: options.maxDepth,
1573
+ maxContextBudget: options.maxContextBudget,
1574
+ minCohesion: options.minCohesion,
1575
+ maxFragmentation: options.maxFragmentation,
1576
+ circularDeps
1577
+ }
1578
+ );
1579
+ const classRecs = getClassificationRecommendations(
1580
+ fileClassification,
1581
+ file,
1582
+ issues
1583
+ );
1584
+ const allRecommendations = Array.from(
1585
+ /* @__PURE__ */ new Set([...recommendations, ...classRecs])
1586
+ );
1587
+ return {
1588
+ file,
1589
+ tokenCost,
1590
+ linesOfCode: node.linesOfCode,
1591
+ importDepth,
1592
+ dependencyCount: transitiveDeps.length,
1593
+ dependencyList: transitiveDeps,
1594
+ circularDeps,
1595
+ cohesionScore,
1596
+ domains: Array.from(
1597
+ new Set(
1598
+ node.exports.flatMap((e) => e.domains?.map((d) => d.domain) || [])
1599
+ )
1600
+ ),
1601
+ exportCount: node.exports.length,
1602
+ contextBudget,
1603
+ fragmentationScore,
1604
+ relatedFiles: cluster ? cluster.files : [],
1605
+ fileClassification,
1606
+ severity,
1607
+ issues,
1608
+ recommendations: allRecommendations,
1609
+ potentialSavings
1610
+ };
1611
+ }
1612
+
1613
+ // src/orchestrator.ts
1486
1614
  function calculateCohesion(exports2, filePath, options) {
1487
1615
  return calculateEnhancedCohesion(exports2, filePath, options);
1488
1616
  }
@@ -1535,10 +1663,8 @@ async function analyzeContext(options) {
1535
1663
  file: metric.file,
1536
1664
  tokenCost: 0,
1537
1665
  linesOfCode: 0,
1538
- // Not provided by python context yet
1539
1666
  importDepth: metric.importDepth,
1540
1667
  dependencyCount: 0,
1541
- // Not provided
1542
1668
  dependencyList: [],
1543
1669
  circularDeps: [],
1544
1670
  cohesionScore: metric.cohesion,
@@ -1558,76 +1684,12 @@ async function analyzeContext(options) {
1558
1684
  const clusters = detectModuleClusters(graph);
1559
1685
  const allCircularDeps = detectCircularDependencies(graph);
1560
1686
  const results = Array.from(graph.nodes.values()).map(
1561
- (node) => {
1562
- const file = node.file;
1563
- const tokenCost = node.tokenCost;
1564
- const importDepth = calculateImportDepth(file, graph);
1565
- const transitiveDeps = getTransitiveDependencies(file, graph);
1566
- const contextBudget = calculateContextBudget(file, graph);
1567
- const circularDeps = allCircularDeps.filter(
1568
- (cycle) => cycle.includes(file)
1569
- );
1570
- const cluster = clusters.find((c) => c.files.includes(file));
1571
- const rawFragmentationScore = cluster ? cluster.fragmentationScore : 0;
1572
- const rawCohesionScore = calculateEnhancedCohesion(
1573
- node.exports,
1574
- file,
1575
- options
1576
- );
1577
- const fileClassification = classifyFile(node, rawCohesionScore);
1578
- const cohesionScore = adjustCohesionForClassification(
1579
- rawCohesionScore,
1580
- fileClassification
1581
- );
1582
- const fragmentationScore = adjustFragmentationForClassification(
1583
- rawFragmentationScore,
1584
- fileClassification
1585
- );
1586
- const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
1587
- file,
1588
- importDepth,
1589
- contextBudget,
1590
- cohesionScore,
1591
- fragmentationScore,
1592
- maxDepth,
1593
- maxContextBudget,
1594
- minCohesion,
1595
- maxFragmentation,
1596
- circularDeps
1597
- });
1598
- const classRecs = getClassificationRecommendations(
1599
- fileClassification,
1600
- file,
1601
- issues
1602
- );
1603
- const allRecommendations = Array.from(
1604
- /* @__PURE__ */ new Set([...recommendations, ...classRecs])
1605
- );
1606
- return {
1607
- file,
1608
- tokenCost,
1609
- linesOfCode: node.linesOfCode,
1610
- importDepth,
1611
- dependencyCount: transitiveDeps.length,
1612
- dependencyList: transitiveDeps,
1613
- circularDeps,
1614
- cohesionScore,
1615
- domains: Array.from(
1616
- new Set(
1617
- node.exports.flatMap((e) => e.domains?.map((d) => d.domain) || [])
1618
- )
1619
- ),
1620
- exportCount: node.exports.length,
1621
- contextBudget,
1622
- fragmentationScore,
1623
- relatedFiles: cluster ? cluster.files : [],
1624
- fileClassification,
1625
- severity,
1626
- issues,
1627
- recommendations: allRecommendations,
1628
- potentialSavings
1629
- };
1630
- }
1687
+ (node) => mapNodeToResult(node, graph, clusters, allCircularDeps, {
1688
+ maxDepth,
1689
+ maxContextBudget,
1690
+ minCohesion,
1691
+ maxFragmentation
1692
+ })
1631
1693
  );
1632
1694
  return [...results, ...pythonResults];
1633
1695
  }