@aiready/context-analyzer 0.5.0 → 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.0 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/cli.js 39.27 KB
13
- CJS dist/index.js 20.62 KB
14
- CJS ⚡️ Build success in 42ms
12
+ CJS dist/cli.js 39.84 KB
13
+ CJS dist/index.js 21.19 KB
14
+ CJS ⚡️ Build success in 57ms
15
15
  ESM dist/cli.mjs 18.45 KB
16
+ ESM dist/chunk-EX7HCWAO.mjs 20.05 KB
16
17
  ESM dist/index.mjs 164.00 B
17
- ESM dist/chunk-NJUW6VED.mjs 19.48 KB
18
- ESM ⚡️ Build success in 42ms
18
+ ESM ⚡️ Build success in 57ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 551ms
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,33 +1,12 @@
1
1
 
2
2
  
3
- > @aiready/context-analyzer@0.5.0 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
- [?25l · src/__tests__/analyzer.test.ts (13)
10
- · buildDependencyGraph (1)
11
- · should build a basic dependency graph
12
- · calculateImportDepth (2)
13
- · should calculate import depth correctly
14
- · should handle circular dependencies gracefully
15
- · getTransitiveDependencies (1)
16
- · should get all transitive dependencies
17
- · calculateContextBudget (1)
18
- · should calculate total token cost including dependencies
19
- · detectCircularDependencies (2)
20
- · should detect circular dependencies
21
- · should return empty for no circular dependencies
22
- · calculateCohesion (3)
23
- · should return 1 for single export
24
- · should return high cohesion for related exports
25
- · should return low cohesion for mixed exports
26
- · calculateFragmentation (3)
27
- · should return 0 for single file
28
- · should return 0 for files in same directory
29
- · should return high fragmentation for scattered files
30
- [?25l ✓ src/__tests__/analyzer.test.ts (13)
9
+ ✓ src/__tests__/analyzer.test.ts (14)
31
10
  ✓ buildDependencyGraph (1)
32
11
  ✓ should build a basic dependency graph
33
12
  ✓ calculateImportDepth (2)
@@ -40,18 +19,19 @@
40
19
  ✓ detectCircularDependencies (2)
41
20
  ✓ should detect circular dependencies
42
21
  ✓ should return empty for no circular dependencies
43
- ✓ calculateCohesion (3)
22
+ ✓ calculateCohesion (4)
44
23
  ✓ should return 1 for single export
45
24
  ✓ should return high cohesion for related exports
46
25
  ✓ should return low cohesion for mixed exports
26
+ ✓ should return 1 for test files even with mixed domains
47
27
  ✓ calculateFragmentation (3)
48
28
  ✓ should return 0 for single file
49
29
  ✓ should return 0 for files in same directory
50
30
  ✓ should return high fragmentation for scattered files
51
31
 
52
32
   Test Files  1 passed (1)
53
-  Tests  13 passed (13)
54
-  Start at  07:46:59
55
-  Duration  394ms (transform 66ms, setup 0ms, collect 75ms, tests 41ms, environment 0ms, prepare 42ms)
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)
56
36
 
57
- [?25h[?25h
37
+ [?25h
package/README.md CHANGED
@@ -4,10 +4,15 @@
4
4
 
5
5
  When AI tools try to help with your code, they need to load files into their context window. Fragmented code structures make this expensive and sometimes impossible. This tool analyzes your codebase to identify:
6
6
 
7
- - Deep import chains that require loading dozens of files
8
- - Fragmented modules scattered across many directories
9
- - Low-cohesion files mixing unrelated concerns
10
- - Files with excessive context budgets
7
+ - 📦 **High Context Budget**: Files that cost too many AI tokens to understand (file + dependencies)
8
+ - 🔗 **Deep Import Chains**: Cascading dependencies that force AI to load many files
9
+ - 🎯 **Low Cohesion**: Files mixing unrelated concerns (God objects)
10
+ - 🗂️ **High Fragmentation**: Domains scattered across many directories
11
+
12
+ **Quick Start:**
13
+ ```bash
14
+ npx @aiready/context-analyzer ./src
15
+ ```
11
16
 
12
17
  ## 🎯 Why This Tool?
13
18
 
@@ -50,6 +55,147 @@ Result: AI sees everything, gives complete answers ✅
50
55
  - Use **@aiready/context-analyzer** to optimize for AI tools (advisory)
51
56
  - Track improvements over time with SaaS tier
52
57
 
58
+ ## 🧠 Understanding the Metrics
59
+
60
+ This tool measures four key dimensions that affect how much context AI tools need to load:
61
+
62
+ ### 📊 Context Budget (Tokens)
63
+
64
+ **What it measures:** Total AI tokens needed to understand a file (file content + all dependencies)
65
+
66
+ **Why it matters:** AI tools have limited context windows (e.g., 128K tokens). Large context budgets mean:
67
+ - AI needs to load more files to understand your code
68
+ - Risk of hitting context limits → incomplete/wrong answers
69
+ - Slower AI responses (more processing time)
70
+
71
+ **Example:**
72
+ ```typescript
73
+ // High context budget (15,000 tokens)
74
+ import { A, B, C } from './deeply/nested/utils' // +5,000 tokens
75
+ import { X, Y, Z } from './another/chain' // +8,000 tokens
76
+ // Your file: 2,000 tokens
77
+ // Total: 15,000 tokens just to understand this one file!
78
+
79
+ // Low context budget (2,500 tokens)
80
+ // No deep imports, self-contained logic
81
+ // Total: 2,500 tokens
82
+ ```
83
+
84
+ **🎯 Recommendation:** Files with high context budgets should be **split into smaller, more focused modules**.
85
+
86
+ ---
87
+
88
+ ### 🔗 Import Depth
89
+
90
+ **What it measures:** How many layers deep your import chains go
91
+
92
+ **Why it matters:** Deep import chains create cascading context loads:
93
+ ```
94
+ app.ts → service.ts → helper.ts → util.ts → core.ts → base.ts
95
+ ```
96
+ AI must load all 6 files just to understand app.ts!
97
+
98
+ **Example:**
99
+ ```typescript
100
+ // Deep chain (depth 8) = AI loads 8+ files
101
+ import { validate } from '../../../utils/validators/user/schema'
102
+
103
+ // Shallow (depth 2) = AI loads 2 files
104
+ import { validate } from './validators'
105
+ ```
106
+
107
+ **🎯 Recommendation:** Flatten dependency trees or use **facade patterns** to reduce depth.
108
+
109
+ ---
110
+
111
+ ### 🎯 Cohesion Score (0-1)
112
+
113
+ **What it measures:** How related the exports in a file are to each other
114
+
115
+ **How it's calculated:** Uses Shannon entropy of inferred domains
116
+ - 1.0 = Perfect cohesion (all exports are related)
117
+ - 0.0 = Zero cohesion (completely unrelated exports)
118
+
119
+ **Why it matters:** Low cohesion = "God object" pattern = AI confusion
120
+ ```typescript
121
+ // Low cohesion (0.3) - mixing unrelated concerns
122
+ export function validateUser() { } // User domain
123
+ export function formatDate() { } // Date domain
124
+ export function sendEmail() { } // Email domain
125
+ export class DatabasePool { } // Database domain
126
+ // AI thinks: "What does this file actually do?"
127
+
128
+ // High cohesion (0.9) - focused responsibility
129
+ export function validateUser() { }
130
+ export function createUser() { }
131
+ export function updateUser() { }
132
+ export interface User { }
133
+ // AI thinks: "Clear! This is user management."
134
+ ```
135
+
136
+ **🎯 Recommendation:** Files with low cohesion should be **split by domain** into separate, focused files.
137
+
138
+ ---
139
+
140
+ ### 🗂️ Fragmentation Score (0-1)
141
+
142
+ **What it measures:** How scattered a domain/concept is across different directories
143
+
144
+ **How it's calculated:** `(unique directories - 1) / (total files - 1)`
145
+ - 0.0 = No fragmentation (all files in same directory)
146
+ - 1.0 = Maximum fragmentation (each file in different directory)
147
+
148
+ **Why it matters:** Scattered domains force AI to load many unrelated paths
149
+ ```typescript
150
+ // High fragmentation (0.8) - User domain scattered
151
+ src/api/user-routes.ts // 800 tokens
152
+ src/services/user-service.ts // 1,200 tokens
153
+ src/helpers/user-helpers.ts // 600 tokens
154
+ src/utils/user-utils.ts // 500 tokens
155
+ src/validators/user-validator.ts // 700 tokens
156
+ src/models/user-model.ts // 900 tokens
157
+ // Total: 4,700 tokens spread across 6 directories!
158
+ // AI must navigate entire codebase to understand "User"
159
+
160
+ // Low fragmentation (0.0) - consolidated
161
+ src/user/user.ts // 2,800 tokens
162
+ src/user/types.ts // 600 tokens
163
+ // Total: 3,400 tokens in one place (29% savings!)
164
+ // AI finds everything in one logical location
165
+ ```
166
+
167
+ **🎯 Recommendation:** Domains with high fragmentation should be **consolidated** into cohesive modules.
168
+
169
+ ---
170
+
171
+ ### ⚖️ The Tradeoff: Splitting vs. Consolidating
172
+
173
+ **Important:** These metrics can pull in opposite directions!
174
+
175
+ | Action | Context Budget ⬇️ | Fragmentation ⬇️ | Cohesion ⬆️ |
176
+ |--------|------------------|------------------|-------------|
177
+ | **Split large file** | ✅ Reduces | ⚠️ May increase | ✅ Can improve |
178
+ | **Consolidate scattered files** | ⚠️ May increase | ✅ Reduces | ⚠️ May decrease |
179
+
180
+ **Best Practice:** Optimize for your use case:
181
+ - **Large files with mixed concerns** → Split by domain (improves cohesion + reduces budget)
182
+ - **Scattered single-domain files** → Consolidate (reduces fragmentation)
183
+ - **Large files with high cohesion** → May be OK if under context budget threshold
184
+ - **Small scattered files** → Consolidate into domain modules
185
+
186
+ **The tool helps you identify the right balance!**
187
+
188
+ ### 📋 Quick Reference Table
189
+
190
+ | Metric | Good ✅ | Bad ❌ | Fix |
191
+ |--------|---------|--------|-----|
192
+ | **Context Budget** | < 10K tokens | > 25K tokens | Split large files |
193
+ | **Import Depth** | ≤ 5 levels | ≥ 8 levels | Flatten dependencies |
194
+ | **Cohesion** | > 0.6 (60%) | < 0.4 (40%) | Split by domain |
195
+ | **Fragmentation** | < 0.5 (50%) | > 0.7 (70%) | Consolidate domain |
196
+
197
+ **Rule of thumb:** The tool flags files that make AI's job harder (expensive to load, confusing to understand, scattered to find).
198
+
53
199
  ## 🚀 Installation
54
200
 
55
201
  ```bash
@@ -93,6 +239,13 @@ aiready-context ./src --output json --output-file custom-report.json
93
239
 
94
240
  > **💡 Tip:** By default, console output shows the top 10 results per category. Use `--max-results <number>` to see more, or use `--output json` to get complete details of all issues.
95
241
 
242
+ ### Understanding Threshold Tuning
243
+
244
+ Each parameter controls **when the tool flags a file as problematic**. Think of them as sensitivity dials:
245
+
246
+ - **Lower values** = More strict = More issues reported = More sensitive
247
+ - **Higher values** = More lenient = Fewer issues reported = Less sensitive
248
+
96
249
  ### Getting More/Fewer Results
97
250
 
98
251
  **Want to catch MORE potential issues?** (More sensitive, shows smaller problems)
@@ -100,23 +253,39 @@ aiready-context ./src --output json --output-file custom-report.json
100
253
  ```bash
101
254
  # Lower thresholds to be more strict:
102
255
  aiready-context ./src --max-depth 3 --max-context 5000 --min-cohesion 0.7 --max-fragmentation 0.4
256
+ # ↓ ↓ ↑ ↓
257
+ # Catches depth≥4 Catches 5K+ tokens Requires 70%+ cohesion Catches 40%+ fragmentation
103
258
  ```
104
259
 
260
+ **What this means:**
261
+ - `--max-depth 3`: Flag files with import depth ≥4 (stricter than default 5-7)
262
+ - `--max-context 5000`: Flag files needing 5K+ tokens (catches smaller files)
263
+ - `--min-cohesion 0.7`: Require 70%+ cohesion (stricter about mixed concerns)
264
+ - `--max-fragmentation 0.4`: Flag domains with 40%+ scatter (catches less severe fragmentation)
265
+
105
266
  **Want to see FEWER issues?** (Less noise, focus on critical problems only)
106
267
 
107
268
  ```bash
108
269
  # Raise thresholds to be more lenient:
109
270
  aiready-context ./src --max-depth 10 --max-context 30000 --min-cohesion 0.4 --max-fragmentation 0.8
271
+ # ↑ ↑ ↓ ↑
272
+ # Only depth≥11 Only 30K+ tokens Allows 40%+ cohesion Only 80%+ fragmentation
110
273
  ```
111
274
 
275
+ **What this means:**
276
+ - `--max-depth 10`: Only flag import depth ≥11 (very deep chains)
277
+ - `--max-context 30000`: Only flag files needing 30K+ tokens (only huge files)
278
+ - `--min-cohesion 0.4`: Accept 40%+ cohesion (more lenient about mixed concerns)
279
+ - `--max-fragmentation 0.8`: Only flag 80%+ scatter (only severely fragmented)
280
+
112
281
  ### Threshold Parameters Explained
113
282
 
114
- | Parameter | Default (Auto) | Lower = More Strict | Higher = Less Strict |
115
- |-----------|---------------|-------------------|---------------------|
116
- | `--max-depth` | 4-10* | Catches shallower imports | Only very deep chains |
117
- | `--max-context` | 8k-40k* | Catches smaller files | Only huge files |
118
- | `--min-cohesion` | 0.35-0.5* | Stricter about mixed concerns | More lenient |
119
- | `--max-fragmentation` | 0.5-0.8* | Catches less scattered code | Only severely scattered |
283
+ | Parameter | Default (Auto) | Lower = More Strict | Higher = Less Strict | Impact |
284
+ |-----------|---------------|-------------------|---------------------|--------|
285
+ | `--max-depth` | 4-10* | Catches shallower imports | Only very deep chains | More splits → flatter structure |
286
+ | `--max-context` | 8k-40k* | Catches smaller files | Only huge files | More splits → smaller modules |
287
+ | `--min-cohesion` | 0.35-0.5* | Stricter about mixed concerns | More lenient | More splits → focused files |
288
+ | `--max-fragmentation` | 0.5-0.8* | Catches less scattered code | Only severely scattered | More consolidation → domain modules |
120
289
 
121
290
  \* Auto-adjusted based on your repository size (100 files vs 2000+ files)
122
291
 
@@ -125,16 +294,36 @@ aiready-context ./src --max-depth 10 --max-context 30000 --min-cohesion 0.4 --ma
125
294
  **Small codebase getting too many warnings?**
126
295
  ```bash
127
296
  aiready-context ./src --max-depth 6 --min-cohesion 0.5
297
+ # Explanation: Allow slightly deeper imports and more mixed concerns
298
+ # Use when: Your codebase is naturally small and warnings feel excessive
128
299
  ```
129
300
 
130
301
  **Large codebase showing too few issues?**
131
302
  ```bash
132
303
  aiready-context ./src --max-depth 5 --max-context 15000
304
+ # Explanation: Be stricter about depth and context to catch more problems
305
+ # Use when: You know there are issues but they're not being detected
133
306
  ```
134
307
 
135
308
  **Focus on critical issues only:**
136
309
  ```bash
137
310
  aiready-context ./src --max-depth 8 --max-context 25000 --min-cohesion 0.3
311
+ # Explanation: Very lenient - only show the worst offenders
312
+ # Use when: Fixing warnings in stages, start with critical issues first
313
+ ```
314
+
315
+ **Preparing for AI refactoring sprint:**
316
+ ```bash
317
+ aiready-context ./src --max-depth 4 --max-context 8000 --min-cohesion 0.6 --max-fragmentation 0.5
318
+ # Explanation: Strict on all dimensions to get comprehensive issue list
319
+ # Use when: Planning a major refactoring effort, need complete audit
320
+ ```
321
+
322
+ **Microservices architecture (naturally fragmented):**
323
+ ```bash
324
+ aiready-context ./src --max-fragmentation 0.9
325
+ # Explanation: Very lenient on fragmentation (services are meant to be separate)
326
+ # Use when: Analyzing microservices where fragmentation is intentional
138
327
  ```
139
328
 
140
329
  ## 📤 Output Options
@@ -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.0",
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;