@aiready/context-analyzer 0.9.34 → 0.9.36
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/.github/FUNDING.yml +2 -2
- package/.turbo/turbo-build.log +9 -9
- package/.turbo/turbo-test.log +19 -19
- package/CONTRIBUTING.md +10 -2
- package/dist/chunk-7LUSCLGR.mjs +2058 -0
- package/dist/cli.js +346 -83
- package/dist/cli.mjs +137 -33
- package/dist/index.js +231 -56
- package/dist/index.mjs +1 -1
- package/dist/python-context-GOH747QU.mjs +202 -0
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +69 -17
- package/src/__tests__/auto-detection.test.ts +1 -1
- package/src/__tests__/enhanced-cohesion.test.ts +19 -7
- package/src/__tests__/file-classification.test.ts +188 -53
- package/src/__tests__/fragmentation-advanced.test.ts +2 -11
- package/src/__tests__/fragmentation-coupling.test.ts +8 -2
- package/src/__tests__/fragmentation-log.test.ts +9 -9
- package/src/__tests__/scoring.test.ts +19 -7
- package/src/__tests__/structural-cohesion.test.ts +33 -21
- package/src/analyzer.ts +724 -376
- package/src/analyzers/python-context.ts +33 -10
- package/src/cli.ts +223 -59
- package/src/index.ts +112 -55
- package/src/scoring.ts +53 -43
- package/src/semantic-analysis.ts +73 -55
- package/src/types.ts +12 -13
package/src/analyzer.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
estimateTokens,
|
|
3
|
+
parseFileExports,
|
|
4
|
+
calculateImportSimilarity,
|
|
5
|
+
} from '@aiready/core';
|
|
2
6
|
import type {
|
|
3
|
-
ContextAnalysisResult,
|
|
4
7
|
DependencyGraph,
|
|
5
8
|
DependencyNode,
|
|
6
9
|
ExportInfo,
|
|
7
10
|
ModuleCluster,
|
|
8
11
|
FileClassification,
|
|
9
12
|
} from './types';
|
|
10
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
buildCoUsageMatrix,
|
|
15
|
+
buildTypeGraph,
|
|
16
|
+
inferDomainFromSemantics,
|
|
17
|
+
} from './semantic-analysis';
|
|
11
18
|
|
|
12
19
|
interface FileContent {
|
|
13
20
|
file: string;
|
|
@@ -20,22 +27,45 @@ interface FileContent {
|
|
|
20
27
|
*/
|
|
21
28
|
function extractDomainKeywordsFromPaths(files: FileContent[]): string[] {
|
|
22
29
|
const folderNames = new Set<string>();
|
|
23
|
-
|
|
30
|
+
|
|
24
31
|
for (const { file } of files) {
|
|
25
32
|
const segments = file.split('/');
|
|
26
33
|
// Extract meaningful folder names (skip common infrastructure folders)
|
|
27
|
-
const skipFolders = new Set([
|
|
28
|
-
|
|
34
|
+
const skipFolders = new Set([
|
|
35
|
+
'src',
|
|
36
|
+
'lib',
|
|
37
|
+
'dist',
|
|
38
|
+
'build',
|
|
39
|
+
'node_modules',
|
|
40
|
+
'test',
|
|
41
|
+
'tests',
|
|
42
|
+
'__tests__',
|
|
43
|
+
'spec',
|
|
44
|
+
'e2e',
|
|
45
|
+
'scripts',
|
|
46
|
+
'components',
|
|
47
|
+
'utils',
|
|
48
|
+
'helpers',
|
|
49
|
+
'util',
|
|
50
|
+
'helper',
|
|
51
|
+
'api',
|
|
52
|
+
'apis',
|
|
53
|
+
]);
|
|
54
|
+
|
|
29
55
|
for (const segment of segments) {
|
|
30
56
|
const normalized = segment.toLowerCase();
|
|
31
|
-
if (
|
|
57
|
+
if (
|
|
58
|
+
normalized &&
|
|
59
|
+
!skipFolders.has(normalized) &&
|
|
60
|
+
!normalized.includes('.')
|
|
61
|
+
) {
|
|
32
62
|
// Singularize common plural forms for better matching
|
|
33
63
|
const singular = singularize(normalized);
|
|
34
64
|
folderNames.add(singular);
|
|
35
65
|
}
|
|
36
66
|
}
|
|
37
67
|
}
|
|
38
|
-
|
|
68
|
+
|
|
39
69
|
return Array.from(folderNames);
|
|
40
70
|
}
|
|
41
71
|
|
|
@@ -50,11 +80,11 @@ function singularize(word: string): string {
|
|
|
50
80
|
men: 'man',
|
|
51
81
|
women: 'woman',
|
|
52
82
|
};
|
|
53
|
-
|
|
83
|
+
|
|
54
84
|
if (irregulars[word]) {
|
|
55
85
|
return irregulars[word];
|
|
56
86
|
}
|
|
57
|
-
|
|
87
|
+
|
|
58
88
|
// Common plural patterns
|
|
59
89
|
if (word.endsWith('ies')) {
|
|
60
90
|
return word.slice(0, -3) + 'y'; // categories -> category
|
|
@@ -65,29 +95,35 @@ function singularize(word: string): string {
|
|
|
65
95
|
if (word.endsWith('s') && word.length > 3) {
|
|
66
96
|
return word.slice(0, -1); // orders -> order
|
|
67
97
|
}
|
|
68
|
-
|
|
98
|
+
|
|
69
99
|
return word;
|
|
70
100
|
}
|
|
71
101
|
|
|
72
102
|
/**
|
|
73
103
|
* Build a dependency graph from file contents
|
|
74
104
|
*/
|
|
75
|
-
export function buildDependencyGraph(
|
|
76
|
-
files: FileContent[],
|
|
77
|
-
): DependencyGraph {
|
|
105
|
+
export function buildDependencyGraph(files: FileContent[]): DependencyGraph {
|
|
78
106
|
const nodes = new Map<string, DependencyNode>();
|
|
79
107
|
const edges = new Map<string, Set<string>>();
|
|
80
108
|
|
|
81
109
|
// Auto-detect domain keywords from workspace folder structure
|
|
82
110
|
const autoDetectedKeywords = extractDomainKeywordsFromPaths(files);
|
|
83
111
|
|
|
112
|
+
// Some imported helpers are optional for future features; reference to avoid lint warnings
|
|
113
|
+
void calculateImportSimilarity;
|
|
114
|
+
|
|
84
115
|
// First pass: Create nodes with folder-based domain inference
|
|
85
116
|
for (const { file, content } of files) {
|
|
86
117
|
const imports = extractImportsFromContent(content);
|
|
87
|
-
|
|
118
|
+
|
|
88
119
|
// Use AST-based extraction for better accuracy, fallback to regex
|
|
89
|
-
const exports = extractExportsWithAST(
|
|
90
|
-
|
|
120
|
+
const exports = extractExportsWithAST(
|
|
121
|
+
content,
|
|
122
|
+
file,
|
|
123
|
+
{ domainKeywords: autoDetectedKeywords },
|
|
124
|
+
imports
|
|
125
|
+
);
|
|
126
|
+
|
|
91
127
|
const tokenCost = estimateTokens(content);
|
|
92
128
|
const linesOfCode = content.split('\n').length;
|
|
93
129
|
|
|
@@ -106,7 +142,7 @@ export function buildDependencyGraph(
|
|
|
106
142
|
const graph: DependencyGraph = { nodes, edges };
|
|
107
143
|
const coUsageMatrix = buildCoUsageMatrix(graph);
|
|
108
144
|
const typeGraph = buildTypeGraph(graph);
|
|
109
|
-
|
|
145
|
+
|
|
110
146
|
// Add semantic data to graph
|
|
111
147
|
graph.coUsageMatrix = coUsageMatrix;
|
|
112
148
|
graph.typeGraph = typeGraph;
|
|
@@ -123,10 +159,10 @@ export function buildDependencyGraph(
|
|
|
123
159
|
typeGraph,
|
|
124
160
|
exp.typeReferences
|
|
125
161
|
);
|
|
126
|
-
|
|
162
|
+
|
|
127
163
|
// Add multi-domain assignments with confidence scores
|
|
128
164
|
exp.domains = semanticAssignments;
|
|
129
|
-
|
|
165
|
+
|
|
130
166
|
// Keep inferredDomain for backwards compatibility (use highest confidence)
|
|
131
167
|
if (semanticAssignments.length > 0) {
|
|
132
168
|
exp.inferredDomain = semanticAssignments[0].domain;
|
|
@@ -248,9 +284,7 @@ export function calculateContextBudget(
|
|
|
248
284
|
/**
|
|
249
285
|
* Detect circular dependencies
|
|
250
286
|
*/
|
|
251
|
-
export function detectCircularDependencies(
|
|
252
|
-
graph: DependencyGraph
|
|
253
|
-
): string[][] {
|
|
287
|
+
export function detectCircularDependencies(graph: DependencyGraph): string[][] {
|
|
254
288
|
const cycles: string[][] = [];
|
|
255
289
|
const visited = new Set<string>();
|
|
256
290
|
const recursionStack = new Set<string>();
|
|
@@ -301,7 +335,14 @@ export function detectCircularDependencies(
|
|
|
301
335
|
export function calculateCohesion(
|
|
302
336
|
exports: ExportInfo[],
|
|
303
337
|
filePath?: string,
|
|
304
|
-
options?: {
|
|
338
|
+
options?: {
|
|
339
|
+
coUsageMatrix?: Map<string, Map<string, number>>;
|
|
340
|
+
weights?: {
|
|
341
|
+
importBased?: number;
|
|
342
|
+
structural?: number;
|
|
343
|
+
domainBased?: number;
|
|
344
|
+
};
|
|
345
|
+
}
|
|
305
346
|
): number {
|
|
306
347
|
return calculateEnhancedCohesion(exports, filePath, options);
|
|
307
348
|
}
|
|
@@ -333,7 +374,9 @@ export function calculateFragmentation(
|
|
|
333
374
|
if (files.length <= 1) return 0; // Single file = no fragmentation
|
|
334
375
|
|
|
335
376
|
// Calculate how many different directories contain these files
|
|
336
|
-
const directories = new Set(
|
|
377
|
+
const directories = new Set(
|
|
378
|
+
files.map((f) => f.split('/').slice(0, -1).join('/'))
|
|
379
|
+
);
|
|
337
380
|
const uniqueDirs = directories.size;
|
|
338
381
|
|
|
339
382
|
// If log-scaling requested, normalize using logarithms so that
|
|
@@ -450,7 +493,9 @@ export function detectModuleClusters(
|
|
|
450
493
|
return sum + (node?.tokenCost || 0);
|
|
451
494
|
}, 0);
|
|
452
495
|
|
|
453
|
-
const baseFragmentation = calculateFragmentation(files, domain, {
|
|
496
|
+
const baseFragmentation = calculateFragmentation(files, domain, {
|
|
497
|
+
useLogScale: !!options?.useLogScale,
|
|
498
|
+
});
|
|
454
499
|
|
|
455
500
|
// Compute import-based cohesion across files in this domain cluster.
|
|
456
501
|
// This measures how much the files actually "talk" to each other.
|
|
@@ -466,16 +511,18 @@ export function detectModuleClusters(
|
|
|
466
511
|
const n2 = graph.nodes.get(f2)?.imports || [];
|
|
467
512
|
|
|
468
513
|
// Treat two empty import lists as not coupled (similarity 0)
|
|
469
|
-
const similarity =
|
|
470
|
-
|
|
471
|
-
|
|
514
|
+
const similarity =
|
|
515
|
+
n1.length === 0 && n2.length === 0
|
|
516
|
+
? 0
|
|
517
|
+
: calculateJaccardSimilarity(n1, n2);
|
|
472
518
|
|
|
473
519
|
importSimilarityTotal += similarity;
|
|
474
520
|
importComparisons++;
|
|
475
521
|
}
|
|
476
522
|
}
|
|
477
523
|
|
|
478
|
-
const importCohesion =
|
|
524
|
+
const importCohesion =
|
|
525
|
+
importComparisons > 0 ? importSimilarityTotal / importComparisons : 0;
|
|
479
526
|
|
|
480
527
|
// Coupling discount: if files are heavily importing each other, reduce fragmentation penalty.
|
|
481
528
|
// Following recommendation: up to 20% discount proportional to import cohesion.
|
|
@@ -490,7 +537,14 @@ export function detectModuleClusters(
|
|
|
490
537
|
const avgCohesion =
|
|
491
538
|
files.reduce((sum, file) => {
|
|
492
539
|
const node = graph.nodes.get(file);
|
|
493
|
-
return
|
|
540
|
+
return (
|
|
541
|
+
sum +
|
|
542
|
+
(node
|
|
543
|
+
? calculateCohesion(node.exports, file, {
|
|
544
|
+
coUsageMatrix: graph.coUsageMatrix,
|
|
545
|
+
})
|
|
546
|
+
: 0)
|
|
547
|
+
);
|
|
494
548
|
}, 0) / files.length;
|
|
495
549
|
|
|
496
550
|
// Generate consolidation plan
|
|
@@ -528,7 +582,11 @@ export function detectModuleClusters(
|
|
|
528
582
|
function extractExports(
|
|
529
583
|
content: string,
|
|
530
584
|
filePath?: string,
|
|
531
|
-
domainOptions?: {
|
|
585
|
+
domainOptions?: {
|
|
586
|
+
domainKeywords?: string[];
|
|
587
|
+
domainPatterns?: string[];
|
|
588
|
+
pathDomainMap?: Record<string, string>;
|
|
589
|
+
},
|
|
532
590
|
fileImports?: string[]
|
|
533
591
|
): ExportInfo[] {
|
|
534
592
|
const exports: ExportInfo[] = [];
|
|
@@ -557,7 +615,12 @@ function extractExports(
|
|
|
557
615
|
while ((match = pattern.exec(content)) !== null) {
|
|
558
616
|
const name = match[1] || 'default';
|
|
559
617
|
const type = types[index];
|
|
560
|
-
const inferredDomain = inferDomain(
|
|
618
|
+
const inferredDomain = inferDomain(
|
|
619
|
+
name,
|
|
620
|
+
filePath,
|
|
621
|
+
domainOptions,
|
|
622
|
+
fileImports
|
|
623
|
+
);
|
|
561
624
|
|
|
562
625
|
exports.push({ name, type, inferredDomain });
|
|
563
626
|
}
|
|
@@ -612,9 +675,10 @@ function inferDomain(
|
|
|
612
675
|
'auth',
|
|
613
676
|
];
|
|
614
677
|
|
|
615
|
-
const domainKeywords =
|
|
616
|
-
|
|
617
|
-
|
|
678
|
+
const domainKeywords =
|
|
679
|
+
domainOptions?.domainKeywords && domainOptions.domainKeywords.length
|
|
680
|
+
? [...domainOptions.domainKeywords, ...defaultKeywords]
|
|
681
|
+
: defaultKeywords;
|
|
618
682
|
|
|
619
683
|
// Try word boundary matching first for more accurate detection
|
|
620
684
|
for (const keyword of domainKeywords) {
|
|
@@ -637,23 +701,29 @@ function inferDomain(
|
|
|
637
701
|
// e.g., '@/orders/service' -> ['orders', 'service']
|
|
638
702
|
// '../payments/processor' -> ['payments', 'processor']
|
|
639
703
|
const allSegments = importPath.split('/');
|
|
640
|
-
const relevantSegments = allSegments
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
704
|
+
const relevantSegments = allSegments
|
|
705
|
+
.filter((s) => {
|
|
706
|
+
if (!s) return false;
|
|
707
|
+
// Skip '.' and '..' but keep everything else
|
|
708
|
+
if (s === '.' || s === '..') return false;
|
|
709
|
+
// Skip '@' prefix but keep the path after it
|
|
710
|
+
if (s.startsWith('@') && s.length === 1) return false;
|
|
711
|
+
// Remove '@' prefix from scoped imports like '@/orders'
|
|
712
|
+
return true;
|
|
713
|
+
})
|
|
714
|
+
.map((s) => (s.startsWith('@') ? s.slice(1) : s));
|
|
715
|
+
|
|
650
716
|
for (const segment of relevantSegments) {
|
|
651
717
|
const segLower = segment.toLowerCase();
|
|
652
718
|
const singularSegment = singularize(segLower);
|
|
653
|
-
|
|
719
|
+
|
|
654
720
|
// Check if any domain keyword matches the import path segment (with singularization)
|
|
655
721
|
for (const keyword of domainKeywords) {
|
|
656
|
-
if (
|
|
722
|
+
if (
|
|
723
|
+
singularSegment === keyword ||
|
|
724
|
+
segLower === keyword ||
|
|
725
|
+
segLower.includes(keyword)
|
|
726
|
+
) {
|
|
657
727
|
return keyword;
|
|
658
728
|
}
|
|
659
729
|
}
|
|
@@ -667,9 +737,13 @@ function inferDomain(
|
|
|
667
737
|
const pathSegments = filePath.toLowerCase().split('/');
|
|
668
738
|
for (const segment of pathSegments) {
|
|
669
739
|
const singularSegment = singularize(segment);
|
|
670
|
-
|
|
740
|
+
|
|
671
741
|
for (const keyword of domainKeywords) {
|
|
672
|
-
if (
|
|
742
|
+
if (
|
|
743
|
+
singularSegment === keyword ||
|
|
744
|
+
segment === keyword ||
|
|
745
|
+
segment.includes(keyword)
|
|
746
|
+
) {
|
|
673
747
|
return keyword;
|
|
674
748
|
}
|
|
675
749
|
}
|
|
@@ -731,15 +805,22 @@ export function extractExportsWithAST(
|
|
|
731
805
|
): ExportInfo[] {
|
|
732
806
|
try {
|
|
733
807
|
const { exports: astExports } = parseFileExports(content, filePath);
|
|
734
|
-
|
|
735
|
-
return astExports.map(exp => ({
|
|
808
|
+
|
|
809
|
+
return astExports.map((exp) => ({
|
|
736
810
|
name: exp.name,
|
|
737
811
|
type: exp.type,
|
|
738
|
-
inferredDomain: inferDomain(
|
|
812
|
+
inferredDomain: inferDomain(
|
|
813
|
+
exp.name,
|
|
814
|
+
filePath,
|
|
815
|
+
domainOptions,
|
|
816
|
+
fileImports
|
|
817
|
+
),
|
|
739
818
|
imports: exp.imports,
|
|
740
819
|
dependencies: exp.dependencies,
|
|
741
820
|
}));
|
|
742
821
|
} catch (error) {
|
|
822
|
+
// Avoid unused variable lint
|
|
823
|
+
void error;
|
|
743
824
|
// Fallback to regex-based extraction
|
|
744
825
|
return extractExports(content, filePath, domainOptions, fileImports);
|
|
745
826
|
}
|
|
@@ -747,17 +828,24 @@ export function extractExportsWithAST(
|
|
|
747
828
|
|
|
748
829
|
/**
|
|
749
830
|
* Calculate enhanced cohesion score using both domain inference and import similarity
|
|
750
|
-
*
|
|
831
|
+
*
|
|
751
832
|
* This combines:
|
|
752
833
|
* 1. Domain-based cohesion (entropy of inferred domains)
|
|
753
834
|
* 2. Import-based cohesion (Jaccard similarity of shared imports)
|
|
754
|
-
*
|
|
835
|
+
*
|
|
755
836
|
* Weight: 60% import-based, 40% domain-based (import analysis is more reliable)
|
|
756
837
|
*/
|
|
757
838
|
export function calculateEnhancedCohesion(
|
|
758
839
|
exports: ExportInfo[],
|
|
759
840
|
filePath?: string,
|
|
760
|
-
options?: {
|
|
841
|
+
options?: {
|
|
842
|
+
coUsageMatrix?: Map<string, Map<string, number>>;
|
|
843
|
+
weights?: {
|
|
844
|
+
importBased?: number;
|
|
845
|
+
structural?: number;
|
|
846
|
+
domainBased?: number;
|
|
847
|
+
};
|
|
848
|
+
}
|
|
761
849
|
): number {
|
|
762
850
|
if (exports.length === 0) return 1;
|
|
763
851
|
if (exports.length === 1) return 1;
|
|
@@ -772,27 +860,41 @@ export function calculateEnhancedCohesion(
|
|
|
772
860
|
|
|
773
861
|
// Calculate import-based cohesion if imports are available
|
|
774
862
|
const hasImportData = exports.some((e) => e.imports && e.imports.length > 0);
|
|
775
|
-
const importCohesion = hasImportData
|
|
863
|
+
const importCohesion = hasImportData
|
|
864
|
+
? calculateImportBasedCohesion(exports)
|
|
865
|
+
: undefined;
|
|
776
866
|
|
|
777
867
|
// Calculate structural cohesion (co-usage) if coUsageMatrix and filePath available
|
|
778
868
|
const coUsageMatrix = options?.coUsageMatrix;
|
|
779
|
-
const structuralCohesion =
|
|
869
|
+
const structuralCohesion =
|
|
870
|
+
filePath && coUsageMatrix
|
|
871
|
+
? calculateStructuralCohesionFromCoUsage(filePath, coUsageMatrix)
|
|
872
|
+
: undefined;
|
|
780
873
|
|
|
781
874
|
// Default weights (can be overridden via options)
|
|
782
|
-
const defaultWeights = {
|
|
875
|
+
const defaultWeights = {
|
|
876
|
+
importBased: 0.5,
|
|
877
|
+
structural: 0.3,
|
|
878
|
+
domainBased: 0.2,
|
|
879
|
+
};
|
|
783
880
|
const weights = { ...defaultWeights, ...(options?.weights || {}) };
|
|
784
881
|
|
|
785
882
|
// Collect available signals and normalize weights
|
|
786
883
|
const signals: Array<{ score: number; weight: number }> = [];
|
|
787
|
-
if (importCohesion !== undefined)
|
|
788
|
-
|
|
884
|
+
if (importCohesion !== undefined)
|
|
885
|
+
signals.push({ score: importCohesion, weight: weights.importBased });
|
|
886
|
+
if (structuralCohesion !== undefined)
|
|
887
|
+
signals.push({ score: structuralCohesion, weight: weights.structural });
|
|
789
888
|
// domain cohesion is always available
|
|
790
889
|
signals.push({ score: domainCohesion, weight: weights.domainBased });
|
|
791
890
|
|
|
792
891
|
const totalWeight = signals.reduce((s, el) => s + el.weight, 0);
|
|
793
892
|
if (totalWeight === 0) return domainCohesion;
|
|
794
893
|
|
|
795
|
-
const combined = signals.reduce(
|
|
894
|
+
const combined = signals.reduce(
|
|
895
|
+
(sum, el) => sum + el.score * (el.weight / totalWeight),
|
|
896
|
+
0
|
|
897
|
+
);
|
|
796
898
|
return combined;
|
|
797
899
|
}
|
|
798
900
|
|
|
@@ -838,8 +940,10 @@ export function calculateStructuralCohesionFromCoUsage(
|
|
|
838
940
|
* Calculate cohesion based on shared imports (Jaccard similarity)
|
|
839
941
|
*/
|
|
840
942
|
function calculateImportBasedCohesion(exports: ExportInfo[]): number {
|
|
841
|
-
const exportsWithImports = exports.filter(
|
|
842
|
-
|
|
943
|
+
const exportsWithImports = exports.filter(
|
|
944
|
+
(e) => e.imports && e.imports.length > 0
|
|
945
|
+
);
|
|
946
|
+
|
|
843
947
|
if (exportsWithImports.length < 2) {
|
|
844
948
|
return 1; // Not enough data
|
|
845
949
|
}
|
|
@@ -852,7 +956,7 @@ function calculateImportBasedCohesion(exports: ExportInfo[]): number {
|
|
|
852
956
|
for (let j = i + 1; j < exportsWithImports.length; j++) {
|
|
853
957
|
const exp1 = exportsWithImports[i] as ExportInfo & { imports: string[] };
|
|
854
958
|
const exp2 = exportsWithImports[j] as ExportInfo & { imports: string[] };
|
|
855
|
-
|
|
959
|
+
|
|
856
960
|
const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
|
|
857
961
|
totalSimilarity += similarity;
|
|
858
962
|
comparisons++;
|
|
@@ -871,10 +975,10 @@ function calculateJaccardSimilarity(arr1: string[], arr2: string[]): number {
|
|
|
871
975
|
|
|
872
976
|
const set1 = new Set(arr1);
|
|
873
977
|
const set2 = new Set(arr2);
|
|
874
|
-
|
|
875
|
-
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
978
|
+
|
|
979
|
+
const intersection = new Set([...set1].filter((x) => set2.has(x)));
|
|
876
980
|
const union = new Set([...set1, ...set2]);
|
|
877
|
-
|
|
981
|
+
|
|
878
982
|
return intersection.size / union.size;
|
|
879
983
|
}
|
|
880
984
|
|
|
@@ -906,7 +1010,7 @@ function calculateDomainCohesion(exports: ExportInfo[]): number {
|
|
|
906
1010
|
/**
|
|
907
1011
|
* Classify a file based on its characteristics to help distinguish
|
|
908
1012
|
* real issues from false positives.
|
|
909
|
-
*
|
|
1013
|
+
*
|
|
910
1014
|
* Classification types:
|
|
911
1015
|
* - barrel-export: Re-exports from other modules (index.ts files)
|
|
912
1016
|
* - type-definition: Primarily type/interface definitions
|
|
@@ -925,57 +1029,61 @@ export function classifyFile(
|
|
|
925
1029
|
domains: string[]
|
|
926
1030
|
): FileClassification {
|
|
927
1031
|
const { exports, imports, linesOfCode, file } = node;
|
|
928
|
-
|
|
1032
|
+
|
|
1033
|
+
// Some node fields are inspected by heuristics later; reference to avoid lint warnings
|
|
1034
|
+
void imports;
|
|
1035
|
+
void linesOfCode;
|
|
1036
|
+
|
|
929
1037
|
// 1. Check for barrel export (index file that re-exports)
|
|
930
1038
|
if (isBarrelExport(node)) {
|
|
931
1039
|
return 'barrel-export';
|
|
932
1040
|
}
|
|
933
|
-
|
|
1041
|
+
|
|
934
1042
|
// 2. Check for type definition file
|
|
935
1043
|
if (isTypeDefinitionFile(node)) {
|
|
936
1044
|
return 'type-definition';
|
|
937
1045
|
}
|
|
938
|
-
|
|
1046
|
+
|
|
939
1047
|
// 3. Check for config/schema file (special case - acceptable multi-domain)
|
|
940
1048
|
if (isConfigOrSchemaFile(node)) {
|
|
941
1049
|
return 'cohesive-module'; // Treat as cohesive since it's intentional
|
|
942
1050
|
}
|
|
943
|
-
|
|
1051
|
+
|
|
944
1052
|
// 4. Check for lambda handlers FIRST (they often look like mixed concerns)
|
|
945
1053
|
if (isLambdaHandler(node)) {
|
|
946
1054
|
return 'lambda-handler';
|
|
947
1055
|
}
|
|
948
|
-
|
|
1056
|
+
|
|
949
1057
|
// 4b. Check for data access layer (DAL) files
|
|
950
1058
|
if (isDataAccessFile(node)) {
|
|
951
1059
|
return 'cohesive-module';
|
|
952
1060
|
}
|
|
953
|
-
|
|
1061
|
+
|
|
954
1062
|
// 5. Check for email templates (they reference multiple domains but serve one purpose)
|
|
955
1063
|
if (isEmailTemplate(node)) {
|
|
956
1064
|
return 'email-template';
|
|
957
1065
|
}
|
|
958
|
-
|
|
1066
|
+
|
|
959
1067
|
// 6. Check for parser/transformer files
|
|
960
1068
|
if (isParserFile(node)) {
|
|
961
1069
|
return 'parser-file';
|
|
962
1070
|
}
|
|
963
|
-
|
|
1071
|
+
|
|
964
1072
|
// 7. Check for service files
|
|
965
1073
|
if (isServiceFile(node)) {
|
|
966
1074
|
return 'service-file';
|
|
967
1075
|
}
|
|
968
|
-
|
|
1076
|
+
|
|
969
1077
|
// 8. Check for session/state management files
|
|
970
1078
|
if (isSessionFile(node)) {
|
|
971
1079
|
return 'cohesive-module'; // Session files manage state cohesively
|
|
972
1080
|
}
|
|
973
|
-
|
|
1081
|
+
|
|
974
1082
|
// 9. Check for Next.js App Router pages (metadata + faqJsonLd + default export)
|
|
975
1083
|
if (isNextJsPage(node)) {
|
|
976
1084
|
return 'nextjs-page';
|
|
977
1085
|
}
|
|
978
|
-
|
|
1086
|
+
|
|
979
1087
|
// 10. Check for utility file pattern (multiple domains but utility purpose)
|
|
980
1088
|
if (isUtilityFile(node)) {
|
|
981
1089
|
return 'utility-module';
|
|
@@ -985,14 +1093,17 @@ export function classifyFile(
|
|
|
985
1093
|
// should be classified as utility-module regardless of domain count.
|
|
986
1094
|
// This ensures common helper modules (e.g., src/utils/dynamodb-utils.ts)
|
|
987
1095
|
// are treated as utility modules in tests and analysis.
|
|
988
|
-
if (
|
|
1096
|
+
if (
|
|
1097
|
+
file.toLowerCase().includes('/utils/') ||
|
|
1098
|
+
file.toLowerCase().includes('/helpers/')
|
|
1099
|
+
) {
|
|
989
1100
|
return 'utility-module';
|
|
990
1101
|
}
|
|
991
|
-
|
|
1102
|
+
|
|
992
1103
|
// 10. Check for cohesive module (single domain + reasonable cohesion)
|
|
993
|
-
const uniqueDomains = domains.filter(d => d !== 'unknown');
|
|
1104
|
+
const uniqueDomains = domains.filter((d) => d !== 'unknown');
|
|
994
1105
|
const hasSingleDomain = uniqueDomains.length <= 1;
|
|
995
|
-
|
|
1106
|
+
|
|
996
1107
|
// Single domain files are almost always cohesive (even with lower cohesion score)
|
|
997
1108
|
if (hasSingleDomain) {
|
|
998
1109
|
return 'cohesive-module';
|
|
@@ -1003,27 +1114,27 @@ export function classifyFile(
|
|
|
1003
1114
|
if (allExportsShareEntityNoun(exports)) {
|
|
1004
1115
|
return 'cohesive-module';
|
|
1005
1116
|
}
|
|
1006
|
-
|
|
1117
|
+
|
|
1007
1118
|
// 11. Check for mixed concerns (multiple domains + low cohesion)
|
|
1008
1119
|
const hasMultipleDomains = uniqueDomains.length > 1;
|
|
1009
1120
|
const hasLowCohesion = cohesionScore < 0.4; // Lowered threshold
|
|
1010
|
-
|
|
1121
|
+
|
|
1011
1122
|
if (hasMultipleDomains && hasLowCohesion) {
|
|
1012
1123
|
return 'mixed-concerns';
|
|
1013
1124
|
}
|
|
1014
|
-
|
|
1125
|
+
|
|
1015
1126
|
// 12. Default to cohesive-module for files with reasonable cohesion
|
|
1016
1127
|
// This reduces false positives for legitimate files
|
|
1017
1128
|
if (cohesionScore >= 0.5) {
|
|
1018
1129
|
return 'cohesive-module';
|
|
1019
1130
|
}
|
|
1020
|
-
|
|
1131
|
+
|
|
1021
1132
|
return 'unknown';
|
|
1022
1133
|
}
|
|
1023
1134
|
|
|
1024
1135
|
/**
|
|
1025
1136
|
* Detect if a file is a barrel export (re-exports from other modules)
|
|
1026
|
-
*
|
|
1137
|
+
*
|
|
1027
1138
|
* Characteristics of barrel exports:
|
|
1028
1139
|
* - Named "index.ts" or "index.js"
|
|
1029
1140
|
* - Many re-export statements (export * from, export { x } from)
|
|
@@ -1032,42 +1143,46 @@ export function classifyFile(
|
|
|
1032
1143
|
*/
|
|
1033
1144
|
function isBarrelExport(node: DependencyNode): boolean {
|
|
1034
1145
|
const { file, exports, imports, linesOfCode } = node;
|
|
1035
|
-
|
|
1146
|
+
|
|
1036
1147
|
// Check filename pattern
|
|
1037
1148
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1038
|
-
const isIndexFile =
|
|
1039
|
-
|
|
1040
|
-
|
|
1149
|
+
const isIndexFile =
|
|
1150
|
+
fileName === 'index.ts' ||
|
|
1151
|
+
fileName === 'index.js' ||
|
|
1152
|
+
fileName === 'index.tsx' ||
|
|
1153
|
+
fileName === 'index.jsx';
|
|
1154
|
+
|
|
1041
1155
|
// Calculate re-export ratio
|
|
1042
1156
|
// Re-exports typically have form: export { x } from 'module' or export * from 'module'
|
|
1043
1157
|
// They have imports AND exports, with exports coming from those imports
|
|
1044
1158
|
const hasReExports = exports.length > 0 && imports.length > 0;
|
|
1045
|
-
const highExportToLinesRatio =
|
|
1046
|
-
|
|
1159
|
+
const highExportToLinesRatio =
|
|
1160
|
+
exports.length > 3 && linesOfCode < exports.length * 5;
|
|
1161
|
+
|
|
1047
1162
|
// Little actual code (mostly import/export statements)
|
|
1048
1163
|
const sparseCode = linesOfCode > 0 && linesOfCode < 50 && exports.length >= 2;
|
|
1049
|
-
|
|
1164
|
+
|
|
1050
1165
|
// Index files with re-export patterns
|
|
1051
1166
|
if (isIndexFile && hasReExports) {
|
|
1052
1167
|
return true;
|
|
1053
1168
|
}
|
|
1054
|
-
|
|
1169
|
+
|
|
1055
1170
|
// Non-index files that are clearly barrel exports
|
|
1056
1171
|
if (highExportToLinesRatio && imports.length >= exports.length * 0.5) {
|
|
1057
1172
|
return true;
|
|
1058
1173
|
}
|
|
1059
|
-
|
|
1174
|
+
|
|
1060
1175
|
// Very sparse files with multiple re-exports
|
|
1061
1176
|
if (sparseCode && imports.length > 0) {
|
|
1062
1177
|
return true;
|
|
1063
1178
|
}
|
|
1064
|
-
|
|
1179
|
+
|
|
1065
1180
|
return false;
|
|
1066
1181
|
}
|
|
1067
1182
|
|
|
1068
1183
|
/**
|
|
1069
1184
|
* Detect if a file is primarily a type definition file
|
|
1070
|
-
*
|
|
1185
|
+
*
|
|
1071
1186
|
* Characteristics:
|
|
1072
1187
|
* - Mostly type/interface exports
|
|
1073
1188
|
* - Little to no runtime code
|
|
@@ -1076,41 +1191,57 @@ function isBarrelExport(node: DependencyNode): boolean {
|
|
|
1076
1191
|
*/
|
|
1077
1192
|
function isTypeDefinitionFile(node: DependencyNode): boolean {
|
|
1078
1193
|
const { file, exports } = node;
|
|
1079
|
-
|
|
1194
|
+
|
|
1080
1195
|
// Check filename pattern
|
|
1081
1196
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1082
|
-
const isTypesFile =
|
|
1083
|
-
|
|
1084
|
-
|
|
1197
|
+
const isTypesFile =
|
|
1198
|
+
fileName?.includes('types') ||
|
|
1199
|
+
fileName?.includes('.d.ts') ||
|
|
1200
|
+
fileName === 'types.ts' ||
|
|
1201
|
+
fileName === 'interfaces.ts';
|
|
1202
|
+
|
|
1085
1203
|
// Check if file is in a types directory (path-based detection)
|
|
1086
1204
|
const lowerPath = file.toLowerCase();
|
|
1087
|
-
const isTypesPath =
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1205
|
+
const isTypesPath =
|
|
1206
|
+
lowerPath.includes('/types/') ||
|
|
1207
|
+
lowerPath.includes('/typings/') ||
|
|
1208
|
+
lowerPath.includes('/@types/') ||
|
|
1209
|
+
lowerPath.startsWith('types/') ||
|
|
1210
|
+
lowerPath.startsWith('typings/');
|
|
1211
|
+
|
|
1093
1212
|
// Count type exports vs other exports
|
|
1094
|
-
const typeExports = exports.filter(
|
|
1095
|
-
|
|
1096
|
-
|
|
1213
|
+
const typeExports = exports.filter(
|
|
1214
|
+
(e) => e.type === 'type' || e.type === 'interface'
|
|
1215
|
+
);
|
|
1216
|
+
const runtimeExports = exports.filter(
|
|
1217
|
+
(e) => e.type === 'function' || e.type === 'class' || e.type === 'const'
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1097
1220
|
// High ratio of type exports
|
|
1098
|
-
const mostlyTypes =
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1221
|
+
const mostlyTypes =
|
|
1222
|
+
exports.length > 0 &&
|
|
1223
|
+
typeExports.length > runtimeExports.length &&
|
|
1224
|
+
typeExports.length / exports.length > 0.7;
|
|
1225
|
+
|
|
1102
1226
|
// Pure type files (only type/interface exports, no runtime code)
|
|
1103
|
-
const pureTypeFile =
|
|
1104
|
-
|
|
1227
|
+
const pureTypeFile =
|
|
1228
|
+
exports.length > 0 && typeExports.length === exports.length;
|
|
1229
|
+
|
|
1105
1230
|
// Empty export file in types directory (might just be re-exports)
|
|
1106
1231
|
const emptyOrReExportInTypesDir = isTypesPath && exports.length === 0;
|
|
1107
|
-
|
|
1108
|
-
return
|
|
1232
|
+
|
|
1233
|
+
return (
|
|
1234
|
+
isTypesFile ||
|
|
1235
|
+
isTypesPath ||
|
|
1236
|
+
mostlyTypes ||
|
|
1237
|
+
pureTypeFile ||
|
|
1238
|
+
emptyOrReExportInTypesDir
|
|
1239
|
+
);
|
|
1109
1240
|
}
|
|
1110
1241
|
|
|
1111
1242
|
/**
|
|
1112
1243
|
* Detect if a file is a config/schema file
|
|
1113
|
-
*
|
|
1244
|
+
*
|
|
1114
1245
|
* Characteristics:
|
|
1115
1246
|
* - Named with config, schema, or settings patterns
|
|
1116
1247
|
* - Often defines database schemas, configuration objects
|
|
@@ -1118,38 +1249,51 @@ function isTypeDefinitionFile(node: DependencyNode): boolean {
|
|
|
1118
1249
|
*/
|
|
1119
1250
|
function isConfigOrSchemaFile(node: DependencyNode): boolean {
|
|
1120
1251
|
const { file, exports } = node;
|
|
1121
|
-
|
|
1252
|
+
|
|
1122
1253
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1123
|
-
|
|
1254
|
+
|
|
1124
1255
|
// Check filename patterns for config/schema files
|
|
1125
1256
|
const configPatterns = [
|
|
1126
|
-
'config',
|
|
1127
|
-
'
|
|
1257
|
+
'config',
|
|
1258
|
+
'schema',
|
|
1259
|
+
'settings',
|
|
1260
|
+
'options',
|
|
1261
|
+
'constants',
|
|
1262
|
+
'env',
|
|
1263
|
+
'environment',
|
|
1264
|
+
'.config.',
|
|
1265
|
+
'-config.',
|
|
1266
|
+
'_config.',
|
|
1128
1267
|
];
|
|
1129
|
-
|
|
1130
|
-
const isConfigName = configPatterns.some(
|
|
1131
|
-
|
|
1268
|
+
|
|
1269
|
+
const isConfigName = configPatterns.some(
|
|
1270
|
+
(pattern) =>
|
|
1271
|
+
fileName?.includes(pattern) ||
|
|
1272
|
+
fileName?.startsWith(pattern) ||
|
|
1273
|
+
fileName?.endsWith(`${pattern}.ts`)
|
|
1132
1274
|
);
|
|
1133
|
-
|
|
1275
|
+
|
|
1134
1276
|
// Check if file is in a config/settings directory
|
|
1135
|
-
const isConfigPath =
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1277
|
+
const isConfigPath =
|
|
1278
|
+
file.toLowerCase().includes('/config/') ||
|
|
1279
|
+
file.toLowerCase().includes('/schemas/') ||
|
|
1280
|
+
file.toLowerCase().includes('/settings/');
|
|
1281
|
+
|
|
1139
1282
|
// Check for schema-like exports (often have table/model definitions)
|
|
1140
|
-
const hasSchemaExports = exports.some(
|
|
1141
|
-
e
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1283
|
+
const hasSchemaExports = exports.some(
|
|
1284
|
+
(e) =>
|
|
1285
|
+
e.name.toLowerCase().includes('table') ||
|
|
1286
|
+
e.name.toLowerCase().includes('schema') ||
|
|
1287
|
+
e.name.toLowerCase().includes('config') ||
|
|
1288
|
+
e.name.toLowerCase().includes('setting')
|
|
1145
1289
|
);
|
|
1146
|
-
|
|
1290
|
+
|
|
1147
1291
|
return isConfigName || isConfigPath || hasSchemaExports;
|
|
1148
1292
|
}
|
|
1149
1293
|
|
|
1150
1294
|
/**
|
|
1151
1295
|
* Detect if a file is a utility/helper file
|
|
1152
|
-
*
|
|
1296
|
+
*
|
|
1153
1297
|
* Characteristics:
|
|
1154
1298
|
* - Named with util, helper, or utility patterns
|
|
1155
1299
|
* - Often contains mixed helper functions by design
|
|
@@ -1157,35 +1301,48 @@ function isConfigOrSchemaFile(node: DependencyNode): boolean {
|
|
|
1157
1301
|
*/
|
|
1158
1302
|
function isUtilityFile(node: DependencyNode): boolean {
|
|
1159
1303
|
const { file, exports } = node;
|
|
1160
|
-
|
|
1304
|
+
|
|
1161
1305
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1162
|
-
|
|
1306
|
+
|
|
1163
1307
|
// Check filename patterns for utility files
|
|
1164
1308
|
const utilityPatterns = [
|
|
1165
|
-
'util',
|
|
1166
|
-
'
|
|
1167
|
-
'
|
|
1309
|
+
'util',
|
|
1310
|
+
'utility',
|
|
1311
|
+
'utilities',
|
|
1312
|
+
'helper',
|
|
1313
|
+
'helpers',
|
|
1314
|
+
'common',
|
|
1315
|
+
'shared',
|
|
1316
|
+
'toolbox',
|
|
1317
|
+
'toolkit',
|
|
1318
|
+
'.util.',
|
|
1319
|
+
'-util.',
|
|
1320
|
+
'_util.',
|
|
1321
|
+
'-utils.',
|
|
1322
|
+
'.utils.',
|
|
1168
1323
|
];
|
|
1169
|
-
|
|
1170
|
-
const isUtilityName = utilityPatterns.some(pattern =>
|
|
1324
|
+
|
|
1325
|
+
const isUtilityName = utilityPatterns.some((pattern) =>
|
|
1171
1326
|
fileName?.includes(pattern)
|
|
1172
1327
|
);
|
|
1173
|
-
|
|
1328
|
+
|
|
1174
1329
|
// Check if file is in a utils/helpers directory
|
|
1175
|
-
const isUtilityPath =
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1330
|
+
const isUtilityPath =
|
|
1331
|
+
file.toLowerCase().includes('/utils/') ||
|
|
1332
|
+
file.toLowerCase().includes('/helpers/') ||
|
|
1333
|
+
file.toLowerCase().includes('/common/') ||
|
|
1334
|
+
file.toLowerCase().endsWith('-utils.ts') ||
|
|
1335
|
+
file.toLowerCase().endsWith('-util.ts') ||
|
|
1336
|
+
file.toLowerCase().endsWith('-helper.ts') ||
|
|
1337
|
+
file.toLowerCase().endsWith('-helpers.ts');
|
|
1338
|
+
|
|
1183
1339
|
// Only consider many small exports as utility pattern if also in utility-like path
|
|
1184
1340
|
// This prevents false positives for regular modules with many functions
|
|
1185
|
-
const hasManySmallExportsInUtilityContext =
|
|
1186
|
-
exports.
|
|
1341
|
+
const hasManySmallExportsInUtilityContext =
|
|
1342
|
+
exports.length >= 3 &&
|
|
1343
|
+
exports.every((e) => e.type === 'function' || e.type === 'const') &&
|
|
1187
1344
|
(isUtilityName || isUtilityPath);
|
|
1188
|
-
|
|
1345
|
+
|
|
1189
1346
|
return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
|
|
1190
1347
|
}
|
|
1191
1348
|
|
|
@@ -1204,13 +1361,59 @@ function splitCamelCase(name: string): string[] {
|
|
|
1204
1361
|
|
|
1205
1362
|
/** Common English verbs and adjectives to ignore when extracting entity nouns */
|
|
1206
1363
|
const SKIP_WORDS = new Set([
|
|
1207
|
-
'get',
|
|
1208
|
-
'
|
|
1209
|
-
'
|
|
1210
|
-
'
|
|
1211
|
-
'
|
|
1212
|
-
'
|
|
1213
|
-
'
|
|
1364
|
+
'get',
|
|
1365
|
+
'set',
|
|
1366
|
+
'create',
|
|
1367
|
+
'update',
|
|
1368
|
+
'delete',
|
|
1369
|
+
'fetch',
|
|
1370
|
+
'save',
|
|
1371
|
+
'load',
|
|
1372
|
+
'parse',
|
|
1373
|
+
'format',
|
|
1374
|
+
'validate',
|
|
1375
|
+
'convert',
|
|
1376
|
+
'transform',
|
|
1377
|
+
'build',
|
|
1378
|
+
'generate',
|
|
1379
|
+
'render',
|
|
1380
|
+
'send',
|
|
1381
|
+
'receive',
|
|
1382
|
+
'find',
|
|
1383
|
+
'list',
|
|
1384
|
+
'add',
|
|
1385
|
+
'remove',
|
|
1386
|
+
'insert',
|
|
1387
|
+
'upsert',
|
|
1388
|
+
'put',
|
|
1389
|
+
'read',
|
|
1390
|
+
'write',
|
|
1391
|
+
'check',
|
|
1392
|
+
'handle',
|
|
1393
|
+
'process',
|
|
1394
|
+
'compute',
|
|
1395
|
+
'calculate',
|
|
1396
|
+
'init',
|
|
1397
|
+
'reset',
|
|
1398
|
+
'clear',
|
|
1399
|
+
'pending',
|
|
1400
|
+
'active',
|
|
1401
|
+
'current',
|
|
1402
|
+
'new',
|
|
1403
|
+
'old',
|
|
1404
|
+
'all',
|
|
1405
|
+
'by',
|
|
1406
|
+
'with',
|
|
1407
|
+
'from',
|
|
1408
|
+
'to',
|
|
1409
|
+
'and',
|
|
1410
|
+
'or',
|
|
1411
|
+
'is',
|
|
1412
|
+
'has',
|
|
1413
|
+
'in',
|
|
1414
|
+
'on',
|
|
1415
|
+
'of',
|
|
1416
|
+
'the',
|
|
1214
1417
|
]);
|
|
1215
1418
|
|
|
1216
1419
|
/** Singularize a word simply (strip trailing 's') */
|
|
@@ -1227,7 +1430,7 @@ function simpleSingularize(word: string): string {
|
|
|
1227
1430
|
*/
|
|
1228
1431
|
function extractEntityNouns(name: string): string[] {
|
|
1229
1432
|
return splitCamelCase(name)
|
|
1230
|
-
.filter(token => !SKIP_WORDS.has(token) && token.length > 2)
|
|
1433
|
+
.filter((token) => !SKIP_WORDS.has(token) && token.length > 2)
|
|
1231
1434
|
.map(simpleSingularize);
|
|
1232
1435
|
}
|
|
1233
1436
|
|
|
@@ -1237,16 +1440,16 @@ function extractEntityNouns(name: string): string[] {
|
|
|
1237
1440
|
*/
|
|
1238
1441
|
function allExportsShareEntityNoun(exports: ExportInfo[]): boolean {
|
|
1239
1442
|
if (exports.length < 2 || exports.length > 30) return false;
|
|
1240
|
-
|
|
1241
|
-
const nounSets = exports.map(e => new Set(extractEntityNouns(e.name)));
|
|
1242
|
-
if (nounSets.some(s => s.size === 0)) return false;
|
|
1243
|
-
|
|
1443
|
+
|
|
1444
|
+
const nounSets = exports.map((e) => new Set(extractEntityNouns(e.name)));
|
|
1445
|
+
if (nounSets.some((s) => s.size === 0)) return false;
|
|
1446
|
+
|
|
1244
1447
|
// Find nouns that appear in ALL exports
|
|
1245
1448
|
const [first, ...rest] = nounSets;
|
|
1246
|
-
const commonNouns = Array.from(first).filter(noun =>
|
|
1247
|
-
rest.every(s => s.has(noun))
|
|
1449
|
+
const commonNouns = Array.from(first).filter((noun) =>
|
|
1450
|
+
rest.every((s) => s.has(noun))
|
|
1248
1451
|
);
|
|
1249
|
-
|
|
1452
|
+
|
|
1250
1453
|
return commonNouns.length > 0;
|
|
1251
1454
|
}
|
|
1252
1455
|
|
|
@@ -1261,26 +1464,40 @@ function allExportsShareEntityNoun(exports: ExportInfo[]): boolean {
|
|
|
1261
1464
|
function isDataAccessFile(node: DependencyNode): boolean {
|
|
1262
1465
|
const { file, exports } = node;
|
|
1263
1466
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1264
|
-
|
|
1467
|
+
|
|
1265
1468
|
const dalPatterns = [
|
|
1266
|
-
'dynamo',
|
|
1267
|
-
'
|
|
1268
|
-
'
|
|
1469
|
+
'dynamo',
|
|
1470
|
+
'database',
|
|
1471
|
+
'repository',
|
|
1472
|
+
'repo',
|
|
1473
|
+
'dao',
|
|
1474
|
+
'firestore',
|
|
1475
|
+
'postgres',
|
|
1476
|
+
'mysql',
|
|
1477
|
+
'mongo',
|
|
1478
|
+
'redis',
|
|
1479
|
+
'sqlite',
|
|
1480
|
+
'supabase',
|
|
1481
|
+
'prisma',
|
|
1269
1482
|
];
|
|
1270
|
-
|
|
1271
|
-
const isDalName = dalPatterns.some(p => fileName?.includes(p));
|
|
1272
|
-
|
|
1273
|
-
const isDalPath =
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1483
|
+
|
|
1484
|
+
const isDalName = dalPatterns.some((p) => fileName?.includes(p));
|
|
1485
|
+
|
|
1486
|
+
const isDalPath =
|
|
1487
|
+
file.toLowerCase().includes('/repositories/') ||
|
|
1488
|
+
file.toLowerCase().includes('/dao/') ||
|
|
1489
|
+
file.toLowerCase().includes('/data/');
|
|
1490
|
+
|
|
1277
1491
|
// File with few exports (≤10) that all share a common entity noun
|
|
1278
|
-
const hasDalExportPattern =
|
|
1492
|
+
const hasDalExportPattern =
|
|
1493
|
+
exports.length >= 1 &&
|
|
1279
1494
|
exports.length <= 10 &&
|
|
1280
1495
|
allExportsShareEntityNoun(exports);
|
|
1281
|
-
|
|
1496
|
+
|
|
1282
1497
|
// Exclude obvious utility paths from DAL detection (e.g., src/utils/)
|
|
1283
|
-
const isUtilityPathLocal =
|
|
1498
|
+
const isUtilityPathLocal =
|
|
1499
|
+
file.toLowerCase().includes('/utils/') ||
|
|
1500
|
+
file.toLowerCase().includes('/helpers/');
|
|
1284
1501
|
|
|
1285
1502
|
// Only treat as DAL when the file is in a DAL path, or when the name/pattern
|
|
1286
1503
|
// indicates a data access module AND exports follow a DAL-like pattern.
|
|
@@ -1290,7 +1507,7 @@ function isDataAccessFile(node: DependencyNode): boolean {
|
|
|
1290
1507
|
|
|
1291
1508
|
/**
|
|
1292
1509
|
* Detect if a file is a Lambda/API handler
|
|
1293
|
-
*
|
|
1510
|
+
*
|
|
1294
1511
|
* Characteristics:
|
|
1295
1512
|
* - Named with handler patterns or in handler directories
|
|
1296
1513
|
* - Single entry point (handler function)
|
|
@@ -1298,46 +1515,58 @@ function isDataAccessFile(node: DependencyNode): boolean {
|
|
|
1298
1515
|
*/
|
|
1299
1516
|
function isLambdaHandler(node: DependencyNode): boolean {
|
|
1300
1517
|
const { file, exports } = node;
|
|
1301
|
-
|
|
1518
|
+
|
|
1302
1519
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1303
|
-
|
|
1520
|
+
|
|
1304
1521
|
// Check filename patterns for lambda handlers
|
|
1305
1522
|
const handlerPatterns = [
|
|
1306
|
-
'handler',
|
|
1307
|
-
'
|
|
1523
|
+
'handler',
|
|
1524
|
+
'.handler.',
|
|
1525
|
+
'-handler.',
|
|
1526
|
+
'lambda',
|
|
1527
|
+
'.lambda.',
|
|
1528
|
+
'-lambda.',
|
|
1308
1529
|
];
|
|
1309
|
-
|
|
1310
|
-
const isHandlerName = handlerPatterns.some(pattern =>
|
|
1530
|
+
|
|
1531
|
+
const isHandlerName = handlerPatterns.some((pattern) =>
|
|
1311
1532
|
fileName?.includes(pattern)
|
|
1312
1533
|
);
|
|
1313
|
-
|
|
1534
|
+
|
|
1314
1535
|
// Check if file is in a handlers/lambdas/functions/lambda directory
|
|
1315
1536
|
// Exclude /api/ unless it has handler-specific naming
|
|
1316
|
-
const isHandlerPath =
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1537
|
+
const isHandlerPath =
|
|
1538
|
+
file.toLowerCase().includes('/handlers/') ||
|
|
1539
|
+
file.toLowerCase().includes('/lambdas/') ||
|
|
1540
|
+
file.toLowerCase().includes('/lambda/') ||
|
|
1541
|
+
file.toLowerCase().includes('/functions/');
|
|
1542
|
+
|
|
1321
1543
|
// Check for typical lambda handler exports (handler, main, etc.)
|
|
1322
|
-
const hasHandlerExport = exports.some(
|
|
1323
|
-
e
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1544
|
+
const hasHandlerExport = exports.some(
|
|
1545
|
+
(e) =>
|
|
1546
|
+
e.name.toLowerCase() === 'handler' ||
|
|
1547
|
+
e.name.toLowerCase() === 'main' ||
|
|
1548
|
+
e.name.toLowerCase() === 'lambdahandler' ||
|
|
1549
|
+
e.name.toLowerCase().endsWith('handler')
|
|
1327
1550
|
);
|
|
1328
|
-
|
|
1551
|
+
|
|
1329
1552
|
// Only consider single export as lambda handler if it's in a handler-like context
|
|
1330
1553
|
// (either in handler directory OR has handler naming)
|
|
1331
|
-
const hasSingleEntryInHandlerContext =
|
|
1554
|
+
const hasSingleEntryInHandlerContext =
|
|
1555
|
+
exports.length === 1 &&
|
|
1332
1556
|
(exports[0].type === 'function' || exports[0].name === 'default') &&
|
|
1333
1557
|
(isHandlerPath || isHandlerName);
|
|
1334
|
-
|
|
1335
|
-
return
|
|
1558
|
+
|
|
1559
|
+
return (
|
|
1560
|
+
isHandlerName ||
|
|
1561
|
+
isHandlerPath ||
|
|
1562
|
+
hasHandlerExport ||
|
|
1563
|
+
hasSingleEntryInHandlerContext
|
|
1564
|
+
);
|
|
1336
1565
|
}
|
|
1337
1566
|
|
|
1338
1567
|
/**
|
|
1339
1568
|
* Detect if a file is a service file
|
|
1340
|
-
*
|
|
1569
|
+
*
|
|
1341
1570
|
* Characteristics:
|
|
1342
1571
|
* - Named with service pattern
|
|
1343
1572
|
* - Often a class or object with multiple methods
|
|
@@ -1345,39 +1574,40 @@ function isLambdaHandler(node: DependencyNode): boolean {
|
|
|
1345
1574
|
*/
|
|
1346
1575
|
function isServiceFile(node: DependencyNode): boolean {
|
|
1347
1576
|
const { file, exports } = node;
|
|
1348
|
-
|
|
1577
|
+
|
|
1349
1578
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1350
|
-
|
|
1579
|
+
|
|
1351
1580
|
// Check filename patterns for service files
|
|
1352
|
-
const servicePatterns = [
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
const isServiceName = servicePatterns.some(pattern =>
|
|
1581
|
+
const servicePatterns = ['service', '.service.', '-service.', '_service.'];
|
|
1582
|
+
|
|
1583
|
+
const isServiceName = servicePatterns.some((pattern) =>
|
|
1357
1584
|
fileName?.includes(pattern)
|
|
1358
1585
|
);
|
|
1359
|
-
|
|
1586
|
+
|
|
1360
1587
|
// Check if file is in a services directory
|
|
1361
1588
|
const isServicePath = file.toLowerCase().includes('/services/');
|
|
1362
|
-
|
|
1589
|
+
|
|
1363
1590
|
// Check for service-like exports (class with "Service" in the name)
|
|
1364
|
-
const hasServiceNamedExport = exports.some(
|
|
1365
|
-
e
|
|
1366
|
-
|
|
1591
|
+
const hasServiceNamedExport = exports.some(
|
|
1592
|
+
(e) =>
|
|
1593
|
+
e.name.toLowerCase().includes('service') ||
|
|
1594
|
+
e.name.toLowerCase().endsWith('service')
|
|
1367
1595
|
);
|
|
1368
|
-
|
|
1596
|
+
|
|
1369
1597
|
// Check for typical service pattern (class export with service in name)
|
|
1370
|
-
const hasClassExport = exports.some(e => e.type === 'class');
|
|
1371
|
-
|
|
1598
|
+
const hasClassExport = exports.some((e) => e.type === 'class');
|
|
1599
|
+
|
|
1372
1600
|
// Service files need either:
|
|
1373
1601
|
// 1. Service in filename/path, OR
|
|
1374
1602
|
// 2. Class with "Service" in the class name
|
|
1375
|
-
return
|
|
1603
|
+
return (
|
|
1604
|
+
isServiceName || isServicePath || (hasServiceNamedExport && hasClassExport)
|
|
1605
|
+
);
|
|
1376
1606
|
}
|
|
1377
1607
|
|
|
1378
1608
|
/**
|
|
1379
1609
|
* Detect if a file is an email template/layout
|
|
1380
|
-
*
|
|
1610
|
+
*
|
|
1381
1611
|
* Characteristics:
|
|
1382
1612
|
* - Named with email/template patterns
|
|
1383
1613
|
* - Contains render/template logic
|
|
@@ -1385,58 +1615,70 @@ function isServiceFile(node: DependencyNode): boolean {
|
|
|
1385
1615
|
*/
|
|
1386
1616
|
function isEmailTemplate(node: DependencyNode): boolean {
|
|
1387
1617
|
const { file, exports } = node;
|
|
1388
|
-
|
|
1618
|
+
|
|
1389
1619
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1390
|
-
|
|
1620
|
+
|
|
1391
1621
|
// Check filename patterns for email templates (more specific patterns)
|
|
1392
1622
|
const emailTemplatePatterns = [
|
|
1393
|
-
'-email-',
|
|
1394
|
-
'
|
|
1395
|
-
'
|
|
1623
|
+
'-email-',
|
|
1624
|
+
'.email.',
|
|
1625
|
+
'_email_',
|
|
1626
|
+
'-template',
|
|
1627
|
+
'.template.',
|
|
1628
|
+
'_template',
|
|
1629
|
+
'-mail.',
|
|
1630
|
+
'.mail.',
|
|
1396
1631
|
];
|
|
1397
|
-
|
|
1398
|
-
const isEmailTemplateName = emailTemplatePatterns.some(pattern =>
|
|
1632
|
+
|
|
1633
|
+
const isEmailTemplateName = emailTemplatePatterns.some((pattern) =>
|
|
1399
1634
|
fileName?.includes(pattern)
|
|
1400
1635
|
);
|
|
1401
|
-
|
|
1636
|
+
|
|
1402
1637
|
// Specific template file names
|
|
1403
|
-
const isSpecificTemplateName =
|
|
1638
|
+
const isSpecificTemplateName =
|
|
1404
1639
|
fileName?.includes('receipt') ||
|
|
1405
1640
|
fileName?.includes('invoice-email') ||
|
|
1406
1641
|
fileName?.includes('welcome-email') ||
|
|
1407
1642
|
fileName?.includes('notification-email') ||
|
|
1408
|
-
fileName?.includes('writer') && fileName.includes('receipt');
|
|
1409
|
-
|
|
1643
|
+
(fileName?.includes('writer') && fileName.includes('receipt'));
|
|
1644
|
+
|
|
1410
1645
|
// Check if file is in emails/templates directory (high confidence)
|
|
1411
|
-
const isEmailPath =
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1646
|
+
const isEmailPath =
|
|
1647
|
+
file.toLowerCase().includes('/emails/') ||
|
|
1648
|
+
file.toLowerCase().includes('/mail/') ||
|
|
1649
|
+
file.toLowerCase().includes('/notifications/');
|
|
1650
|
+
|
|
1415
1651
|
// Check for template patterns (function that returns string/HTML)
|
|
1416
1652
|
// More specific: must have render/generate in the function name
|
|
1417
|
-
const hasTemplateFunction = exports.some(
|
|
1418
|
-
e
|
|
1419
|
-
e.
|
|
1420
|
-
e.name.toLowerCase().startsWith('
|
|
1421
|
-
|
|
1422
|
-
|
|
1653
|
+
const hasTemplateFunction = exports.some(
|
|
1654
|
+
(e) =>
|
|
1655
|
+
e.type === 'function' &&
|
|
1656
|
+
(e.name.toLowerCase().startsWith('render') ||
|
|
1657
|
+
e.name.toLowerCase().startsWith('generate') ||
|
|
1658
|
+
(e.name.toLowerCase().includes('template') &&
|
|
1659
|
+
e.name.toLowerCase().includes('email')))
|
|
1423
1660
|
);
|
|
1424
|
-
|
|
1661
|
+
|
|
1425
1662
|
// Check for email-related exports (but not service classes)
|
|
1426
|
-
const hasEmailExport = exports.some(
|
|
1427
|
-
(e
|
|
1428
|
-
|
|
1429
|
-
|
|
1663
|
+
const hasEmailExport = exports.some(
|
|
1664
|
+
(e) =>
|
|
1665
|
+
(e.name.toLowerCase().includes('template') && e.type === 'function') ||
|
|
1666
|
+
(e.name.toLowerCase().includes('render') && e.type === 'function') ||
|
|
1667
|
+
(e.name.toLowerCase().includes('email') && e.type !== 'class')
|
|
1430
1668
|
);
|
|
1431
|
-
|
|
1669
|
+
|
|
1432
1670
|
// Require path-based match OR combination of name and export patterns
|
|
1433
|
-
return
|
|
1434
|
-
|
|
1671
|
+
return (
|
|
1672
|
+
isEmailPath ||
|
|
1673
|
+
isEmailTemplateName ||
|
|
1674
|
+
isSpecificTemplateName ||
|
|
1675
|
+
(hasTemplateFunction && hasEmailExport)
|
|
1676
|
+
);
|
|
1435
1677
|
}
|
|
1436
1678
|
|
|
1437
1679
|
/**
|
|
1438
1680
|
* Detect if a file is a parser/transformer
|
|
1439
|
-
*
|
|
1681
|
+
*
|
|
1440
1682
|
* Characteristics:
|
|
1441
1683
|
* - Named with parser/transform patterns
|
|
1442
1684
|
* - Contains parse/transform logic
|
|
@@ -1444,56 +1686,68 @@ function isEmailTemplate(node: DependencyNode): boolean {
|
|
|
1444
1686
|
*/
|
|
1445
1687
|
function isParserFile(node: DependencyNode): boolean {
|
|
1446
1688
|
const { file, exports } = node;
|
|
1447
|
-
|
|
1689
|
+
|
|
1448
1690
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1449
|
-
|
|
1691
|
+
|
|
1450
1692
|
// Check filename patterns for parser files
|
|
1451
1693
|
const parserPatterns = [
|
|
1452
|
-
'parser',
|
|
1453
|
-
'
|
|
1454
|
-
'
|
|
1455
|
-
'
|
|
1456
|
-
'
|
|
1694
|
+
'parser',
|
|
1695
|
+
'.parser.',
|
|
1696
|
+
'-parser.',
|
|
1697
|
+
'_parser.',
|
|
1698
|
+
'transform',
|
|
1699
|
+
'.transform.',
|
|
1700
|
+
'-transform.',
|
|
1701
|
+
'converter',
|
|
1702
|
+
'.converter.',
|
|
1703
|
+
'-converter.',
|
|
1704
|
+
'mapper',
|
|
1705
|
+
'.mapper.',
|
|
1706
|
+
'-mapper.',
|
|
1707
|
+
'serializer',
|
|
1708
|
+
'.serializer.',
|
|
1457
1709
|
'deterministic', // For base-parser-deterministic.ts pattern
|
|
1458
1710
|
];
|
|
1459
|
-
|
|
1460
|
-
const isParserName = parserPatterns.some(pattern =>
|
|
1711
|
+
|
|
1712
|
+
const isParserName = parserPatterns.some((pattern) =>
|
|
1461
1713
|
fileName?.includes(pattern)
|
|
1462
1714
|
);
|
|
1463
|
-
|
|
1715
|
+
|
|
1464
1716
|
// Check if file is in parsers/transformers directory
|
|
1465
|
-
const isParserPath =
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1717
|
+
const isParserPath =
|
|
1718
|
+
file.toLowerCase().includes('/parsers/') ||
|
|
1719
|
+
file.toLowerCase().includes('/transformers/') ||
|
|
1720
|
+
file.toLowerCase().includes('/converters/') ||
|
|
1721
|
+
file.toLowerCase().includes('/mappers/');
|
|
1722
|
+
|
|
1470
1723
|
// Check for parser-related exports
|
|
1471
|
-
const hasParserExport = exports.some(
|
|
1472
|
-
e
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1724
|
+
const hasParserExport = exports.some(
|
|
1725
|
+
(e) =>
|
|
1726
|
+
e.name.toLowerCase().includes('parse') ||
|
|
1727
|
+
e.name.toLowerCase().includes('transform') ||
|
|
1728
|
+
e.name.toLowerCase().includes('convert') ||
|
|
1729
|
+
e.name.toLowerCase().includes('map') ||
|
|
1730
|
+
e.name.toLowerCase().includes('serialize') ||
|
|
1731
|
+
e.name.toLowerCase().includes('deserialize')
|
|
1478
1732
|
);
|
|
1479
|
-
|
|
1733
|
+
|
|
1480
1734
|
// Check for function patterns typical of parsers
|
|
1481
|
-
const hasParseFunction = exports.some(
|
|
1482
|
-
e
|
|
1483
|
-
e.
|
|
1484
|
-
e.name.toLowerCase().startsWith('
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1735
|
+
const hasParseFunction = exports.some(
|
|
1736
|
+
(e) =>
|
|
1737
|
+
e.type === 'function' &&
|
|
1738
|
+
(e.name.toLowerCase().startsWith('parse') ||
|
|
1739
|
+
e.name.toLowerCase().startsWith('transform') ||
|
|
1740
|
+
e.name.toLowerCase().startsWith('convert') ||
|
|
1741
|
+
e.name.toLowerCase().startsWith('map') ||
|
|
1742
|
+
e.name.toLowerCase().startsWith('extract'))
|
|
1489
1743
|
);
|
|
1490
|
-
|
|
1744
|
+
|
|
1491
1745
|
return isParserName || isParserPath || hasParserExport || hasParseFunction;
|
|
1492
1746
|
}
|
|
1493
1747
|
|
|
1494
1748
|
/**
|
|
1495
1749
|
* Detect if a file is a session/state management file
|
|
1496
|
-
*
|
|
1750
|
+
*
|
|
1497
1751
|
* Characteristics:
|
|
1498
1752
|
* - Named with session/state patterns
|
|
1499
1753
|
* - Manages state across operations
|
|
@@ -1501,42 +1755,52 @@ function isParserFile(node: DependencyNode): boolean {
|
|
|
1501
1755
|
*/
|
|
1502
1756
|
function isSessionFile(node: DependencyNode): boolean {
|
|
1503
1757
|
const { file, exports } = node;
|
|
1504
|
-
|
|
1758
|
+
|
|
1505
1759
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1506
|
-
|
|
1760
|
+
|
|
1507
1761
|
// Check filename patterns for session files
|
|
1508
1762
|
const sessionPatterns = [
|
|
1509
|
-
'session',
|
|
1510
|
-
'
|
|
1511
|
-
'
|
|
1512
|
-
'
|
|
1763
|
+
'session',
|
|
1764
|
+
'.session.',
|
|
1765
|
+
'-session.',
|
|
1766
|
+
'state',
|
|
1767
|
+
'.state.',
|
|
1768
|
+
'-state.',
|
|
1769
|
+
'context',
|
|
1770
|
+
'.context.',
|
|
1771
|
+
'-context.',
|
|
1772
|
+
'store',
|
|
1773
|
+
'.store.',
|
|
1774
|
+
'-store.',
|
|
1513
1775
|
];
|
|
1514
|
-
|
|
1515
|
-
const isSessionName = sessionPatterns.some(pattern =>
|
|
1776
|
+
|
|
1777
|
+
const isSessionName = sessionPatterns.some((pattern) =>
|
|
1516
1778
|
fileName?.includes(pattern)
|
|
1517
1779
|
);
|
|
1518
|
-
|
|
1780
|
+
|
|
1519
1781
|
// Check if file is in sessions/state directory
|
|
1520
|
-
const isSessionPath =
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1782
|
+
const isSessionPath =
|
|
1783
|
+
file.toLowerCase().includes('/sessions/') ||
|
|
1784
|
+
file.toLowerCase().includes('/state/') ||
|
|
1785
|
+
file.toLowerCase().includes('/context/') ||
|
|
1786
|
+
file.toLowerCase().includes('/store/');
|
|
1787
|
+
|
|
1525
1788
|
// Check for session-related exports
|
|
1526
|
-
const hasSessionExport = exports.some(
|
|
1527
|
-
e
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1789
|
+
const hasSessionExport = exports.some(
|
|
1790
|
+
(e) =>
|
|
1791
|
+
e.name.toLowerCase().includes('session') ||
|
|
1792
|
+
e.name.toLowerCase().includes('state') ||
|
|
1793
|
+
e.name.toLowerCase().includes('context') ||
|
|
1794
|
+
e.name.toLowerCase().includes('manager') ||
|
|
1795
|
+
e.name.toLowerCase().includes('store')
|
|
1532
1796
|
);
|
|
1533
|
-
|
|
1797
|
+
|
|
1534
1798
|
return isSessionName || isSessionPath || hasSessionExport;
|
|
1535
1799
|
}
|
|
1536
1800
|
|
|
1537
1801
|
/**
|
|
1538
1802
|
* Detect if a file is a Next.js App Router page
|
|
1539
|
-
*
|
|
1803
|
+
*
|
|
1540
1804
|
* Characteristics:
|
|
1541
1805
|
* - Located in /app/ directory (Next.js App Router)
|
|
1542
1806
|
* - Named page.tsx or page.ts
|
|
@@ -1544,38 +1808,47 @@ function isSessionFile(node: DependencyNode): boolean {
|
|
|
1544
1808
|
* - faqJsonLd, jsonLd (structured data)
|
|
1545
1809
|
* - icon (for tool cards)
|
|
1546
1810
|
* - generateMetadata (dynamic SEO)
|
|
1547
|
-
*
|
|
1811
|
+
*
|
|
1548
1812
|
* This is the canonical Next.js pattern for SEO-optimized pages.
|
|
1549
1813
|
* Multiple exports are COHESIVE - they all serve the page's purpose.
|
|
1550
1814
|
*/
|
|
1551
1815
|
function isNextJsPage(node: DependencyNode): boolean {
|
|
1552
1816
|
const { file, exports } = node;
|
|
1553
|
-
|
|
1817
|
+
|
|
1554
1818
|
const lowerPath = file.toLowerCase();
|
|
1555
1819
|
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1556
|
-
|
|
1820
|
+
|
|
1557
1821
|
// Must be in /app/ directory (Next.js App Router)
|
|
1558
|
-
const isInAppDir =
|
|
1559
|
-
|
|
1822
|
+
const isInAppDir =
|
|
1823
|
+
lowerPath.includes('/app/') || lowerPath.startsWith('app/');
|
|
1824
|
+
|
|
1560
1825
|
// Must be named page.tsx or page.ts
|
|
1561
1826
|
const isPageFile = fileName === 'page.tsx' || fileName === 'page.ts';
|
|
1562
|
-
|
|
1827
|
+
|
|
1563
1828
|
if (!isInAppDir || !isPageFile) {
|
|
1564
1829
|
return false;
|
|
1565
1830
|
}
|
|
1566
|
-
|
|
1831
|
+
|
|
1567
1832
|
// Check for Next.js page export patterns
|
|
1568
|
-
const exportNames = exports.map(e => e.name.toLowerCase());
|
|
1569
|
-
|
|
1833
|
+
const exportNames = exports.map((e) => e.name.toLowerCase());
|
|
1834
|
+
|
|
1570
1835
|
// Must have default export (the page component)
|
|
1571
|
-
const hasDefaultExport = exports.some(e => e.type === 'default');
|
|
1572
|
-
|
|
1836
|
+
const hasDefaultExport = exports.some((e) => e.type === 'default');
|
|
1837
|
+
|
|
1573
1838
|
// Common Next.js page exports
|
|
1574
|
-
const nextJsExports = [
|
|
1575
|
-
|
|
1576
|
-
|
|
1839
|
+
const nextJsExports = [
|
|
1840
|
+
'metadata',
|
|
1841
|
+
'generatemetadata',
|
|
1842
|
+
'faqjsonld',
|
|
1843
|
+
'jsonld',
|
|
1844
|
+
'icon',
|
|
1845
|
+
'viewport',
|
|
1846
|
+
'dynamic',
|
|
1847
|
+
];
|
|
1848
|
+
const hasNextJsExports = exportNames.some(
|
|
1849
|
+
(name) => nextJsExports.includes(name) || name.includes('jsonld')
|
|
1577
1850
|
);
|
|
1578
|
-
|
|
1851
|
+
|
|
1579
1852
|
// A Next.js page typically has:
|
|
1580
1853
|
// 1. Default export (page component) - required
|
|
1581
1854
|
// 2. Metadata or other Next.js-specific exports - optional but indicative
|
|
@@ -1584,7 +1857,7 @@ function isNextJsPage(node: DependencyNode): boolean {
|
|
|
1584
1857
|
|
|
1585
1858
|
/**
|
|
1586
1859
|
* Adjust cohesion score based on file classification.
|
|
1587
|
-
*
|
|
1860
|
+
*
|
|
1588
1861
|
* This reduces false positives by recognizing that certain file types
|
|
1589
1862
|
* have inherently different cohesion patterns:
|
|
1590
1863
|
* - Utility modules may touch multiple domains but serve one purpose
|
|
@@ -1592,7 +1865,7 @@ function isNextJsPage(node: DependencyNode): boolean {
|
|
|
1592
1865
|
* - Lambda handlers coordinate multiple services
|
|
1593
1866
|
* - Email templates reference multiple domains for rendering
|
|
1594
1867
|
* - Parser files transform data across domains
|
|
1595
|
-
*
|
|
1868
|
+
*
|
|
1596
1869
|
* @param baseCohesion - The calculated cohesion score (0-1)
|
|
1597
1870
|
* @param classification - The file classification
|
|
1598
1871
|
* @param node - Optional node for additional heuristics
|
|
@@ -1614,10 +1887,10 @@ export function adjustCohesionForClassification(
|
|
|
1614
1887
|
// Utility modules serve a functional purpose despite multi-domain.
|
|
1615
1888
|
// Use a floor of 0.75 so related utilities never appear as low-cohesion.
|
|
1616
1889
|
if (node) {
|
|
1617
|
-
const exportNames = node.exports.map(e => e.name.toLowerCase());
|
|
1890
|
+
const exportNames = node.exports.map((e) => e.name.toLowerCase());
|
|
1618
1891
|
const hasRelatedNames = hasRelatedExportNames(exportNames);
|
|
1619
1892
|
if (hasRelatedNames) {
|
|
1620
|
-
return Math.max(0.
|
|
1893
|
+
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1621
1894
|
}
|
|
1622
1895
|
}
|
|
1623
1896
|
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
@@ -1625,18 +1898,19 @@ export function adjustCohesionForClassification(
|
|
|
1625
1898
|
case 'service-file': {
|
|
1626
1899
|
// Services orchestrate dependencies by design.
|
|
1627
1900
|
// Floor at 0.72 so service files are never flagged as low-cohesion.
|
|
1628
|
-
if (node?.exports.some(e => e.type === 'class')) {
|
|
1629
|
-
return Math.max(0.78, Math.min(1, baseCohesion + 0.
|
|
1901
|
+
if (node?.exports.some((e) => e.type === 'class')) {
|
|
1902
|
+
return Math.max(0.78, Math.min(1, baseCohesion + 0.4));
|
|
1630
1903
|
}
|
|
1631
|
-
return Math.max(0.72, Math.min(1, baseCohesion + 0.
|
|
1904
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1632
1905
|
}
|
|
1633
1906
|
case 'lambda-handler': {
|
|
1634
1907
|
// Lambda handlers have single business purpose; floor at 0.75.
|
|
1635
1908
|
if (node) {
|
|
1636
|
-
const hasSingleEntry =
|
|
1637
|
-
node.exports.
|
|
1909
|
+
const hasSingleEntry =
|
|
1910
|
+
node.exports.length === 1 ||
|
|
1911
|
+
node.exports.some((e) => e.name.toLowerCase() === 'handler');
|
|
1638
1912
|
if (hasSingleEntry) {
|
|
1639
|
-
return Math.max(0.
|
|
1913
|
+
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1640
1914
|
}
|
|
1641
1915
|
}
|
|
1642
1916
|
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
@@ -1644,31 +1918,33 @@ export function adjustCohesionForClassification(
|
|
|
1644
1918
|
case 'email-template': {
|
|
1645
1919
|
// Email templates are structurally cohesive (single rendering purpose); floor at 0.72.
|
|
1646
1920
|
if (node) {
|
|
1647
|
-
const hasTemplateFunc = node.exports.some(
|
|
1648
|
-
e
|
|
1649
|
-
|
|
1650
|
-
|
|
1921
|
+
const hasTemplateFunc = node.exports.some(
|
|
1922
|
+
(e) =>
|
|
1923
|
+
e.name.toLowerCase().includes('render') ||
|
|
1924
|
+
e.name.toLowerCase().includes('generate') ||
|
|
1925
|
+
e.name.toLowerCase().includes('template')
|
|
1651
1926
|
);
|
|
1652
1927
|
if (hasTemplateFunc) {
|
|
1653
|
-
return Math.max(0.75, Math.min(1, baseCohesion + 0.
|
|
1928
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1654
1929
|
}
|
|
1655
1930
|
}
|
|
1656
|
-
return Math.max(0.72, Math.min(1, baseCohesion + 0.
|
|
1931
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1657
1932
|
}
|
|
1658
1933
|
case 'parser-file': {
|
|
1659
1934
|
// Parsers transform data - single transformation purpose
|
|
1660
1935
|
if (node) {
|
|
1661
1936
|
// Check for parse/transform functions
|
|
1662
|
-
const hasParseFunc = node.exports.some(
|
|
1663
|
-
e
|
|
1664
|
-
|
|
1665
|
-
|
|
1937
|
+
const hasParseFunc = node.exports.some(
|
|
1938
|
+
(e) =>
|
|
1939
|
+
e.name.toLowerCase().startsWith('parse') ||
|
|
1940
|
+
e.name.toLowerCase().startsWith('transform') ||
|
|
1941
|
+
e.name.toLowerCase().startsWith('convert')
|
|
1666
1942
|
);
|
|
1667
1943
|
if (hasParseFunc) {
|
|
1668
|
-
return Math.max(0.75, Math.min(1, baseCohesion + 0.
|
|
1944
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1669
1945
|
}
|
|
1670
1946
|
}
|
|
1671
|
-
return Math.max(0.
|
|
1947
|
+
return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
|
|
1672
1948
|
}
|
|
1673
1949
|
case 'nextjs-page':
|
|
1674
1950
|
// Next.js pages have multiple exports by design (metadata, jsonLd, page component)
|
|
@@ -1682,13 +1958,13 @@ export function adjustCohesionForClassification(
|
|
|
1682
1958
|
return baseCohesion;
|
|
1683
1959
|
default:
|
|
1684
1960
|
// Unknown - give benefit of doubt with small boost
|
|
1685
|
-
return Math.min(1, baseCohesion + 0.
|
|
1961
|
+
return Math.min(1, baseCohesion + 0.1);
|
|
1686
1962
|
}
|
|
1687
1963
|
}
|
|
1688
1964
|
|
|
1689
1965
|
/**
|
|
1690
1966
|
* Check if export names suggest related functionality
|
|
1691
|
-
*
|
|
1967
|
+
*
|
|
1692
1968
|
* Examples of related patterns:
|
|
1693
1969
|
* - formatDate, parseDate, validateDate (date utilities)
|
|
1694
1970
|
* - getUser, saveUser, deleteUser (user utilities)
|
|
@@ -1696,40 +1972,73 @@ export function adjustCohesionForClassification(
|
|
|
1696
1972
|
*/
|
|
1697
1973
|
function hasRelatedExportNames(exportNames: string[]): boolean {
|
|
1698
1974
|
if (exportNames.length < 2) return true;
|
|
1699
|
-
|
|
1975
|
+
|
|
1700
1976
|
// Extract common prefixes/suffixes
|
|
1701
1977
|
const stems = new Set<string>();
|
|
1702
1978
|
const domains = new Set<string>();
|
|
1703
|
-
|
|
1979
|
+
|
|
1704
1980
|
for (const name of exportNames) {
|
|
1705
1981
|
// Check for common verb prefixes
|
|
1706
|
-
const verbs = [
|
|
1982
|
+
const verbs = [
|
|
1983
|
+
'get',
|
|
1984
|
+
'set',
|
|
1985
|
+
'create',
|
|
1986
|
+
'update',
|
|
1987
|
+
'delete',
|
|
1988
|
+
'fetch',
|
|
1989
|
+
'save',
|
|
1990
|
+
'load',
|
|
1991
|
+
'parse',
|
|
1992
|
+
'format',
|
|
1993
|
+
'validate',
|
|
1994
|
+
'convert',
|
|
1995
|
+
'transform',
|
|
1996
|
+
'build',
|
|
1997
|
+
'generate',
|
|
1998
|
+
'render',
|
|
1999
|
+
'send',
|
|
2000
|
+
'receive',
|
|
2001
|
+
];
|
|
1707
2002
|
for (const verb of verbs) {
|
|
1708
2003
|
if (name.startsWith(verb) && name.length > verb.length) {
|
|
1709
2004
|
stems.add(name.slice(verb.length).toLowerCase());
|
|
1710
2005
|
}
|
|
1711
2006
|
}
|
|
1712
|
-
|
|
2007
|
+
|
|
1713
2008
|
// Check for domain suffixes (User, Order, etc.)
|
|
1714
|
-
const domainPatterns = [
|
|
2009
|
+
const domainPatterns = [
|
|
2010
|
+
'user',
|
|
2011
|
+
'order',
|
|
2012
|
+
'product',
|
|
2013
|
+
'session',
|
|
2014
|
+
'email',
|
|
2015
|
+
'file',
|
|
2016
|
+
'db',
|
|
2017
|
+
's3',
|
|
2018
|
+
'dynamo',
|
|
2019
|
+
'api',
|
|
2020
|
+
'config',
|
|
2021
|
+
];
|
|
1715
2022
|
for (const domain of domainPatterns) {
|
|
1716
2023
|
if (name.includes(domain)) {
|
|
1717
2024
|
domains.add(domain);
|
|
1718
2025
|
}
|
|
1719
2026
|
}
|
|
1720
2027
|
}
|
|
1721
|
-
|
|
2028
|
+
|
|
1722
2029
|
// If exports share common stems or domains, they're related
|
|
1723
2030
|
if (stems.size === 1 && exportNames.length >= 2) return true;
|
|
1724
2031
|
if (domains.size === 1 && exportNames.length >= 2) return true;
|
|
1725
|
-
|
|
2032
|
+
|
|
1726
2033
|
// Check for utilities with same service prefix (e.g., dynamodbGet, dynamodbPut)
|
|
1727
|
-
const prefixes = exportNames
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
2034
|
+
const prefixes = exportNames
|
|
2035
|
+
.map((name) => {
|
|
2036
|
+
// Extract prefix before first capital letter or common separator
|
|
2037
|
+
const match = name.match(/^([a-z]+)/);
|
|
2038
|
+
return match ? match[1] : '';
|
|
2039
|
+
})
|
|
2040
|
+
.filter((p) => p.length >= 3);
|
|
2041
|
+
|
|
1733
2042
|
if (prefixes.length >= 2) {
|
|
1734
2043
|
const uniquePrefixes = new Set(prefixes);
|
|
1735
2044
|
if (uniquePrefixes.size === 1) return true;
|
|
@@ -1737,23 +2046,62 @@ function hasRelatedExportNames(exportNames: string[]): boolean {
|
|
|
1737
2046
|
|
|
1738
2047
|
// Check for shared entity noun across all exports using camelCase token splitting
|
|
1739
2048
|
// e.g. getUserReceipts + createPendingReceipt both contain 'receipt'
|
|
1740
|
-
const nounSets = exportNames.map(name => {
|
|
2049
|
+
const nounSets = exportNames.map((name) => {
|
|
1741
2050
|
const tokens = name
|
|
1742
2051
|
.replace(/([A-Z])/g, ' $1')
|
|
1743
2052
|
.trim()
|
|
1744
2053
|
.toLowerCase()
|
|
1745
2054
|
.split(/[\s_-]+/)
|
|
1746
2055
|
.filter(Boolean);
|
|
1747
|
-
const skip = new Set([
|
|
1748
|
-
'
|
|
1749
|
-
'
|
|
1750
|
-
'
|
|
1751
|
-
|
|
1752
|
-
|
|
2056
|
+
const skip = new Set([
|
|
2057
|
+
'get',
|
|
2058
|
+
'set',
|
|
2059
|
+
'create',
|
|
2060
|
+
'update',
|
|
2061
|
+
'delete',
|
|
2062
|
+
'fetch',
|
|
2063
|
+
'save',
|
|
2064
|
+
'load',
|
|
2065
|
+
'parse',
|
|
2066
|
+
'format',
|
|
2067
|
+
'validate',
|
|
2068
|
+
'convert',
|
|
2069
|
+
'transform',
|
|
2070
|
+
'build',
|
|
2071
|
+
'generate',
|
|
2072
|
+
'render',
|
|
2073
|
+
'send',
|
|
2074
|
+
'receive',
|
|
2075
|
+
'find',
|
|
2076
|
+
'list',
|
|
2077
|
+
'add',
|
|
2078
|
+
'remove',
|
|
2079
|
+
'insert',
|
|
2080
|
+
'upsert',
|
|
2081
|
+
'put',
|
|
2082
|
+
'read',
|
|
2083
|
+
'write',
|
|
2084
|
+
'check',
|
|
2085
|
+
'handle',
|
|
2086
|
+
'process',
|
|
2087
|
+
'pending',
|
|
2088
|
+
'active',
|
|
2089
|
+
'current',
|
|
2090
|
+
'new',
|
|
2091
|
+
'old',
|
|
2092
|
+
'all',
|
|
2093
|
+
]);
|
|
2094
|
+
const singularize = (w: string) =>
|
|
2095
|
+
w.endsWith('s') && w.length > 3 ? w.slice(0, -1) : w;
|
|
2096
|
+
return new Set(
|
|
2097
|
+
tokens.filter((t) => !skip.has(t) && t.length > 2).map(singularize)
|
|
2098
|
+
);
|
|
1753
2099
|
});
|
|
1754
|
-
if (nounSets.length >= 2 && nounSets.every(s => s.size > 0)) {
|
|
2100
|
+
if (nounSets.length >= 2 && nounSets.every((s) => s.size > 0)) {
|
|
1755
2101
|
const [first, ...rest] = nounSets;
|
|
1756
|
-
const commonNouns = Array.from(first).filter(n =>
|
|
2102
|
+
const commonNouns = Array.from(first).filter((n) =>
|
|
2103
|
+
rest.every((s) => s.has(n))
|
|
2104
|
+
);
|
|
1757
2105
|
if (commonNouns.length > 0) return true;
|
|
1758
2106
|
}
|
|
1759
2107
|
|
|
@@ -1762,7 +2110,7 @@ function hasRelatedExportNames(exportNames: string[]): boolean {
|
|
|
1762
2110
|
|
|
1763
2111
|
/**
|
|
1764
2112
|
* Adjust fragmentation score based on file classification
|
|
1765
|
-
*
|
|
2113
|
+
*
|
|
1766
2114
|
* This reduces false positives by:
|
|
1767
2115
|
* - Ignoring fragmentation for barrel exports (they're meant to aggregate)
|
|
1768
2116
|
* - Ignoring fragmentation for type definitions (centralized types are good)
|