@aiready/context-analyzer 0.5.1 → 0.6.0

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
@@ -36,7 +36,7 @@ function buildDependencyGraph(files) {
36
36
  const edges = /* @__PURE__ */ new Map();
37
37
  for (const { file, content } of files) {
38
38
  const imports = extractImportsFromContent(content);
39
- const exports2 = extractExports(content);
39
+ const exports2 = extractExportsWithAST(content, file);
40
40
  const tokenCost = (0, import_core.estimateTokens)(content);
41
41
  const linesOfCode = content.split("\n").length;
42
42
  nodes.set(file, {
@@ -150,24 +150,12 @@ function detectCircularDependencies(graph) {
150
150
  }
151
151
  return cycles;
152
152
  }
153
- function calculateCohesion(exports2) {
154
- if (exports2.length === 0) return 1;
155
- if (exports2.length === 1) return 1;
156
- const domains = exports2.map((e) => e.inferredDomain || "unknown");
157
- const domainCounts = /* @__PURE__ */ new Map();
158
- for (const domain of domains) {
159
- domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
160
- }
161
- const total = domains.length;
162
- let entropy = 0;
163
- for (const count of domainCounts.values()) {
164
- const p = count / total;
165
- if (p > 0) {
166
- entropy -= p * Math.log2(p);
167
- }
168
- }
169
- const maxEntropy = Math.log2(total);
170
- return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
153
+ function calculateCohesion(exports2, filePath) {
154
+ return calculateEnhancedCohesion(exports2, filePath);
155
+ }
156
+ function isTestFile(filePath) {
157
+ const lower = filePath.toLowerCase();
158
+ return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
171
159
  }
172
160
  function calculateFragmentation(files, domain) {
173
161
  if (files.length <= 1) return 0;
@@ -194,7 +182,7 @@ function detectModuleClusters(graph) {
194
182
  const fragmentationScore = calculateFragmentation(files, domain);
195
183
  const avgCohesion = files.reduce((sum, file) => {
196
184
  const node = graph.nodes.get(file);
197
- return sum + (node ? calculateCohesion(node.exports) : 0);
185
+ return sum + (node ? calculateCohesion(node.exports, file) : 0);
198
186
  }, 0) / files.length;
199
187
  const targetFiles = Math.max(1, Math.ceil(files.length / 3));
200
188
  const consolidationPlan = generateConsolidationPlan(
@@ -248,25 +236,33 @@ function extractExports(content) {
248
236
  function inferDomain(name) {
249
237
  const lower = name.toLowerCase();
250
238
  const domainKeywords = [
251
- "user",
252
- "auth",
253
- "order",
254
- "product",
239
+ "authentication",
240
+ "authorization",
255
241
  "payment",
256
- "cart",
257
242
  "invoice",
258
243
  "customer",
244
+ "product",
245
+ "order",
246
+ "cart",
247
+ "user",
259
248
  "admin",
260
- "api",
261
- "util",
262
- "helper",
263
- "config",
264
- "service",
265
249
  "repository",
266
250
  "controller",
251
+ "service",
252
+ "config",
267
253
  "model",
268
- "view"
254
+ "view",
255
+ "auth",
256
+ "api",
257
+ "helper",
258
+ "util"
269
259
  ];
260
+ for (const keyword of domainKeywords) {
261
+ const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
262
+ if (wordBoundaryPattern.test(name)) {
263
+ return keyword;
264
+ }
265
+ }
270
266
  for (const keyword of domainKeywords) {
271
267
  if (lower.includes(keyword)) {
272
268
  return keyword;
@@ -300,6 +296,78 @@ function generateConsolidationPlan(domain, files, targetFiles) {
300
296
  );
301
297
  return plan;
302
298
  }
299
+ function extractExportsWithAST(content, filePath) {
300
+ try {
301
+ const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
302
+ return astExports.map((exp) => ({
303
+ name: exp.name,
304
+ type: exp.type,
305
+ inferredDomain: inferDomain(exp.name),
306
+ imports: exp.imports,
307
+ dependencies: exp.dependencies
308
+ }));
309
+ } catch (error) {
310
+ return extractExports(content);
311
+ }
312
+ }
313
+ function calculateEnhancedCohesion(exports2, filePath) {
314
+ if (exports2.length === 0) return 1;
315
+ if (exports2.length === 1) return 1;
316
+ if (filePath && isTestFile(filePath)) {
317
+ return 1;
318
+ }
319
+ const domainCohesion = calculateDomainCohesion(exports2);
320
+ const hasImportData = exports2.some((e) => e.imports && e.imports.length > 0);
321
+ if (!hasImportData) {
322
+ return domainCohesion;
323
+ }
324
+ const importCohesion = calculateImportBasedCohesion(exports2);
325
+ return importCohesion * 0.6 + domainCohesion * 0.4;
326
+ }
327
+ function calculateImportBasedCohesion(exports2) {
328
+ const exportsWithImports = exports2.filter((e) => e.imports && e.imports.length > 0);
329
+ if (exportsWithImports.length < 2) {
330
+ return 1;
331
+ }
332
+ let totalSimilarity = 0;
333
+ let comparisons = 0;
334
+ for (let i = 0; i < exportsWithImports.length; i++) {
335
+ for (let j = i + 1; j < exportsWithImports.length; j++) {
336
+ const exp1 = exportsWithImports[i];
337
+ const exp2 = exportsWithImports[j];
338
+ const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
339
+ totalSimilarity += similarity;
340
+ comparisons++;
341
+ }
342
+ }
343
+ return comparisons > 0 ? totalSimilarity / comparisons : 1;
344
+ }
345
+ function calculateJaccardSimilarity(arr1, arr2) {
346
+ if (arr1.length === 0 && arr2.length === 0) return 1;
347
+ if (arr1.length === 0 || arr2.length === 0) return 0;
348
+ const set1 = new Set(arr1);
349
+ const set2 = new Set(arr2);
350
+ const intersection = new Set([...set1].filter((x) => set2.has(x)));
351
+ const union = /* @__PURE__ */ new Set([...set1, ...set2]);
352
+ return intersection.size / union.size;
353
+ }
354
+ function calculateDomainCohesion(exports2) {
355
+ const domains = exports2.map((e) => e.inferredDomain || "unknown");
356
+ const domainCounts = /* @__PURE__ */ new Map();
357
+ for (const domain of domains) {
358
+ domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
359
+ }
360
+ const total = domains.length;
361
+ let entropy = 0;
362
+ for (const count of domainCounts.values()) {
363
+ const p = count / total;
364
+ if (p > 0) {
365
+ entropy -= p * Math.log2(p);
366
+ }
367
+ }
368
+ const maxEntropy = Math.log2(total);
369
+ return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
370
+ }
303
371
 
304
372
  // src/index.ts
305
373
  async function analyzeContext(options) {
@@ -341,7 +409,7 @@ async function analyzeContext(options) {
341
409
  const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
342
410
  const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
343
411
  const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
344
- const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports) : 1;
412
+ const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
345
413
  const fragmentationScore = fragmentationMap.get(file) || 0;
346
414
  const relatedFiles = [];
347
415
  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-DD7UVNE3.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ function buildDependencyGraph(files) {
34
34
  const edges = /* @__PURE__ */ new Map();
35
35
  for (const { file, content } of files) {
36
36
  const imports = extractImportsFromContent(content);
37
- const exports2 = extractExports(content);
37
+ const exports2 = extractExportsWithAST(content, file);
38
38
  const tokenCost = (0, import_core.estimateTokens)(content);
39
39
  const linesOfCode = content.split("\n").length;
40
40
  nodes.set(file, {
@@ -148,24 +148,12 @@ function detectCircularDependencies(graph) {
148
148
  }
149
149
  return cycles;
150
150
  }
151
- function calculateCohesion(exports2) {
152
- if (exports2.length === 0) return 1;
153
- if (exports2.length === 1) return 1;
154
- const domains = exports2.map((e) => e.inferredDomain || "unknown");
155
- const domainCounts = /* @__PURE__ */ new Map();
156
- for (const domain of domains) {
157
- domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
158
- }
159
- const total = domains.length;
160
- let entropy = 0;
161
- for (const count of domainCounts.values()) {
162
- const p = count / total;
163
- if (p > 0) {
164
- entropy -= p * Math.log2(p);
165
- }
166
- }
167
- const maxEntropy = Math.log2(total);
168
- return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
151
+ function calculateCohesion(exports2, filePath) {
152
+ return calculateEnhancedCohesion(exports2, filePath);
153
+ }
154
+ function isTestFile(filePath) {
155
+ const lower = filePath.toLowerCase();
156
+ return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
169
157
  }
170
158
  function calculateFragmentation(files, domain) {
171
159
  if (files.length <= 1) return 0;
@@ -192,7 +180,7 @@ function detectModuleClusters(graph) {
192
180
  const fragmentationScore = calculateFragmentation(files, domain);
193
181
  const avgCohesion = files.reduce((sum, file) => {
194
182
  const node = graph.nodes.get(file);
195
- return sum + (node ? calculateCohesion(node.exports) : 0);
183
+ return sum + (node ? calculateCohesion(node.exports, file) : 0);
196
184
  }, 0) / files.length;
197
185
  const targetFiles = Math.max(1, Math.ceil(files.length / 3));
198
186
  const consolidationPlan = generateConsolidationPlan(
@@ -246,25 +234,33 @@ function extractExports(content) {
246
234
  function inferDomain(name) {
247
235
  const lower = name.toLowerCase();
248
236
  const domainKeywords = [
249
- "user",
250
- "auth",
251
- "order",
252
- "product",
237
+ "authentication",
238
+ "authorization",
253
239
  "payment",
254
- "cart",
255
240
  "invoice",
256
241
  "customer",
242
+ "product",
243
+ "order",
244
+ "cart",
245
+ "user",
257
246
  "admin",
258
- "api",
259
- "util",
260
- "helper",
261
- "config",
262
- "service",
263
247
  "repository",
264
248
  "controller",
249
+ "service",
250
+ "config",
265
251
  "model",
266
- "view"
252
+ "view",
253
+ "auth",
254
+ "api",
255
+ "helper",
256
+ "util"
267
257
  ];
258
+ for (const keyword of domainKeywords) {
259
+ const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
260
+ if (wordBoundaryPattern.test(name)) {
261
+ return keyword;
262
+ }
263
+ }
268
264
  for (const keyword of domainKeywords) {
269
265
  if (lower.includes(keyword)) {
270
266
  return keyword;
@@ -298,6 +294,78 @@ function generateConsolidationPlan(domain, files, targetFiles) {
298
294
  );
299
295
  return plan;
300
296
  }
297
+ function extractExportsWithAST(content, filePath) {
298
+ try {
299
+ const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
300
+ return astExports.map((exp) => ({
301
+ name: exp.name,
302
+ type: exp.type,
303
+ inferredDomain: inferDomain(exp.name),
304
+ imports: exp.imports,
305
+ dependencies: exp.dependencies
306
+ }));
307
+ } catch (error) {
308
+ return extractExports(content);
309
+ }
310
+ }
311
+ function calculateEnhancedCohesion(exports2, filePath) {
312
+ if (exports2.length === 0) return 1;
313
+ if (exports2.length === 1) return 1;
314
+ if (filePath && isTestFile(filePath)) {
315
+ return 1;
316
+ }
317
+ const domainCohesion = calculateDomainCohesion(exports2);
318
+ const hasImportData = exports2.some((e) => e.imports && e.imports.length > 0);
319
+ if (!hasImportData) {
320
+ return domainCohesion;
321
+ }
322
+ const importCohesion = calculateImportBasedCohesion(exports2);
323
+ return importCohesion * 0.6 + domainCohesion * 0.4;
324
+ }
325
+ function calculateImportBasedCohesion(exports2) {
326
+ const exportsWithImports = exports2.filter((e) => e.imports && e.imports.length > 0);
327
+ if (exportsWithImports.length < 2) {
328
+ return 1;
329
+ }
330
+ let totalSimilarity = 0;
331
+ let comparisons = 0;
332
+ for (let i = 0; i < exportsWithImports.length; i++) {
333
+ for (let j = i + 1; j < exportsWithImports.length; j++) {
334
+ const exp1 = exportsWithImports[i];
335
+ const exp2 = exportsWithImports[j];
336
+ const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
337
+ totalSimilarity += similarity;
338
+ comparisons++;
339
+ }
340
+ }
341
+ return comparisons > 0 ? totalSimilarity / comparisons : 1;
342
+ }
343
+ function calculateJaccardSimilarity(arr1, arr2) {
344
+ if (arr1.length === 0 && arr2.length === 0) return 1;
345
+ if (arr1.length === 0 || arr2.length === 0) return 0;
346
+ const set1 = new Set(arr1);
347
+ const set2 = new Set(arr2);
348
+ const intersection = new Set([...set1].filter((x) => set2.has(x)));
349
+ const union = /* @__PURE__ */ new Set([...set1, ...set2]);
350
+ return intersection.size / union.size;
351
+ }
352
+ function calculateDomainCohesion(exports2) {
353
+ const domains = exports2.map((e) => e.inferredDomain || "unknown");
354
+ const domainCounts = /* @__PURE__ */ new Map();
355
+ for (const domain of domains) {
356
+ domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
357
+ }
358
+ const total = domains.length;
359
+ let entropy = 0;
360
+ for (const count of domainCounts.values()) {
361
+ const p = count / total;
362
+ if (p > 0) {
363
+ entropy -= p * Math.log2(p);
364
+ }
365
+ }
366
+ const maxEntropy = Math.log2(total);
367
+ return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
368
+ }
301
369
 
302
370
  // src/index.ts
303
371
  async function getSmartDefaults(directory, userOptions) {
@@ -383,7 +451,7 @@ async function analyzeContext(options) {
383
451
  const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
384
452
  const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
385
453
  const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
386
- const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports) : 1;
454
+ const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
387
455
  const fragmentationScore = fragmentationMap.get(file) || 0;
388
456
  const relatedFiles = [];
389
457
  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-DD7UVNE3.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.6.0",
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",
@@ -50,7 +50,7 @@
50
50
  "commander": "^12.1.0",
51
51
  "chalk": "^5.3.0",
52
52
  "prompts": "^2.4.2",
53
- "@aiready/core": "0.5.6"
53
+ "@aiready/core": "0.6.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "^22.10.2",
@@ -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', () => {
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { calculateCohesion } from '../analyzer';
3
+ import type { ExportInfo } from '../types';
4
+
5
+ describe('Enhanced Cohesion Calculation', () => {
6
+ it('should use domain-based cohesion when no import data available', () => {
7
+ const exports: ExportInfo[] = [
8
+ { name: 'getUserData', type: 'function', inferredDomain: 'user' },
9
+ { name: 'getProductData', type: 'function', inferredDomain: 'product' },
10
+ ];
11
+
12
+ const cohesion = calculateCohesion(exports);
13
+
14
+ // With mixed domains (user, product) and no import data, should use domain-based calculation
15
+ // Domain entropy for 2 different domains = low cohesion
16
+ expect(cohesion).toBeLessThan(0.5);
17
+ });
18
+
19
+ it('should use import-based cohesion when import data available', () => {
20
+ const exports: ExportInfo[] = [
21
+ {
22
+ name: 'getUserData',
23
+ type: 'function',
24
+ inferredDomain: 'user',
25
+ imports: ['react', 'axios', 'lodash'],
26
+ },
27
+ {
28
+ name: 'getProductData',
29
+ type: 'function',
30
+ inferredDomain: 'product',
31
+ imports: ['react', 'axios', 'lodash'], // Same imports!
32
+ },
33
+ ];
34
+
35
+ const cohesion = calculateCohesion(exports);
36
+
37
+ // Even though domains differ, imports are identical (Jaccard = 1.0)
38
+ // Enhanced cohesion = 0.6 * 1.0 + 0.4 * 0.0 (different domains) = 0.6
39
+ // Should be >= 0.6 (import-based weight)
40
+ expect(cohesion).toBeGreaterThanOrEqual(0.6);
41
+ });
42
+
43
+ it('should weight import-based similarity higher than domain-based', () => {
44
+ const exportsWithSharedImports: ExportInfo[] = [
45
+ {
46
+ name: 'getUserData',
47
+ type: 'function',
48
+ inferredDomain: 'user',
49
+ imports: ['react', 'axios'],
50
+ },
51
+ {
52
+ name: 'getProductData',
53
+ type: 'function',
54
+ inferredDomain: 'product',
55
+ imports: ['react', 'axios'],
56
+ },
57
+ ];
58
+
59
+ const exportsWithoutSharedImports: ExportInfo[] = [
60
+ {
61
+ name: 'getUserData',
62
+ type: 'function',
63
+ inferredDomain: 'user',
64
+ imports: ['react', 'axios'],
65
+ },
66
+ {
67
+ name: 'getProductData',
68
+ type: 'function',
69
+ inferredDomain: 'product',
70
+ imports: ['lodash', 'moment'],
71
+ },
72
+ ];
73
+
74
+ const cohesionWithShared = calculateCohesion(exportsWithSharedImports);
75
+ const cohesionWithoutShared = calculateCohesion(exportsWithoutSharedImports);
76
+
77
+ // Shared imports should result in higher cohesion
78
+ expect(cohesionWithShared).toBeGreaterThan(cohesionWithoutShared);
79
+ });
80
+
81
+ it('should handle mixed case: some exports with imports, some without', () => {
82
+ const exports: ExportInfo[] = [
83
+ {
84
+ name: 'getUserData',
85
+ type: 'function',
86
+ inferredDomain: 'user',
87
+ imports: ['react', 'axios'],
88
+ },
89
+ {
90
+ name: 'getProductData',
91
+ type: 'function',
92
+ inferredDomain: 'product',
93
+ // No imports field
94
+ },
95
+ ];
96
+
97
+ const cohesion = calculateCohesion(exports);
98
+
99
+ // Should fall back to domain-based when not all exports have import data
100
+ expect(cohesion).toBeGreaterThan(0);
101
+ expect(cohesion).toBeLessThan(1);
102
+ });
103
+
104
+ it('should return 1 for single export', () => {
105
+ const exports: ExportInfo[] = [
106
+ {
107
+ name: 'getUserData',
108
+ type: 'function',
109
+ inferredDomain: 'user',
110
+ imports: ['react'],
111
+ },
112
+ ];
113
+
114
+ expect(calculateCohesion(exports)).toBe(1);
115
+ });
116
+
117
+ it('should return 1 for test files regardless of domains or imports', () => {
118
+ const exports: ExportInfo[] = [
119
+ { name: 'testUserLogin', type: 'function', inferredDomain: 'user', imports: ['react'] },
120
+ { name: 'testProductView', type: 'function', inferredDomain: 'product', imports: [] },
121
+ ];
122
+
123
+ const cohesion = calculateCohesion(exports, 'src/utils/test-helpers.ts');
124
+ expect(cohesion).toBe(1);
125
+ });
126
+ });