@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/.turbo/turbo-build.log +7 -7
- package/.turbo/turbo-test.log +10 -27
- package/COHESION-IMPROVEMENTS.md +202 -0
- package/dist/chunk-DD7UVNE3.mjs +678 -0
- package/dist/chunk-EX7HCWAO.mjs +625 -0
- package/dist/cli.js +100 -32
- package/dist/cli.mjs +1 -1
- package/dist/index.js +100 -32
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +24 -0
- package/src/__tests__/enhanced-cohesion.test.ts +126 -0
- package/src/analyzer.ts +178 -40
- package/src/index.ts +1 -1
- package/src/types.ts +3 -0
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
|
-
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
//
|
|
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
|
-
'
|
|
360
|
-
'
|
|
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
|
+
|