@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/src/analyzer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { estimateTokens } from '@aiready/core';
1
+ import { estimateTokens, parseFileExports, calculateImportSimilarity, type ExportWithImports } from '@aiready/core';
2
2
  import type {
3
3
  ContextAnalysisResult,
4
4
  DependencyGraph,
@@ -24,7 +24,10 @@ export function buildDependencyGraph(
24
24
  // First pass: Create nodes
25
25
  for (const { file, content } of files) {
26
26
  const imports = extractImportsFromContent(content);
27
- const exports = extractExports(content);
27
+
28
+ // Use AST-based extraction for better accuracy, fallback to regex
29
+ const exports = extractExportsWithAST(content, file);
30
+
28
31
  const tokenCost = estimateTokens(content);
29
32
  const linesOfCode = content.split('\n').length;
30
33
 
@@ -199,33 +202,28 @@ export function detectCircularDependencies(
199
202
 
200
203
  /**
201
204
  * Calculate cohesion score (how related are exports in a file)
202
- * Uses entropy: low entropy = high cohesion
205
+ * Uses enhanced calculation combining domain-based and import-based analysis
206
+ * @param exports - Array of export information
207
+ * @param filePath - Optional file path for context-aware scoring
203
208
  */
204
- export function calculateCohesion(exports: ExportInfo[]): number {
205
- if (exports.length === 0) return 1;
206
- if (exports.length === 1) return 1; // Single export = perfect cohesion
207
-
208
- const domains = exports.map((e) => e.inferredDomain || 'unknown');
209
- const domainCounts = new Map<string, number>();
210
-
211
- for (const domain of domains) {
212
- domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
213
- }
214
-
215
- // Calculate Shannon entropy
216
- const total = domains.length;
217
- let entropy = 0;
218
-
219
- for (const count of domainCounts.values()) {
220
- const p = count / total;
221
- if (p > 0) {
222
- entropy -= p * Math.log2(p);
223
- }
224
- }
209
+ export function calculateCohesion(exports: ExportInfo[], filePath?: string): number {
210
+ return calculateEnhancedCohesion(exports, filePath);
211
+ }
225
212
 
226
- // Normalize to 0-1 (higher = better cohesion)
227
- const maxEntropy = Math.log2(total);
228
- return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
213
+ /**
214
+ * Check if a file is a test/mock/fixture file
215
+ */
216
+ function isTestFile(filePath: string): boolean {
217
+ const lower = filePath.toLowerCase();
218
+ return (
219
+ lower.includes('test') ||
220
+ lower.includes('spec') ||
221
+ lower.includes('mock') ||
222
+ lower.includes('fixture') ||
223
+ lower.includes('__tests__') ||
224
+ lower.includes('.test.') ||
225
+ lower.includes('.spec.')
226
+ );
229
227
  }
230
228
 
231
229
  /**
@@ -279,7 +277,7 @@ export function detectModuleClusters(
279
277
  const avgCohesion =
280
278
  files.reduce((sum, file) => {
281
279
  const node = graph.nodes.get(file);
282
- return sum + (node ? calculateCohesion(node.exports) : 0);
280
+ return sum + (node ? calculateCohesion(node.exports, file) : 0);
283
281
  }, 0) / files.length;
284
282
 
285
283
  // Generate consolidation plan
@@ -349,33 +347,45 @@ function extractExports(content: string): ExportInfo[] {
349
347
 
350
348
  /**
351
349
  * Infer domain from export name
352
- * Uses common naming patterns
350
+ * Uses common naming patterns with word boundary matching
353
351
  */
354
352
  function inferDomain(name: string): string {
355
353
  const lower = name.toLowerCase();
356
354
 
357
- // Common domain keywords
355
+ // Domain keywords ordered from most specific to most general
356
+ // This prevents generic terms like 'util' from matching before specific domains
358
357
  const domainKeywords = [
359
- 'user',
360
- 'auth',
361
- 'order',
362
- 'product',
358
+ 'authentication',
359
+ 'authorization',
363
360
  'payment',
364
- 'cart',
365
361
  'invoice',
366
362
  'customer',
363
+ 'product',
364
+ 'order',
365
+ 'cart',
366
+ 'user',
367
367
  'admin',
368
- 'api',
369
- 'util',
370
- 'helper',
371
- 'config',
372
- 'service',
373
368
  'repository',
374
369
  'controller',
370
+ 'service',
371
+ 'config',
375
372
  'model',
376
373
  'view',
374
+ 'auth',
375
+ 'api',
376
+ 'helper',
377
+ 'util',
377
378
  ];
378
379
 
380
+ // Try word boundary matching first for more accurate detection
381
+ for (const keyword of domainKeywords) {
382
+ const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, 'i');
383
+ if (wordBoundaryPattern.test(name)) {
384
+ return keyword;
385
+ }
386
+ }
387
+
388
+ // Fallback to substring matching for compound words
379
389
  for (const keyword of domainKeywords) {
380
390
  if (lower.includes(keyword)) {
381
391
  return keyword;
@@ -424,3 +434,131 @@ function generateConsolidationPlan(
424
434
 
425
435
  return plan;
426
436
  }
437
+
438
+ /**
439
+ * Extract exports using AST parsing (enhanced version)
440
+ * Falls back to regex if AST parsing fails
441
+ */
442
+ export function extractExportsWithAST(content: string, filePath: string): ExportInfo[] {
443
+ try {
444
+ const { exports: astExports } = parseFileExports(content, filePath);
445
+
446
+ return astExports.map(exp => ({
447
+ name: exp.name,
448
+ type: exp.type,
449
+ inferredDomain: inferDomain(exp.name),
450
+ imports: exp.imports,
451
+ dependencies: exp.dependencies,
452
+ }));
453
+ } catch (error) {
454
+ // Fallback to regex-based extraction
455
+ return extractExports(content);
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Calculate enhanced cohesion score using both domain inference and import similarity
461
+ *
462
+ * This combines:
463
+ * 1. Domain-based cohesion (entropy of inferred domains)
464
+ * 2. Import-based cohesion (Jaccard similarity of shared imports)
465
+ *
466
+ * Weight: 60% import-based, 40% domain-based (import analysis is more reliable)
467
+ */
468
+ export function calculateEnhancedCohesion(
469
+ exports: ExportInfo[],
470
+ filePath?: string
471
+ ): number {
472
+ if (exports.length === 0) return 1;
473
+ if (exports.length === 1) return 1;
474
+
475
+ // Special case for test files
476
+ if (filePath && isTestFile(filePath)) {
477
+ return 1;
478
+ }
479
+
480
+ // Calculate domain-based cohesion (existing method)
481
+ const domainCohesion = calculateDomainCohesion(exports);
482
+
483
+ // Calculate import-based cohesion if imports are available
484
+ const hasImportData = exports.some(e => e.imports && e.imports.length > 0);
485
+
486
+ if (!hasImportData) {
487
+ // No import data available, use domain-based only
488
+ return domainCohesion;
489
+ }
490
+
491
+ const importCohesion = calculateImportBasedCohesion(exports);
492
+
493
+ // Weighted combination: 60% import-based, 40% domain-based
494
+ return importCohesion * 0.6 + domainCohesion * 0.4;
495
+ }
496
+
497
+ /**
498
+ * Calculate cohesion based on shared imports (Jaccard similarity)
499
+ */
500
+ function calculateImportBasedCohesion(exports: ExportInfo[]): number {
501
+ const exportsWithImports = exports.filter(e => e.imports && e.imports.length > 0);
502
+
503
+ if (exportsWithImports.length < 2) {
504
+ return 1; // Not enough data
505
+ }
506
+
507
+ // Calculate pairwise import similarity
508
+ let totalSimilarity = 0;
509
+ let comparisons = 0;
510
+
511
+ for (let i = 0; i < exportsWithImports.length; i++) {
512
+ for (let j = i + 1; j < exportsWithImports.length; j++) {
513
+ const exp1 = exportsWithImports[i] as ExportInfo & { imports: string[] };
514
+ const exp2 = exportsWithImports[j] as ExportInfo & { imports: string[] };
515
+
516
+ const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
517
+ totalSimilarity += similarity;
518
+ comparisons++;
519
+ }
520
+ }
521
+
522
+ return comparisons > 0 ? totalSimilarity / comparisons : 1;
523
+ }
524
+
525
+ /**
526
+ * Calculate Jaccard similarity between two arrays
527
+ */
528
+ function calculateJaccardSimilarity(arr1: string[], arr2: string[]): number {
529
+ if (arr1.length === 0 && arr2.length === 0) return 1;
530
+ if (arr1.length === 0 || arr2.length === 0) return 0;
531
+
532
+ const set1 = new Set(arr1);
533
+ const set2 = new Set(arr2);
534
+
535
+ const intersection = new Set([...set1].filter(x => set2.has(x)));
536
+ const union = new Set([...set1, ...set2]);
537
+
538
+ return intersection.size / union.size;
539
+ }
540
+
541
+ /**
542
+ * Calculate domain-based cohesion (existing entropy method)
543
+ */
544
+ function calculateDomainCohesion(exports: ExportInfo[]): number {
545
+ const domains = exports.map((e) => e.inferredDomain || 'unknown');
546
+ const domainCounts = new Map<string, number>();
547
+
548
+ for (const domain of domains) {
549
+ domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
550
+ }
551
+
552
+ const total = domains.length;
553
+ let entropy = 0;
554
+
555
+ for (const count of domainCounts.values()) {
556
+ const p = count / total;
557
+ if (p > 0) {
558
+ entropy -= p * Math.log2(p);
559
+ }
560
+ }
561
+
562
+ const maxEntropy = Math.log2(total);
563
+ return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
564
+ }
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;
package/src/types.ts CHANGED
@@ -101,4 +101,7 @@ export interface ExportInfo {
101
101
  name: string;
102
102
  type: 'function' | 'class' | 'const' | 'type' | 'interface' | 'default';
103
103
  inferredDomain?: string; // Inferred from name/usage
104
+ imports?: string[]; // Imports used by this export (for import-based cohesion)
105
+ dependencies?: string[]; // Other exports from same file this depends on
104
106
  }
107
+