@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/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);
@@ -904,6 +904,9 @@ function classifyFile(node, cohesionScore, domains) {
904
904
  if (isLambdaHandler(node)) {
905
905
  return "lambda-handler";
906
906
  }
907
+ if (isDataAccessFile(node)) {
908
+ return "cohesive-module";
909
+ }
907
910
  if (isEmailTemplate(node)) {
908
911
  return "email-template";
909
912
  }
@@ -922,11 +925,17 @@ function classifyFile(node, cohesionScore, domains) {
922
925
  if (isUtilityFile(node)) {
923
926
  return "utility-module";
924
927
  }
928
+ if (file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/")) {
929
+ return "utility-module";
930
+ }
925
931
  const uniqueDomains = domains.filter((d) => d !== "unknown");
926
932
  const hasSingleDomain = uniqueDomains.length <= 1;
927
933
  if (hasSingleDomain) {
928
934
  return "cohesive-module";
929
935
  }
936
+ if (allExportsShareEntityNoun(exports2)) {
937
+ return "cohesive-module";
938
+ }
930
939
  const hasMultipleDomains = uniqueDomains.length > 1;
931
940
  const hasLowCohesion = cohesionScore < 0.4;
932
941
  if (hasMultipleDomains && hasLowCohesion) {
@@ -1018,6 +1027,107 @@ function isUtilityFile(node) {
1018
1027
  const hasManySmallExportsInUtilityContext = exports2.length >= 3 && exports2.every((e) => e.type === "function" || e.type === "const") && (isUtilityName || isUtilityPath);
1019
1028
  return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
1020
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
+ }
1021
1131
  function isLambdaHandler(node) {
1022
1132
  const { file, exports: exports2 } = node;
1023
1133
  const fileName = file.split("/").pop()?.toLowerCase();
@@ -1032,7 +1142,7 @@ function isLambdaHandler(node) {
1032
1142
  const isHandlerName = handlerPatterns.some(
1033
1143
  (pattern) => fileName?.includes(pattern)
1034
1144
  );
1035
- const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/functions/");
1145
+ const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/lambda/") || file.toLowerCase().includes("/functions/");
1036
1146
  const hasHandlerExport = exports2.some(
1037
1147
  (e) => e.name.toLowerCase() === "handler" || e.name.toLowerCase() === "main" || e.name.toLowerCase() === "lambdahandler" || e.name.toLowerCase().endsWith("handler")
1038
1148
  );
@@ -1172,25 +1282,25 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
1172
1282
  const exportNames = node.exports.map((e) => e.name.toLowerCase());
1173
1283
  const hasRelatedNames = hasRelatedExportNames(exportNames);
1174
1284
  if (hasRelatedNames) {
1175
- return Math.min(1, baseCohesion + 0.45);
1285
+ return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
1176
1286
  }
1177
1287
  }
1178
- return Math.min(1, baseCohesion + 0.35);
1288
+ return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
1179
1289
  }
1180
1290
  case "service-file": {
1181
1291
  if (node?.exports.some((e) => e.type === "class")) {
1182
- return Math.min(1, baseCohesion + 0.4);
1292
+ return Math.max(0.78, Math.min(1, baseCohesion + 0.4));
1183
1293
  }
1184
- return Math.min(1, baseCohesion + 0.3);
1294
+ return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
1185
1295
  }
1186
1296
  case "lambda-handler": {
1187
1297
  if (node) {
1188
1298
  const hasSingleEntry = node.exports.length === 1 || node.exports.some((e) => e.name.toLowerCase() === "handler");
1189
1299
  if (hasSingleEntry) {
1190
- return Math.min(1, baseCohesion + 0.45);
1300
+ return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
1191
1301
  }
1192
1302
  }
1193
- return Math.min(1, baseCohesion + 0.35);
1303
+ return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
1194
1304
  }
1195
1305
  case "email-template": {
1196
1306
  if (node) {
@@ -1198,10 +1308,10 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
1198
1308
  (e) => e.name.toLowerCase().includes("render") || e.name.toLowerCase().includes("generate") || e.name.toLowerCase().includes("template")
1199
1309
  );
1200
1310
  if (hasTemplateFunc) {
1201
- return Math.min(1, baseCohesion + 0.4);
1311
+ return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
1202
1312
  }
1203
1313
  }
1204
- return Math.min(1, baseCohesion + 0.3);
1314
+ return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
1205
1315
  }
1206
1316
  case "parser-file": {
1207
1317
  if (node) {
@@ -1209,10 +1319,10 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
1209
1319
  (e) => e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert")
1210
1320
  );
1211
1321
  if (hasParseFunc) {
1212
- return Math.min(1, baseCohesion + 0.4);
1322
+ return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
1213
1323
  }
1214
1324
  }
1215
- return Math.min(1, baseCohesion + 0.3);
1325
+ return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
1216
1326
  }
1217
1327
  case "nextjs-page":
1218
1328
  return 1;
@@ -1252,6 +1362,54 @@ function hasRelatedExportNames(exportNames) {
1252
1362
  const uniquePrefixes = new Set(prefixes);
1253
1363
  if (uniquePrefixes.size === 1) return true;
1254
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
+ }
1255
1413
  return false;
1256
1414
  }
1257
1415
  function adjustFragmentationForClassification(baseFragmentation, classification) {
@@ -1333,6 +1491,9 @@ function getClassificationRecommendations(classification, file, issues) {
1333
1491
  }
1334
1492
  }
1335
1493
 
1494
+ // src/scoring.ts
1495
+ var import_core2 = require("@aiready/core");
1496
+
1336
1497
  // src/index.ts
1337
1498
  async function analyzeContext(options) {
1338
1499
  const {
@@ -1344,7 +1505,7 @@ async function analyzeContext(options) {
1344
1505
  includeNodeModules = false,
1345
1506
  ...scanOptions
1346
1507
  } = options;
1347
- const files = await (0, import_core3.scanFiles)({
1508
+ const files = await (0, import_core4.scanFiles)({
1348
1509
  ...scanOptions,
1349
1510
  // Only add node_modules to exclude if includeNodeModules is false
1350
1511
  // The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
@@ -1356,7 +1517,7 @@ async function analyzeContext(options) {
1356
1517
  const fileContents = await Promise.all(
1357
1518
  files.map(async (file) => ({
1358
1519
  file,
1359
- content: await (0, import_core3.readFileContent)(file)
1520
+ content: await (0, import_core4.readFileContent)(file)
1360
1521
  }))
1361
1522
  );
1362
1523
  const graph = buildDependencyGraph(fileContents.filter((f) => !f.file.toLowerCase().endsWith(".py")));
@@ -1419,7 +1580,7 @@ async function analyzeContext(options) {
1419
1580
  const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
1420
1581
  const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
1421
1582
  const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
1422
- 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;
1423
1584
  const fragmentationScore = fragmentationMap.get(file) || 0;
1424
1585
  const relatedFiles = [];
1425
1586
  for (const cluster of clusters) {
@@ -1728,7 +1889,7 @@ function downgradeSeverity(s) {
1728
1889
  var import_chalk = __toESM(require("chalk"));
1729
1890
  var import_fs = require("fs");
1730
1891
  var import_path2 = require("path");
1731
- var import_core4 = require("@aiready/core");
1892
+ var import_core5 = require("@aiready/core");
1732
1893
  var import_prompts = __toESM(require("prompts"));
1733
1894
  var program = new import_commander.Command();
1734
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(
@@ -1759,7 +1920,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
1759
1920
  exclude: void 0,
1760
1921
  maxResults: 10
1761
1922
  };
1762
- let finalOptions = await (0, import_core4.loadMergedConfig)(directory, defaults, {
1923
+ let finalOptions = await (0, import_core5.loadMergedConfig)(directory, defaults, {
1763
1924
  maxDepth: options.maxDepth ? parseInt(options.maxDepth) : void 0,
1764
1925
  maxContextBudget: options.maxContext ? parseInt(options.maxContext) : void 0,
1765
1926
  minCohesion: options.minCohesion ? parseFloat(options.minCohesion) : void 0,
@@ -1774,7 +1935,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
1774
1935
  finalOptions = await runInteractiveSetup(directory, finalOptions);
1775
1936
  }
1776
1937
  const results = await analyzeContext(finalOptions);
1777
- const elapsedTime = (0, import_core4.getElapsedTime)(startTime);
1938
+ const elapsedTime = (0, import_core5.getElapsedTime)(startTime);
1778
1939
  const summary = generateSummary(results);
1779
1940
  if (options.output === "json") {
1780
1941
  const jsonOutput = {
@@ -1783,18 +1944,18 @@ program.name("aiready-context").description("Analyze AI context window cost and
1783
1944
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1784
1945
  analysisTime: elapsedTime
1785
1946
  };
1786
- const outputPath = (0, import_core4.resolveOutputPath)(
1947
+ const outputPath = (0, import_core5.resolveOutputPath)(
1787
1948
  options.outputFile,
1788
1949
  `context-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
1789
1950
  directory
1790
1951
  );
1791
- (0, import_core4.handleJSONOutput)(jsonOutput, outputPath, `
1952
+ (0, import_core5.handleJSONOutput)(jsonOutput, outputPath, `
1792
1953
  \u2713 JSON report saved to ${outputPath}`);
1793
1954
  return;
1794
1955
  }
1795
1956
  if (options.output === "html") {
1796
1957
  const html = generateHTMLReport(summary, results);
1797
- const outputPath = (0, import_core4.resolveOutputPath)(
1958
+ const outputPath = (0, import_core5.resolveOutputPath)(
1798
1959
  options.outputFile,
1799
1960
  `context-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.html`,
1800
1961
  directory
@@ -1811,7 +1972,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
1811
1972
  displayConsoleReport(summary, results, elapsedTime, finalOptions.maxResults);
1812
1973
  displayTuningGuidance(results, finalOptions);
1813
1974
  } catch (error) {
1814
- (0, import_core4.handleCLIError)(error, "Analysis");
1975
+ (0, import_core5.handleCLIError)(error, "Analysis");
1815
1976
  }
1816
1977
  });
1817
1978
  program.parse();
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  analyzeContext,
4
4
  generateSummary
5
- } from "./chunk-PJD4VCIH.mjs";
5
+ } from "./chunk-M64RHH4D.mjs";
6
6
  import "./chunk-Y6FXYEAI.mjs";
7
7
 
8
8
  // src/cli.ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ScanOptions, ToolScoringOutput } from '@aiready/core';
1
+ import { ScanOptions, CostConfig, ToolScoringOutput } from '@aiready/core';
2
2
 
3
3
  interface ContextAnalyzerOptions extends ScanOptions {
4
4
  maxDepth?: number;
@@ -158,8 +158,12 @@ declare function adjustFragmentationForClassification(baseFragmentation: number,
158
158
  * - Import depth (dependency chain length)
159
159
  * - Fragmentation score (code organization)
160
160
  * - Critical/major issues
161
+ *
162
+ * Includes business value metrics:
163
+ * - Estimated monthly cost of context waste
164
+ * - Estimated developer hours to fix
161
165
  */
162
- declare function calculateContextScore(summary: ContextSummary): ToolScoringOutput;
166
+ declare function calculateContextScore(summary: ContextSummary, costConfig?: Partial<CostConfig>): ToolScoringOutput;
163
167
 
164
168
  /**
165
169
  * Build co-usage matrix: track which files are imported together
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ScanOptions, ToolScoringOutput } from '@aiready/core';
1
+ import { ScanOptions, CostConfig, ToolScoringOutput } from '@aiready/core';
2
2
 
3
3
  interface ContextAnalyzerOptions extends ScanOptions {
4
4
  maxDepth?: number;
@@ -158,8 +158,12 @@ declare function adjustFragmentationForClassification(baseFragmentation: number,
158
158
  * - Import depth (dependency chain length)
159
159
  * - Fragmentation score (code organization)
160
160
  * - Critical/major issues
161
+ *
162
+ * Includes business value metrics:
163
+ * - Estimated monthly cost of context waste
164
+ * - Estimated developer hours to fix
161
165
  */
162
- declare function calculateContextScore(summary: ContextSummary): ToolScoringOutput;
166
+ declare function calculateContextScore(summary: ContextSummary, costConfig?: Partial<CostConfig>): ToolScoringOutput;
163
167
 
164
168
  /**
165
169
  * Build co-usage matrix: track which files are imported together