@aiready/context-analyzer 0.5.1 → 0.5.3

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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  
3
- > @aiready/context-analyzer@0.5.1 build /Users/pengcao/projects/aiready/packages/context-analyzer
3
+ > @aiready/context-analyzer@0.5.3 build /Users/pengcao/projects/aiready/packages/context-analyzer
4
4
  > tsup src/index.ts src/cli.ts --format cjs,esm --dts
5
5
 
6
6
  CLI Building entry: src/cli.ts, src/index.ts
@@ -9,15 +9,15 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
- CJS dist/index.js 20.62 KB
13
- CJS dist/cli.js 39.27 KB
14
- CJS ⚡️ Build success in 48ms
15
- ESM dist/index.mjs 164.00 B
12
+ CJS dist/cli.js 39.84 KB
13
+ CJS dist/index.js 21.19 KB
14
+ CJS ⚡️ Build success in 57ms
16
15
  ESM dist/cli.mjs 18.45 KB
17
- ESM dist/chunk-NJUW6VED.mjs 19.48 KB
18
- ESM ⚡️ Build success in 48ms
16
+ ESM dist/chunk-EX7HCWAO.mjs 20.05 KB
17
+ ESM dist/index.mjs 164.00 B
18
+ ESM ⚡️ Build success in 57ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 547ms
20
+ DTS ⚡️ Build success in 529ms
21
21
  DTS dist/cli.d.ts 20.00 B
22
22
  DTS dist/index.d.ts 2.44 KB
23
23
  DTS dist/cli.d.mts 20.00 B
@@ -1,12 +1,12 @@
1
1
 
2
2
  
3
- > @aiready/context-analyzer@0.5.1 test /Users/pengcao/projects/aiready/packages/context-analyzer
3
+ > @aiready/context-analyzer@0.5.3 test /Users/pengcao/projects/aiready/packages/context-analyzer
4
4
  > vitest run
5
5
 
6
6
 
7
7
   RUN  v2.1.9 /Users/pengcao/projects/aiready/packages/context-analyzer
8
8
 
9
- ✓ src/__tests__/analyzer.test.ts (13)
9
+ ✓ src/__tests__/analyzer.test.ts (14)
10
10
  ✓ buildDependencyGraph (1)
11
11
  ✓ should build a basic dependency graph
12
12
  ✓ calculateImportDepth (2)
@@ -19,18 +19,19 @@
19
19
  ✓ detectCircularDependencies (2)
20
20
  ✓ should detect circular dependencies
21
21
  ✓ should return empty for no circular dependencies
22
- ✓ calculateCohesion (3)
22
+ ✓ calculateCohesion (4)
23
23
  ✓ should return 1 for single export
24
24
  ✓ should return high cohesion for related exports
25
25
  ✓ should return low cohesion for mixed exports
26
+ ✓ should return 1 for test files even with mixed domains
26
27
  ✓ calculateFragmentation (3)
27
28
  ✓ should return 0 for single file
28
29
  ✓ should return 0 for files in same directory
29
30
  ✓ should return high fragmentation for scattered files
30
31
 
31
32
   Test Files  1 passed (1)
32
-  Tests  13 passed (13)
33
-  Start at  08:00:54
34
-  Duration  315ms (transform 75ms, setup 0ms, collect 83ms, tests 3ms, environment 0ms, prepare 44ms)
33
+  Tests  14 passed (14)
34
+  Start at  08:26:52
35
+  Duration  317ms (transform 60ms, setup 0ms, collect 67ms, tests 4ms, environment 0ms, prepare 46ms)
35
36
 
36
37
  [?25h
@@ -0,0 +1,625 @@
1
+ // src/index.ts
2
+ import { scanFiles, readFileContent } from "@aiready/core";
3
+
4
+ // src/analyzer.ts
5
+ import { estimateTokens } from "@aiready/core";
6
+ function buildDependencyGraph(files) {
7
+ const nodes = /* @__PURE__ */ new Map();
8
+ const edges = /* @__PURE__ */ new Map();
9
+ for (const { file, content } of files) {
10
+ const imports = extractImportsFromContent(content);
11
+ const exports = extractExports(content);
12
+ const tokenCost = estimateTokens(content);
13
+ const linesOfCode = content.split("\n").length;
14
+ nodes.set(file, {
15
+ file,
16
+ imports,
17
+ exports,
18
+ tokenCost,
19
+ linesOfCode
20
+ });
21
+ edges.set(file, new Set(imports));
22
+ }
23
+ return { nodes, edges };
24
+ }
25
+ function extractImportsFromContent(content) {
26
+ const imports = [];
27
+ const patterns = [
28
+ /import\s+.*?\s+from\s+['"](.+?)['"]/g,
29
+ // import ... from '...'
30
+ /import\s+['"](.+?)['"]/g,
31
+ // import '...'
32
+ /require\(['"](.+?)['"]\)/g
33
+ // require('...')
34
+ ];
35
+ for (const pattern of patterns) {
36
+ let match;
37
+ while ((match = pattern.exec(content)) !== null) {
38
+ const importPath = match[1];
39
+ if (importPath && !importPath.startsWith("@") && !importPath.startsWith("node:")) {
40
+ imports.push(importPath);
41
+ }
42
+ }
43
+ }
44
+ return [...new Set(imports)];
45
+ }
46
+ function calculateImportDepth(file, graph, visited = /* @__PURE__ */ new Set(), depth = 0) {
47
+ if (visited.has(file)) {
48
+ return depth;
49
+ }
50
+ const dependencies = graph.edges.get(file);
51
+ if (!dependencies || dependencies.size === 0) {
52
+ return depth;
53
+ }
54
+ visited.add(file);
55
+ let maxDepth = depth;
56
+ for (const dep of dependencies) {
57
+ const depDepth = calculateImportDepth(dep, graph, visited, depth + 1);
58
+ maxDepth = Math.max(maxDepth, depDepth);
59
+ }
60
+ visited.delete(file);
61
+ return maxDepth;
62
+ }
63
+ function getTransitiveDependencies(file, graph, visited = /* @__PURE__ */ new Set()) {
64
+ if (visited.has(file)) {
65
+ return [];
66
+ }
67
+ visited.add(file);
68
+ const dependencies = graph.edges.get(file);
69
+ if (!dependencies || dependencies.size === 0) {
70
+ return [];
71
+ }
72
+ const allDeps = [];
73
+ for (const dep of dependencies) {
74
+ allDeps.push(dep);
75
+ allDeps.push(...getTransitiveDependencies(dep, graph, visited));
76
+ }
77
+ return [...new Set(allDeps)];
78
+ }
79
+ function calculateContextBudget(file, graph) {
80
+ const node = graph.nodes.get(file);
81
+ if (!node) return 0;
82
+ let totalTokens = node.tokenCost;
83
+ const deps = getTransitiveDependencies(file, graph);
84
+ for (const dep of deps) {
85
+ const depNode = graph.nodes.get(dep);
86
+ if (depNode) {
87
+ totalTokens += depNode.tokenCost;
88
+ }
89
+ }
90
+ return totalTokens;
91
+ }
92
+ function detectCircularDependencies(graph) {
93
+ const cycles = [];
94
+ const visited = /* @__PURE__ */ new Set();
95
+ const recursionStack = /* @__PURE__ */ new Set();
96
+ function dfs(file, path) {
97
+ if (recursionStack.has(file)) {
98
+ const cycleStart = path.indexOf(file);
99
+ if (cycleStart !== -1) {
100
+ cycles.push([...path.slice(cycleStart), file]);
101
+ }
102
+ return;
103
+ }
104
+ if (visited.has(file)) {
105
+ return;
106
+ }
107
+ visited.add(file);
108
+ recursionStack.add(file);
109
+ path.push(file);
110
+ const dependencies = graph.edges.get(file);
111
+ if (dependencies) {
112
+ for (const dep of dependencies) {
113
+ dfs(dep, [...path]);
114
+ }
115
+ }
116
+ recursionStack.delete(file);
117
+ }
118
+ for (const file of graph.nodes.keys()) {
119
+ if (!visited.has(file)) {
120
+ dfs(file, []);
121
+ }
122
+ }
123
+ return cycles;
124
+ }
125
+ function calculateCohesion(exports, filePath) {
126
+ if (exports.length === 0) return 1;
127
+ if (exports.length === 1) return 1;
128
+ if (filePath && isTestFile(filePath)) {
129
+ return 1;
130
+ }
131
+ const domains = exports.map((e) => e.inferredDomain || "unknown");
132
+ const domainCounts = /* @__PURE__ */ new Map();
133
+ for (const domain of domains) {
134
+ domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
135
+ }
136
+ const total = domains.length;
137
+ let entropy = 0;
138
+ for (const count of domainCounts.values()) {
139
+ const p = count / total;
140
+ if (p > 0) {
141
+ entropy -= p * Math.log2(p);
142
+ }
143
+ }
144
+ const maxEntropy = Math.log2(total);
145
+ return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
146
+ }
147
+ function isTestFile(filePath) {
148
+ const lower = filePath.toLowerCase();
149
+ return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
150
+ }
151
+ function calculateFragmentation(files, domain) {
152
+ if (files.length <= 1) return 0;
153
+ const directories = new Set(files.map((f) => f.split("/").slice(0, -1).join("/")));
154
+ return (directories.size - 1) / (files.length - 1);
155
+ }
156
+ function detectModuleClusters(graph) {
157
+ const domainMap = /* @__PURE__ */ new Map();
158
+ for (const [file, node] of graph.nodes.entries()) {
159
+ const domains = node.exports.map((e) => e.inferredDomain || "unknown");
160
+ const primaryDomain = domains[0] || "unknown";
161
+ if (!domainMap.has(primaryDomain)) {
162
+ domainMap.set(primaryDomain, []);
163
+ }
164
+ domainMap.get(primaryDomain).push(file);
165
+ }
166
+ const clusters = [];
167
+ for (const [domain, files] of domainMap.entries()) {
168
+ if (files.length < 2) continue;
169
+ const totalTokens = files.reduce((sum, file) => {
170
+ const node = graph.nodes.get(file);
171
+ return sum + (node?.tokenCost || 0);
172
+ }, 0);
173
+ const fragmentationScore = calculateFragmentation(files, domain);
174
+ const avgCohesion = files.reduce((sum, file) => {
175
+ const node = graph.nodes.get(file);
176
+ return sum + (node ? calculateCohesion(node.exports, file) : 0);
177
+ }, 0) / files.length;
178
+ const targetFiles = Math.max(1, Math.ceil(files.length / 3));
179
+ const consolidationPlan = generateConsolidationPlan(
180
+ domain,
181
+ files,
182
+ targetFiles
183
+ );
184
+ clusters.push({
185
+ domain,
186
+ files,
187
+ totalTokens,
188
+ fragmentationScore,
189
+ avgCohesion,
190
+ suggestedStructure: {
191
+ targetFiles,
192
+ consolidationPlan
193
+ }
194
+ });
195
+ }
196
+ return clusters.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
197
+ }
198
+ function extractExports(content) {
199
+ const exports = [];
200
+ const patterns = [
201
+ /export\s+function\s+(\w+)/g,
202
+ /export\s+class\s+(\w+)/g,
203
+ /export\s+const\s+(\w+)/g,
204
+ /export\s+type\s+(\w+)/g,
205
+ /export\s+interface\s+(\w+)/g,
206
+ /export\s+default/g
207
+ ];
208
+ const types = [
209
+ "function",
210
+ "class",
211
+ "const",
212
+ "type",
213
+ "interface",
214
+ "default"
215
+ ];
216
+ patterns.forEach((pattern, index) => {
217
+ let match;
218
+ while ((match = pattern.exec(content)) !== null) {
219
+ const name = match[1] || "default";
220
+ const type = types[index];
221
+ const inferredDomain = inferDomain(name);
222
+ exports.push({ name, type, inferredDomain });
223
+ }
224
+ });
225
+ return exports;
226
+ }
227
+ function inferDomain(name) {
228
+ const lower = name.toLowerCase();
229
+ const domainKeywords = [
230
+ "authentication",
231
+ "authorization",
232
+ "payment",
233
+ "invoice",
234
+ "customer",
235
+ "product",
236
+ "order",
237
+ "cart",
238
+ "user",
239
+ "admin",
240
+ "repository",
241
+ "controller",
242
+ "service",
243
+ "config",
244
+ "model",
245
+ "view",
246
+ "auth",
247
+ "api",
248
+ "helper",
249
+ "util"
250
+ ];
251
+ for (const keyword of domainKeywords) {
252
+ const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
253
+ if (wordBoundaryPattern.test(name)) {
254
+ return keyword;
255
+ }
256
+ }
257
+ for (const keyword of domainKeywords) {
258
+ if (lower.includes(keyword)) {
259
+ return keyword;
260
+ }
261
+ }
262
+ return "unknown";
263
+ }
264
+ function generateConsolidationPlan(domain, files, targetFiles) {
265
+ const plan = [];
266
+ if (files.length <= targetFiles) {
267
+ return [`No consolidation needed for ${domain}`];
268
+ }
269
+ plan.push(
270
+ `Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s):`
271
+ );
272
+ const dirGroups = /* @__PURE__ */ new Map();
273
+ for (const file of files) {
274
+ const dir = file.split("/").slice(0, -1).join("/");
275
+ if (!dirGroups.has(dir)) {
276
+ dirGroups.set(dir, []);
277
+ }
278
+ dirGroups.get(dir).push(file);
279
+ }
280
+ plan.push(`1. Create unified ${domain} module file`);
281
+ plan.push(
282
+ `2. Move related functionality from ${files.length} scattered files`
283
+ );
284
+ plan.push(`3. Update imports in dependent files`);
285
+ plan.push(
286
+ `4. Remove old files after consolidation (verify with tests first)`
287
+ );
288
+ return plan;
289
+ }
290
+
291
+ // src/index.ts
292
+ async function getSmartDefaults(directory, userOptions) {
293
+ const files = await scanFiles({
294
+ rootDir: directory,
295
+ include: userOptions.include,
296
+ exclude: userOptions.exclude
297
+ });
298
+ const estimatedBlocks = files.length;
299
+ let maxDepth;
300
+ let maxContextBudget;
301
+ let minCohesion;
302
+ let maxFragmentation;
303
+ if (estimatedBlocks < 100) {
304
+ maxDepth = 4;
305
+ maxContextBudget = 8e3;
306
+ minCohesion = 0.5;
307
+ maxFragmentation = 0.5;
308
+ } else if (estimatedBlocks < 500) {
309
+ maxDepth = 5;
310
+ maxContextBudget = 15e3;
311
+ minCohesion = 0.45;
312
+ maxFragmentation = 0.6;
313
+ } else if (estimatedBlocks < 2e3) {
314
+ maxDepth = 7;
315
+ maxContextBudget = 25e3;
316
+ minCohesion = 0.4;
317
+ maxFragmentation = 0.7;
318
+ } else {
319
+ maxDepth = 10;
320
+ maxContextBudget = 4e4;
321
+ minCohesion = 0.35;
322
+ maxFragmentation = 0.8;
323
+ }
324
+ return {
325
+ maxDepth,
326
+ maxContextBudget,
327
+ minCohesion,
328
+ maxFragmentation,
329
+ focus: "all",
330
+ includeNodeModules: false,
331
+ rootDir: userOptions.rootDir || directory,
332
+ include: userOptions.include,
333
+ exclude: userOptions.exclude
334
+ };
335
+ }
336
+ async function analyzeContext(options) {
337
+ const {
338
+ maxDepth = 5,
339
+ maxContextBudget = 1e4,
340
+ minCohesion = 0.6,
341
+ maxFragmentation = 0.5,
342
+ focus = "all",
343
+ includeNodeModules = false,
344
+ ...scanOptions
345
+ } = options;
346
+ const files = await scanFiles({
347
+ ...scanOptions,
348
+ // Only add node_modules to exclude if includeNodeModules is false
349
+ // The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
350
+ // if user overrides the default exclude list
351
+ exclude: includeNodeModules && scanOptions.exclude ? scanOptions.exclude.filter((pattern) => pattern !== "**/node_modules/**") : scanOptions.exclude
352
+ });
353
+ const fileContents = await Promise.all(
354
+ files.map(async (file) => ({
355
+ file,
356
+ content: await readFileContent(file)
357
+ }))
358
+ );
359
+ const graph = buildDependencyGraph(fileContents);
360
+ const circularDeps = detectCircularDependencies(graph);
361
+ const clusters = detectModuleClusters(graph);
362
+ const fragmentationMap = /* @__PURE__ */ new Map();
363
+ for (const cluster of clusters) {
364
+ for (const file of cluster.files) {
365
+ fragmentationMap.set(file, cluster.fragmentationScore);
366
+ }
367
+ }
368
+ const results = [];
369
+ for (const { file } of fileContents) {
370
+ const node = graph.nodes.get(file);
371
+ if (!node) continue;
372
+ const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
373
+ const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
374
+ const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
375
+ const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
376
+ const fragmentationScore = fragmentationMap.get(file) || 0;
377
+ const relatedFiles = [];
378
+ for (const cluster of clusters) {
379
+ if (cluster.files.includes(file)) {
380
+ relatedFiles.push(...cluster.files.filter((f) => f !== file));
381
+ break;
382
+ }
383
+ }
384
+ const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
385
+ file,
386
+ importDepth,
387
+ contextBudget,
388
+ cohesionScore,
389
+ fragmentationScore,
390
+ maxDepth,
391
+ maxContextBudget,
392
+ minCohesion,
393
+ maxFragmentation,
394
+ circularDeps
395
+ });
396
+ const domains = [
397
+ ...new Set(node.exports.map((e) => e.inferredDomain || "unknown"))
398
+ ];
399
+ results.push({
400
+ file,
401
+ tokenCost: node.tokenCost,
402
+ linesOfCode: node.linesOfCode,
403
+ importDepth,
404
+ dependencyCount: dependencyList.length,
405
+ dependencyList,
406
+ circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
407
+ cohesionScore,
408
+ domains,
409
+ exportCount: node.exports.length,
410
+ contextBudget,
411
+ fragmentationScore,
412
+ relatedFiles,
413
+ severity,
414
+ issues,
415
+ recommendations,
416
+ potentialSavings
417
+ });
418
+ }
419
+ const issuesOnly = results.filter((r) => r.severity !== "info");
420
+ const sorted = issuesOnly.sort((a, b) => {
421
+ const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
422
+ const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
423
+ if (severityDiff !== 0) return severityDiff;
424
+ return b.contextBudget - a.contextBudget;
425
+ });
426
+ return sorted.length > 0 ? sorted : results;
427
+ }
428
+ function generateSummary(results) {
429
+ if (results.length === 0) {
430
+ return {
431
+ totalFiles: 0,
432
+ totalTokens: 0,
433
+ avgContextBudget: 0,
434
+ maxContextBudget: 0,
435
+ avgImportDepth: 0,
436
+ maxImportDepth: 0,
437
+ deepFiles: [],
438
+ avgFragmentation: 0,
439
+ fragmentedModules: [],
440
+ avgCohesion: 0,
441
+ lowCohesionFiles: [],
442
+ criticalIssues: 0,
443
+ majorIssues: 0,
444
+ minorIssues: 0,
445
+ totalPotentialSavings: 0,
446
+ topExpensiveFiles: []
447
+ };
448
+ }
449
+ const totalFiles = results.length;
450
+ const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
451
+ const totalContextBudget = results.reduce(
452
+ (sum, r) => sum + r.contextBudget,
453
+ 0
454
+ );
455
+ const avgContextBudget = totalContextBudget / totalFiles;
456
+ const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
457
+ const avgImportDepth = results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
458
+ const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
459
+ const deepFiles = results.filter((r) => r.importDepth >= 5).map((r) => ({ file: r.file, depth: r.importDepth })).sort((a, b) => b.depth - a.depth).slice(0, 10);
460
+ const avgFragmentation = results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
461
+ const moduleMap = /* @__PURE__ */ new Map();
462
+ for (const result of results) {
463
+ for (const domain of result.domains) {
464
+ if (!moduleMap.has(domain)) {
465
+ moduleMap.set(domain, []);
466
+ }
467
+ moduleMap.get(domain).push(result);
468
+ }
469
+ }
470
+ const fragmentedModules = [];
471
+ for (const [domain, files] of moduleMap.entries()) {
472
+ if (files.length < 2) continue;
473
+ const fragmentationScore = files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
474
+ if (fragmentationScore < 0.3) continue;
475
+ const totalTokens2 = files.reduce((sum, f) => sum + f.tokenCost, 0);
476
+ const avgCohesion2 = files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
477
+ const targetFiles = Math.max(1, Math.ceil(files.length / 3));
478
+ fragmentedModules.push({
479
+ domain,
480
+ files: files.map((f) => f.file),
481
+ totalTokens: totalTokens2,
482
+ fragmentationScore,
483
+ avgCohesion: avgCohesion2,
484
+ suggestedStructure: {
485
+ targetFiles,
486
+ consolidationPlan: [
487
+ `Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s)`,
488
+ `Current token cost: ${totalTokens2.toLocaleString()}`,
489
+ `Estimated savings: ${Math.floor(totalTokens2 * 0.3).toLocaleString()} tokens (30%)`
490
+ ]
491
+ }
492
+ });
493
+ }
494
+ fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
495
+ const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
496
+ const lowCohesionFiles = results.filter((r) => r.cohesionScore < 0.6).map((r) => ({ file: r.file, score: r.cohesionScore })).sort((a, b) => a.score - b.score).slice(0, 10);
497
+ const criticalIssues = results.filter((r) => r.severity === "critical").length;
498
+ const majorIssues = results.filter((r) => r.severity === "major").length;
499
+ const minorIssues = results.filter((r) => r.severity === "minor").length;
500
+ const totalPotentialSavings = results.reduce(
501
+ (sum, r) => sum + r.potentialSavings,
502
+ 0
503
+ );
504
+ const topExpensiveFiles = results.sort((a, b) => b.contextBudget - a.contextBudget).slice(0, 10).map((r) => ({
505
+ file: r.file,
506
+ contextBudget: r.contextBudget,
507
+ severity: r.severity
508
+ }));
509
+ return {
510
+ totalFiles,
511
+ totalTokens,
512
+ avgContextBudget,
513
+ maxContextBudget,
514
+ avgImportDepth,
515
+ maxImportDepth,
516
+ deepFiles,
517
+ avgFragmentation,
518
+ fragmentedModules: fragmentedModules.slice(0, 10),
519
+ avgCohesion,
520
+ lowCohesionFiles,
521
+ criticalIssues,
522
+ majorIssues,
523
+ minorIssues,
524
+ totalPotentialSavings,
525
+ topExpensiveFiles
526
+ };
527
+ }
528
+ function analyzeIssues(params) {
529
+ const {
530
+ file,
531
+ importDepth,
532
+ contextBudget,
533
+ cohesionScore,
534
+ fragmentationScore,
535
+ maxDepth,
536
+ maxContextBudget,
537
+ minCohesion,
538
+ maxFragmentation,
539
+ circularDeps
540
+ } = params;
541
+ const issues = [];
542
+ const recommendations = [];
543
+ let severity = "info";
544
+ let potentialSavings = 0;
545
+ if (circularDeps.length > 0) {
546
+ severity = "critical";
547
+ issues.push(
548
+ `Part of ${circularDeps.length} circular dependency chain(s)`
549
+ );
550
+ recommendations.push("Break circular dependencies by extracting interfaces or using dependency injection");
551
+ potentialSavings += contextBudget * 0.2;
552
+ }
553
+ if (importDepth > maxDepth * 1.5) {
554
+ severity = severity === "critical" ? "critical" : "critical";
555
+ issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
556
+ recommendations.push("Flatten dependency tree or use facade pattern");
557
+ potentialSavings += contextBudget * 0.3;
558
+ } else if (importDepth > maxDepth) {
559
+ severity = severity === "critical" ? "critical" : "major";
560
+ issues.push(`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`);
561
+ recommendations.push("Consider reducing dependency depth");
562
+ potentialSavings += contextBudget * 0.15;
563
+ }
564
+ if (contextBudget > maxContextBudget * 1.5) {
565
+ severity = severity === "critical" ? "critical" : "critical";
566
+ issues.push(`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`);
567
+ recommendations.push("Split into smaller modules or reduce dependency tree");
568
+ potentialSavings += contextBudget * 0.4;
569
+ } else if (contextBudget > maxContextBudget) {
570
+ severity = severity === "critical" || severity === "major" ? severity : "major";
571
+ issues.push(`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`);
572
+ recommendations.push("Reduce file size or dependencies");
573
+ potentialSavings += contextBudget * 0.2;
574
+ }
575
+ if (cohesionScore < minCohesion * 0.5) {
576
+ severity = severity === "critical" ? "critical" : "major";
577
+ issues.push(`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`);
578
+ recommendations.push("Split file by domain - separate unrelated functionality");
579
+ potentialSavings += contextBudget * 0.25;
580
+ } else if (cohesionScore < minCohesion) {
581
+ severity = severity === "critical" || severity === "major" ? severity : "minor";
582
+ issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
583
+ recommendations.push("Consider grouping related exports together");
584
+ potentialSavings += contextBudget * 0.1;
585
+ }
586
+ if (fragmentationScore > maxFragmentation) {
587
+ severity = severity === "critical" || severity === "major" ? severity : "minor";
588
+ issues.push(`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`);
589
+ recommendations.push("Consolidate with related files in same domain");
590
+ potentialSavings += contextBudget * 0.3;
591
+ }
592
+ if (issues.length === 0) {
593
+ issues.push("No significant issues detected");
594
+ recommendations.push("File is well-structured for AI context usage");
595
+ }
596
+ if (isBuildArtifact(file)) {
597
+ issues.push("Detected build artifact (bundled/output file)");
598
+ recommendations.push("Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis");
599
+ severity = downgradeSeverity(severity);
600
+ potentialSavings = 0;
601
+ }
602
+ return { severity, issues, recommendations, potentialSavings: Math.floor(potentialSavings) };
603
+ }
604
+ function isBuildArtifact(filePath) {
605
+ const lower = filePath.toLowerCase();
606
+ return lower.includes("/node_modules/") || lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/out/") || lower.includes("/output/") || lower.includes("/cdk.out/") || lower.includes("/.next/") || /\/asset\.[^/]+\//.test(lower);
607
+ }
608
+ function downgradeSeverity(s) {
609
+ switch (s) {
610
+ case "critical":
611
+ return "minor";
612
+ case "major":
613
+ return "minor";
614
+ case "minor":
615
+ return "info";
616
+ default:
617
+ return "info";
618
+ }
619
+ }
620
+
621
+ export {
622
+ getSmartDefaults,
623
+ analyzeContext,
624
+ generateSummary
625
+ };
package/dist/cli.js CHANGED
@@ -150,9 +150,12 @@ function detectCircularDependencies(graph) {
150
150
  }
151
151
  return cycles;
152
152
  }
153
- function calculateCohesion(exports2) {
153
+ function calculateCohesion(exports2, filePath) {
154
154
  if (exports2.length === 0) return 1;
155
155
  if (exports2.length === 1) return 1;
156
+ if (filePath && isTestFile(filePath)) {
157
+ return 1;
158
+ }
156
159
  const domains = exports2.map((e) => e.inferredDomain || "unknown");
157
160
  const domainCounts = /* @__PURE__ */ new Map();
158
161
  for (const domain of domains) {
@@ -169,6 +172,10 @@ function calculateCohesion(exports2) {
169
172
  const maxEntropy = Math.log2(total);
170
173
  return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
171
174
  }
175
+ function isTestFile(filePath) {
176
+ const lower = filePath.toLowerCase();
177
+ return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
178
+ }
172
179
  function calculateFragmentation(files, domain) {
173
180
  if (files.length <= 1) return 0;
174
181
  const directories = new Set(files.map((f) => f.split("/").slice(0, -1).join("/")));
@@ -194,7 +201,7 @@ function detectModuleClusters(graph) {
194
201
  const fragmentationScore = calculateFragmentation(files, domain);
195
202
  const avgCohesion = files.reduce((sum, file) => {
196
203
  const node = graph.nodes.get(file);
197
- return sum + (node ? calculateCohesion(node.exports) : 0);
204
+ return sum + (node ? calculateCohesion(node.exports, file) : 0);
198
205
  }, 0) / files.length;
199
206
  const targetFiles = Math.max(1, Math.ceil(files.length / 3));
200
207
  const consolidationPlan = generateConsolidationPlan(
@@ -248,25 +255,33 @@ function extractExports(content) {
248
255
  function inferDomain(name) {
249
256
  const lower = name.toLowerCase();
250
257
  const domainKeywords = [
251
- "user",
252
- "auth",
253
- "order",
254
- "product",
258
+ "authentication",
259
+ "authorization",
255
260
  "payment",
256
- "cart",
257
261
  "invoice",
258
262
  "customer",
263
+ "product",
264
+ "order",
265
+ "cart",
266
+ "user",
259
267
  "admin",
260
- "api",
261
- "util",
262
- "helper",
263
- "config",
264
- "service",
265
268
  "repository",
266
269
  "controller",
270
+ "service",
271
+ "config",
267
272
  "model",
268
- "view"
273
+ "view",
274
+ "auth",
275
+ "api",
276
+ "helper",
277
+ "util"
269
278
  ];
279
+ for (const keyword of domainKeywords) {
280
+ const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
281
+ if (wordBoundaryPattern.test(name)) {
282
+ return keyword;
283
+ }
284
+ }
270
285
  for (const keyword of domainKeywords) {
271
286
  if (lower.includes(keyword)) {
272
287
  return keyword;
@@ -341,7 +356,7 @@ async function analyzeContext(options) {
341
356
  const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
342
357
  const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
343
358
  const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
344
- const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports) : 1;
359
+ const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
345
360
  const fragmentationScore = fragmentationMap.get(file) || 0;
346
361
  const relatedFiles = [];
347
362
  for (const cluster of clusters) {
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  analyzeContext,
4
4
  generateSummary
5
- } from "./chunk-NJUW6VED.mjs";
5
+ } from "./chunk-EX7HCWAO.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -148,9 +148,12 @@ function detectCircularDependencies(graph) {
148
148
  }
149
149
  return cycles;
150
150
  }
151
- function calculateCohesion(exports2) {
151
+ function calculateCohesion(exports2, filePath) {
152
152
  if (exports2.length === 0) return 1;
153
153
  if (exports2.length === 1) return 1;
154
+ if (filePath && isTestFile(filePath)) {
155
+ return 1;
156
+ }
154
157
  const domains = exports2.map((e) => e.inferredDomain || "unknown");
155
158
  const domainCounts = /* @__PURE__ */ new Map();
156
159
  for (const domain of domains) {
@@ -167,6 +170,10 @@ function calculateCohesion(exports2) {
167
170
  const maxEntropy = Math.log2(total);
168
171
  return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
169
172
  }
173
+ function isTestFile(filePath) {
174
+ const lower = filePath.toLowerCase();
175
+ return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
176
+ }
170
177
  function calculateFragmentation(files, domain) {
171
178
  if (files.length <= 1) return 0;
172
179
  const directories = new Set(files.map((f) => f.split("/").slice(0, -1).join("/")));
@@ -192,7 +199,7 @@ function detectModuleClusters(graph) {
192
199
  const fragmentationScore = calculateFragmentation(files, domain);
193
200
  const avgCohesion = files.reduce((sum, file) => {
194
201
  const node = graph.nodes.get(file);
195
- return sum + (node ? calculateCohesion(node.exports) : 0);
202
+ return sum + (node ? calculateCohesion(node.exports, file) : 0);
196
203
  }, 0) / files.length;
197
204
  const targetFiles = Math.max(1, Math.ceil(files.length / 3));
198
205
  const consolidationPlan = generateConsolidationPlan(
@@ -246,25 +253,33 @@ function extractExports(content) {
246
253
  function inferDomain(name) {
247
254
  const lower = name.toLowerCase();
248
255
  const domainKeywords = [
249
- "user",
250
- "auth",
251
- "order",
252
- "product",
256
+ "authentication",
257
+ "authorization",
253
258
  "payment",
254
- "cart",
255
259
  "invoice",
256
260
  "customer",
261
+ "product",
262
+ "order",
263
+ "cart",
264
+ "user",
257
265
  "admin",
258
- "api",
259
- "util",
260
- "helper",
261
- "config",
262
- "service",
263
266
  "repository",
264
267
  "controller",
268
+ "service",
269
+ "config",
265
270
  "model",
266
- "view"
271
+ "view",
272
+ "auth",
273
+ "api",
274
+ "helper",
275
+ "util"
267
276
  ];
277
+ for (const keyword of domainKeywords) {
278
+ const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
279
+ if (wordBoundaryPattern.test(name)) {
280
+ return keyword;
281
+ }
282
+ }
268
283
  for (const keyword of domainKeywords) {
269
284
  if (lower.includes(keyword)) {
270
285
  return keyword;
@@ -383,7 +398,7 @@ async function analyzeContext(options) {
383
398
  const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
384
399
  const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
385
400
  const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
386
- const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports) : 1;
401
+ const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
387
402
  const fragmentationScore = fragmentationMap.get(file) || 0;
388
403
  const relatedFiles = [];
389
404
  for (const cluster of clusters) {
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  analyzeContext,
3
3
  generateSummary,
4
4
  getSmartDefaults
5
- } from "./chunk-NJUW6VED.mjs";
5
+ } from "./chunk-EX7HCWAO.mjs";
6
6
  export {
7
7
  analyzeContext,
8
8
  generateSummary,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/context-analyzer",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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",
@@ -148,6 +148,30 @@ describe('calculateCohesion', () => {
148
148
  const cohesion = calculateCohesion(exports);
149
149
  expect(cohesion).toBeLessThan(0.5);
150
150
  });
151
+
152
+ it('should return 1 for test files even with mixed domains', () => {
153
+ const exports = [
154
+ { name: 'mockUser', type: 'function' as const, inferredDomain: 'user' },
155
+ { name: 'mockOrder', type: 'function' as const, inferredDomain: 'order' },
156
+ { name: 'setupTestDb', type: 'function' as const, inferredDomain: 'helper' },
157
+ ];
158
+
159
+ // Test file - should return 1 despite mixed domains
160
+ const cohesionTestFile = calculateCohesion(exports, 'src/__tests__/helpers.test.ts');
161
+ expect(cohesionTestFile).toBe(1);
162
+
163
+ // Mock file - should return 1 despite mixed domains
164
+ const cohesionMockFile = calculateCohesion(exports, 'src/test-utils/mocks.ts');
165
+ expect(cohesionMockFile).toBe(1);
166
+
167
+ // Fixture file - should return 1 despite mixed domains
168
+ const cohesionFixtureFile = calculateCohesion(exports, 'src/fixtures/data.ts');
169
+ expect(cohesionFixtureFile).toBe(1);
170
+
171
+ // Regular file - should have low cohesion
172
+ const cohesionRegularFile = calculateCohesion(exports, 'src/utils/helpers.ts');
173
+ expect(cohesionRegularFile).toBeLessThan(0.5);
174
+ });
151
175
  });
152
176
 
153
177
  describe('calculateFragmentation', () => {
package/src/analyzer.ts CHANGED
@@ -200,11 +200,19 @@ export function detectCircularDependencies(
200
200
  /**
201
201
  * Calculate cohesion score (how related are exports in a file)
202
202
  * Uses entropy: low entropy = high cohesion
203
+ * @param exports - Array of export information
204
+ * @param filePath - Optional file path for context-aware scoring
203
205
  */
204
- export function calculateCohesion(exports: ExportInfo[]): number {
206
+ export function calculateCohesion(exports: ExportInfo[], filePath?: string): number {
205
207
  if (exports.length === 0) return 1;
206
208
  if (exports.length === 1) return 1; // Single export = perfect cohesion
207
209
 
210
+ // Special case: Test/mock/fixture files are expected to have multi-domain exports
211
+ // They serve a single purpose (testing) even if they mock different domains
212
+ if (filePath && isTestFile(filePath)) {
213
+ return 1; // Test utilities are inherently cohesive despite mixed domains
214
+ }
215
+
208
216
  const domains = exports.map((e) => e.inferredDomain || 'unknown');
209
217
  const domainCounts = new Map<string, number>();
210
218
 
@@ -228,6 +236,22 @@ export function calculateCohesion(exports: ExportInfo[]): number {
228
236
  return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
229
237
  }
230
238
 
239
+ /**
240
+ * Check if a file is a test/mock/fixture file
241
+ */
242
+ function isTestFile(filePath: string): boolean {
243
+ const lower = filePath.toLowerCase();
244
+ return (
245
+ lower.includes('test') ||
246
+ lower.includes('spec') ||
247
+ lower.includes('mock') ||
248
+ lower.includes('fixture') ||
249
+ lower.includes('__tests__') ||
250
+ lower.includes('.test.') ||
251
+ lower.includes('.spec.')
252
+ );
253
+ }
254
+
231
255
  /**
232
256
  * Calculate fragmentation score (how scattered is a domain)
233
257
  */
@@ -279,7 +303,7 @@ export function detectModuleClusters(
279
303
  const avgCohesion =
280
304
  files.reduce((sum, file) => {
281
305
  const node = graph.nodes.get(file);
282
- return sum + (node ? calculateCohesion(node.exports) : 0);
306
+ return sum + (node ? calculateCohesion(node.exports, file) : 0);
283
307
  }, 0) / files.length;
284
308
 
285
309
  // Generate consolidation plan
@@ -349,33 +373,45 @@ function extractExports(content: string): ExportInfo[] {
349
373
 
350
374
  /**
351
375
  * Infer domain from export name
352
- * Uses common naming patterns
376
+ * Uses common naming patterns with word boundary matching
353
377
  */
354
378
  function inferDomain(name: string): string {
355
379
  const lower = name.toLowerCase();
356
380
 
357
- // Common domain keywords
381
+ // Domain keywords ordered from most specific to most general
382
+ // This prevents generic terms like 'util' from matching before specific domains
358
383
  const domainKeywords = [
359
- 'user',
360
- 'auth',
361
- 'order',
362
- 'product',
384
+ 'authentication',
385
+ 'authorization',
363
386
  'payment',
364
- 'cart',
365
387
  'invoice',
366
388
  'customer',
389
+ 'product',
390
+ 'order',
391
+ 'cart',
392
+ 'user',
367
393
  'admin',
368
- 'api',
369
- 'util',
370
- 'helper',
371
- 'config',
372
- 'service',
373
394
  'repository',
374
395
  'controller',
396
+ 'service',
397
+ 'config',
375
398
  'model',
376
399
  'view',
400
+ 'auth',
401
+ 'api',
402
+ 'helper',
403
+ 'util',
377
404
  ];
378
405
 
406
+ // Try word boundary matching first for more accurate detection
407
+ for (const keyword of domainKeywords) {
408
+ const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, 'i');
409
+ if (wordBoundaryPattern.test(name)) {
410
+ return keyword;
411
+ }
412
+ }
413
+
414
+ // Fallback to substring matching for compound words
379
415
  for (const keyword of domainKeywords) {
380
416
  if (lower.includes(keyword)) {
381
417
  return keyword;
package/src/index.ts CHANGED
@@ -157,7 +157,7 @@ export async function analyzeContext(
157
157
 
158
158
  const cohesionScore =
159
159
  focus === 'cohesion' || focus === 'all'
160
- ? calculateCohesion(node.exports)
160
+ ? calculateCohesion(node.exports, file)
161
161
  : 1;
162
162
 
163
163
  const fragmentationScore = fragmentationMap.get(file) || 0;