@aiready/context-analyzer 0.9.22 → 0.9.25
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 +11 -11
- package/.turbo/turbo-test.log +24 -20
- package/README.md +51 -0
- package/dist/chunk-HOUDVRG2.mjs +1422 -0
- package/dist/chunk-MBE4AQP5.mjs +1362 -0
- package/dist/chunk-XZ645X5U.mjs +1425 -0
- package/dist/cli.js +184 -5
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +188 -5
- package/dist/index.mjs +5 -1
- package/package.json +1 -1
- package/src/__tests__/file-classification.test.ts +251 -0
- package/src/analyzer.ts +281 -0
- package/src/index.ts +52 -5
- package/src/types.ts +14 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
classifyFile,
|
|
4
|
+
adjustFragmentationForClassification,
|
|
5
|
+
getClassificationRecommendations,
|
|
6
|
+
} from '../analyzer';
|
|
7
|
+
import type { DependencyNode, FileClassification } from '../types';
|
|
8
|
+
|
|
9
|
+
describe('file classification', () => {
|
|
10
|
+
const createNode = (overrides: Partial<DependencyNode>): DependencyNode => ({
|
|
11
|
+
file: 'test.ts',
|
|
12
|
+
imports: [],
|
|
13
|
+
exports: [],
|
|
14
|
+
tokenCost: 100,
|
|
15
|
+
linesOfCode: 50,
|
|
16
|
+
...overrides,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('classifyFile', () => {
|
|
20
|
+
it('should classify barrel export files correctly', () => {
|
|
21
|
+
const node = createNode({
|
|
22
|
+
file: 'src/index.ts',
|
|
23
|
+
imports: ['../module1', '../module2', '../module3'],
|
|
24
|
+
exports: [
|
|
25
|
+
{ name: 'func1', type: 'function', inferredDomain: 'module1' },
|
|
26
|
+
{ name: 'func2', type: 'function', inferredDomain: 'module2' },
|
|
27
|
+
{ name: 'func3', type: 'function', inferredDomain: 'module3' },
|
|
28
|
+
],
|
|
29
|
+
linesOfCode: 20, // Sparse code
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const classification = classifyFile(node, 0.5, ['module1', 'module2', 'module3']);
|
|
33
|
+
expect(classification).toBe('barrel-export');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should classify type definition files correctly', () => {
|
|
37
|
+
const node = createNode({
|
|
38
|
+
file: 'src/types.ts',
|
|
39
|
+
exports: [
|
|
40
|
+
{ name: 'User', type: 'interface', inferredDomain: 'user' },
|
|
41
|
+
{ name: 'Order', type: 'interface', inferredDomain: 'order' },
|
|
42
|
+
{ name: 'Product', type: 'type', inferredDomain: 'product' },
|
|
43
|
+
{ name: 'Status', type: 'type', inferredDomain: 'unknown' },
|
|
44
|
+
],
|
|
45
|
+
linesOfCode: 100,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const classification = classifyFile(node, 0.5, ['user', 'order', 'product']);
|
|
49
|
+
expect(classification).toBe('type-definition');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should classify cohesive module files correctly', () => {
|
|
53
|
+
const node = createNode({
|
|
54
|
+
file: 'src/calculator.ts',
|
|
55
|
+
exports: [
|
|
56
|
+
{ name: 'calculate', type: 'function', inferredDomain: 'calc' },
|
|
57
|
+
{ name: 'format', type: 'function', inferredDomain: 'calc' },
|
|
58
|
+
{ name: 'validate', type: 'function', inferredDomain: 'calc' },
|
|
59
|
+
],
|
|
60
|
+
imports: ['../utils'],
|
|
61
|
+
linesOfCode: 300,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const classification = classifyFile(node, 0.8, ['calc']);
|
|
65
|
+
expect(classification).toBe('cohesive-module');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should classify mixed concerns files correctly', () => {
|
|
69
|
+
const node = createNode({
|
|
70
|
+
file: 'src/audit.ts',
|
|
71
|
+
exports: [
|
|
72
|
+
{ name: 'auditStatus', type: 'function', inferredDomain: 'audit' },
|
|
73
|
+
{ name: 'createJob', type: 'function', inferredDomain: 'job' },
|
|
74
|
+
{ name: 'LineItem', type: 'interface', inferredDomain: 'order' },
|
|
75
|
+
{ name: 'SupportingDoc', type: 'type', inferredDomain: 'doc' },
|
|
76
|
+
],
|
|
77
|
+
imports: ['../auth', '../job', '../order'],
|
|
78
|
+
linesOfCode: 384,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const classification = classifyFile(node, 0.3, ['audit', 'job', 'order', 'doc']);
|
|
82
|
+
expect(classification).toBe('mixed-concerns');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should classify files with multiple domains and very low cohesion as mixed-concerns', () => {
|
|
86
|
+
const node = createNode({
|
|
87
|
+
file: 'src/services/mixed-service.ts', // NOT a utility/config path
|
|
88
|
+
exports: [
|
|
89
|
+
{ name: 'DateFormatter', type: 'class', inferredDomain: 'date' }, // Use class to avoid utility detection
|
|
90
|
+
{ name: 'JSONParser', type: 'class', inferredDomain: 'json' },
|
|
91
|
+
{ name: 'EmailValidator', type: 'class', inferredDomain: 'email' },
|
|
92
|
+
],
|
|
93
|
+
imports: [],
|
|
94
|
+
linesOfCode: 150,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Multiple domains + very low cohesion (< 0.4) = mixed concerns
|
|
98
|
+
// Note: NOT in /utils/ or /helpers/ path, uses classes (not just functions/consts)
|
|
99
|
+
const classification = classifyFile(node, 0.3, ['date', 'json', 'email']);
|
|
100
|
+
expect(classification).toBe('mixed-concerns');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should classify single domain files as cohesive-module regardless of cohesion', () => {
|
|
104
|
+
const node = createNode({
|
|
105
|
+
file: 'src/component.ts',
|
|
106
|
+
exports: [
|
|
107
|
+
{ name: 'Component', type: 'function', inferredDomain: 'ui' },
|
|
108
|
+
],
|
|
109
|
+
imports: ['react'],
|
|
110
|
+
linesOfCode: 100,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Single domain = cohesive module (even with medium cohesion)
|
|
114
|
+
const classification = classifyFile(node, 0.6, ['ui']);
|
|
115
|
+
expect(classification).toBe('cohesive-module');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should classify utility files as cohesive-module by design', () => {
|
|
119
|
+
const node = createNode({
|
|
120
|
+
file: 'src/utils/helpers.ts',
|
|
121
|
+
exports: [
|
|
122
|
+
{ name: 'formatDate', type: 'function', inferredDomain: 'date' },
|
|
123
|
+
{ name: 'parseJSON', type: 'function', inferredDomain: 'json' },
|
|
124
|
+
{ name: 'validateEmail', type: 'function', inferredDomain: 'email' },
|
|
125
|
+
],
|
|
126
|
+
imports: [],
|
|
127
|
+
linesOfCode: 150,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Utility files are classified as cohesive by design
|
|
131
|
+
const classification = classifyFile(node, 0.4, ['date', 'json', 'email']);
|
|
132
|
+
expect(classification).toBe('cohesive-module');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should classify config/schema files as cohesive-module', () => {
|
|
136
|
+
const node = createNode({
|
|
137
|
+
file: 'src/db-schema.ts',
|
|
138
|
+
exports: [
|
|
139
|
+
{ name: 'userTable', type: 'const', inferredDomain: 'db' },
|
|
140
|
+
{ name: 'userSchema', type: 'const', inferredDomain: 'schema' },
|
|
141
|
+
],
|
|
142
|
+
imports: ['../db'],
|
|
143
|
+
linesOfCode: 81,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Config/schema files are classified as cohesive
|
|
147
|
+
const classification = classifyFile(node, 0.4, ['db', 'schema']);
|
|
148
|
+
expect(classification).toBe('cohesive-module');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('adjustFragmentationForClassification', () => {
|
|
153
|
+
it('should return 0 fragmentation for barrel exports', () => {
|
|
154
|
+
const result = adjustFragmentationForClassification(0.8, 'barrel-export');
|
|
155
|
+
expect(result).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should return 0 fragmentation for type definitions', () => {
|
|
159
|
+
const result = adjustFragmentationForClassification(0.9, 'type-definition');
|
|
160
|
+
expect(result).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should reduce fragmentation by 70% for cohesive modules', () => {
|
|
164
|
+
const result = adjustFragmentationForClassification(0.6, 'cohesive-module');
|
|
165
|
+
expect(result).toBeCloseTo(0.18, 2); // 0.6 * 0.3
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should keep full fragmentation for mixed concerns', () => {
|
|
169
|
+
const result = adjustFragmentationForClassification(0.7, 'mixed-concerns');
|
|
170
|
+
expect(result).toBe(0.7);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should reduce fragmentation by 30% for unknown classification', () => {
|
|
174
|
+
const result = adjustFragmentationForClassification(0.5, 'unknown');
|
|
175
|
+
expect(result).toBeCloseTo(0.35, 2); // 0.5 * 0.7
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('getClassificationRecommendations', () => {
|
|
180
|
+
it('should provide barrel export recommendations', () => {
|
|
181
|
+
const recommendations = getClassificationRecommendations(
|
|
182
|
+
'barrel-export',
|
|
183
|
+
'src/index.ts',
|
|
184
|
+
['High fragmentation']
|
|
185
|
+
);
|
|
186
|
+
expect(recommendations).toContain('Barrel export file detected - multiple domains are expected here');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should provide type definition recommendations', () => {
|
|
190
|
+
const recommendations = getClassificationRecommendations(
|
|
191
|
+
'type-definition',
|
|
192
|
+
'src/types.ts',
|
|
193
|
+
['High fragmentation']
|
|
194
|
+
);
|
|
195
|
+
expect(recommendations).toContain('Type definition file - centralized types improve consistency');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should provide cohesive module recommendations', () => {
|
|
199
|
+
const recommendations = getClassificationRecommendations(
|
|
200
|
+
'cohesive-module',
|
|
201
|
+
'src/calculator.ts',
|
|
202
|
+
[]
|
|
203
|
+
);
|
|
204
|
+
expect(recommendations).toContain('Module has good cohesion despite its size');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should provide mixed concerns recommendations', () => {
|
|
208
|
+
const recommendations = getClassificationRecommendations(
|
|
209
|
+
'mixed-concerns',
|
|
210
|
+
'src/audit.ts',
|
|
211
|
+
['Multiple domains detected']
|
|
212
|
+
);
|
|
213
|
+
expect(recommendations).toContain('Consider splitting this file by domain');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('integration: barrel export detection edge cases', () => {
|
|
218
|
+
it('should detect barrel export even for non-index files with re-export patterns', () => {
|
|
219
|
+
const node = createNode({
|
|
220
|
+
file: 'src/exports.ts',
|
|
221
|
+
imports: ['../module1', '../module2', '../module3', '../module4', '../module5'],
|
|
222
|
+
exports: [
|
|
223
|
+
{ name: 'a', type: 'function' },
|
|
224
|
+
{ name: 'b', type: 'function' },
|
|
225
|
+
{ name: 'c', type: 'function' },
|
|
226
|
+
{ name: 'd', type: 'function' },
|
|
227
|
+
{ name: 'e', type: 'function' },
|
|
228
|
+
],
|
|
229
|
+
linesOfCode: 25, // Very sparse - mostly re-exports
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const classification = classifyFile(node, 0.5, ['module1', 'module2']);
|
|
233
|
+
expect(classification).toBe('barrel-export');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should not misclassify large component files as barrel exports', () => {
|
|
237
|
+
const node = createNode({
|
|
238
|
+
file: 'src/components/Calculator.tsx', // NOT an index file
|
|
239
|
+
imports: ['react', '../hooks', '../utils'],
|
|
240
|
+
exports: [
|
|
241
|
+
{ name: 'Calculator', type: 'function' },
|
|
242
|
+
],
|
|
243
|
+
linesOfCode: 346, // Substantial code
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Single domain, high cohesion
|
|
247
|
+
const classification = classifyFile(node, 0.9, ['calculator']);
|
|
248
|
+
expect(classification).toBe('cohesive-module');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
package/src/analyzer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
DependencyNode,
|
|
6
6
|
ExportInfo,
|
|
7
7
|
ModuleCluster,
|
|
8
|
+
FileClassification,
|
|
8
9
|
} from './types';
|
|
9
10
|
import { buildCoUsageMatrix, buildTypeGraph, inferDomainFromSemantics } from './semantic-analysis';
|
|
10
11
|
|
|
@@ -901,3 +902,283 @@ function calculateDomainCohesion(exports: ExportInfo[]): number {
|
|
|
901
902
|
const maxEntropy = Math.log2(total);
|
|
902
903
|
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
903
904
|
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Classify a file based on its characteristics to help distinguish
|
|
908
|
+
* real issues from false positives.
|
|
909
|
+
*
|
|
910
|
+
* Classification types:
|
|
911
|
+
* - barrel-export: Re-exports from other modules (index.ts files)
|
|
912
|
+
* - type-definition: Primarily type/interface definitions
|
|
913
|
+
* - cohesive-module: Single domain, high cohesion (acceptable large files)
|
|
914
|
+
* - mixed-concerns: Multiple domains, potential refactoring candidate
|
|
915
|
+
* - unknown: Unable to classify
|
|
916
|
+
*/
|
|
917
|
+
export function classifyFile(
|
|
918
|
+
node: DependencyNode,
|
|
919
|
+
cohesionScore: number,
|
|
920
|
+
domains: string[]
|
|
921
|
+
): FileClassification {
|
|
922
|
+
const { exports, imports, linesOfCode, file } = node;
|
|
923
|
+
|
|
924
|
+
// 1. Check for barrel export (index file that re-exports)
|
|
925
|
+
if (isBarrelExport(node)) {
|
|
926
|
+
return 'barrel-export';
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// 2. Check for type definition file
|
|
930
|
+
if (isTypeDefinitionFile(node)) {
|
|
931
|
+
return 'type-definition';
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// 3. Check for config/schema file (special case - acceptable multi-domain)
|
|
935
|
+
if (isConfigOrSchemaFile(node)) {
|
|
936
|
+
return 'cohesive-module'; // Treat as cohesive since it's intentional
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// 4. Check for cohesive module (single domain + reasonable cohesion)
|
|
940
|
+
const uniqueDomains = domains.filter(d => d !== 'unknown');
|
|
941
|
+
const hasSingleDomain = uniqueDomains.length <= 1;
|
|
942
|
+
const hasReasonableCohesion = cohesionScore >= 0.5; // Lowered threshold
|
|
943
|
+
|
|
944
|
+
// Single domain files are almost always cohesive (even with lower cohesion score)
|
|
945
|
+
if (hasSingleDomain) {
|
|
946
|
+
return 'cohesive-module';
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// 5. Check for utility file pattern (multiple domains but utility purpose)
|
|
950
|
+
if (isUtilityFile(node)) {
|
|
951
|
+
return 'cohesive-module'; // Utilities often have mixed imports by design
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// 6. Check for mixed concerns (multiple domains + low cohesion)
|
|
955
|
+
const hasMultipleDomains = uniqueDomains.length > 1;
|
|
956
|
+
const hasLowCohesion = cohesionScore < 0.4; // Lowered threshold
|
|
957
|
+
|
|
958
|
+
if (hasMultipleDomains && hasLowCohesion) {
|
|
959
|
+
return 'mixed-concerns';
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// 7. Default to cohesive-module for files with reasonable cohesion
|
|
963
|
+
// This reduces false positives for legitimate files
|
|
964
|
+
if (cohesionScore >= 0.5) {
|
|
965
|
+
return 'cohesive-module';
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return 'unknown';
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Detect if a file is a barrel export (re-exports from other modules)
|
|
973
|
+
*
|
|
974
|
+
* Characteristics of barrel exports:
|
|
975
|
+
* - Named "index.ts" or "index.js"
|
|
976
|
+
* - Many re-export statements (export * from, export { x } from)
|
|
977
|
+
* - Little to no actual implementation code
|
|
978
|
+
* - High export count relative to lines of code
|
|
979
|
+
*/
|
|
980
|
+
function isBarrelExport(node: DependencyNode): boolean {
|
|
981
|
+
const { file, exports, imports, linesOfCode } = node;
|
|
982
|
+
|
|
983
|
+
// Check filename pattern
|
|
984
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
985
|
+
const isIndexFile = fileName === 'index.ts' || fileName === 'index.js' ||
|
|
986
|
+
fileName === 'index.tsx' || fileName === 'index.jsx';
|
|
987
|
+
|
|
988
|
+
// Calculate re-export ratio
|
|
989
|
+
// Re-exports typically have form: export { x } from 'module' or export * from 'module'
|
|
990
|
+
// They have imports AND exports, with exports coming from those imports
|
|
991
|
+
const hasReExports = exports.length > 0 && imports.length > 0;
|
|
992
|
+
const highExportToLinesRatio = exports.length > 3 && linesOfCode < exports.length * 5;
|
|
993
|
+
|
|
994
|
+
// Little actual code (mostly import/export statements)
|
|
995
|
+
const sparseCode = linesOfCode > 0 && linesOfCode < 50 && exports.length >= 2;
|
|
996
|
+
|
|
997
|
+
// Index files with re-export patterns
|
|
998
|
+
if (isIndexFile && hasReExports) {
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Non-index files that are clearly barrel exports
|
|
1003
|
+
if (highExportToLinesRatio && imports.length >= exports.length * 0.5) {
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Very sparse files with multiple re-exports
|
|
1008
|
+
if (sparseCode && imports.length > 0) {
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Detect if a file is primarily a type definition file
|
|
1017
|
+
*
|
|
1018
|
+
* Characteristics:
|
|
1019
|
+
* - Mostly type/interface exports
|
|
1020
|
+
* - Little to no runtime code
|
|
1021
|
+
* - Often named *.d.ts or types.ts
|
|
1022
|
+
*/
|
|
1023
|
+
function isTypeDefinitionFile(node: DependencyNode): boolean {
|
|
1024
|
+
const { file, exports } = node;
|
|
1025
|
+
|
|
1026
|
+
// Check filename pattern
|
|
1027
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1028
|
+
const isTypesFile = fileName?.includes('types') || fileName?.includes('.d.ts') ||
|
|
1029
|
+
fileName === 'types.ts' || fileName === 'interfaces.ts';
|
|
1030
|
+
|
|
1031
|
+
// Count type exports vs other exports
|
|
1032
|
+
const typeExports = exports.filter(e => e.type === 'type' || e.type === 'interface');
|
|
1033
|
+
const runtimeExports = exports.filter(e => e.type === 'function' || e.type === 'class' || e.type === 'const');
|
|
1034
|
+
|
|
1035
|
+
// High ratio of type exports
|
|
1036
|
+
const mostlyTypes = exports.length > 0 &&
|
|
1037
|
+
typeExports.length > runtimeExports.length &&
|
|
1038
|
+
typeExports.length / exports.length > 0.7;
|
|
1039
|
+
|
|
1040
|
+
return isTypesFile || mostlyTypes;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Detect if a file is a config/schema file
|
|
1045
|
+
*
|
|
1046
|
+
* Characteristics:
|
|
1047
|
+
* - Named with config, schema, or settings patterns
|
|
1048
|
+
* - Often defines database schemas, configuration objects
|
|
1049
|
+
* - Multiple domains are acceptable (centralized config)
|
|
1050
|
+
*/
|
|
1051
|
+
function isConfigOrSchemaFile(node: DependencyNode): boolean {
|
|
1052
|
+
const { file, exports } = node;
|
|
1053
|
+
|
|
1054
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1055
|
+
|
|
1056
|
+
// Check filename patterns for config/schema files
|
|
1057
|
+
const configPatterns = [
|
|
1058
|
+
'config', 'schema', 'settings', 'options', 'constants',
|
|
1059
|
+
'env', 'environment', '.config.', '-config.', '_config.',
|
|
1060
|
+
];
|
|
1061
|
+
|
|
1062
|
+
const isConfigName = configPatterns.some(pattern =>
|
|
1063
|
+
fileName?.includes(pattern) || fileName?.startsWith(pattern) || fileName?.endsWith(`${pattern}.ts`)
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
// Check if file is in a config/settings directory
|
|
1067
|
+
const isConfigPath = file.toLowerCase().includes('/config/') ||
|
|
1068
|
+
file.toLowerCase().includes('/schemas/') ||
|
|
1069
|
+
file.toLowerCase().includes('/settings/');
|
|
1070
|
+
|
|
1071
|
+
// Check for schema-like exports (often have table/model definitions)
|
|
1072
|
+
const hasSchemaExports = exports.some(e =>
|
|
1073
|
+
e.name.toLowerCase().includes('table') ||
|
|
1074
|
+
e.name.toLowerCase().includes('schema') ||
|
|
1075
|
+
e.name.toLowerCase().includes('config') ||
|
|
1076
|
+
e.name.toLowerCase().includes('setting')
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
return isConfigName || isConfigPath || hasSchemaExports;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Detect if a file is a utility/helper file
|
|
1084
|
+
*
|
|
1085
|
+
* Characteristics:
|
|
1086
|
+
* - Named with util, helper, or utility patterns
|
|
1087
|
+
* - Often contains mixed helper functions by design
|
|
1088
|
+
* - Multiple domains are acceptable (utility purpose)
|
|
1089
|
+
*/
|
|
1090
|
+
function isUtilityFile(node: DependencyNode): boolean {
|
|
1091
|
+
const { file, exports } = node;
|
|
1092
|
+
|
|
1093
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1094
|
+
|
|
1095
|
+
// Check filename patterns for utility files
|
|
1096
|
+
const utilityPatterns = [
|
|
1097
|
+
'util', 'utility', 'utilities', 'helper', 'helpers',
|
|
1098
|
+
'common', 'shared', 'lib', 'toolbox', 'toolkit',
|
|
1099
|
+
'.util.', '-util.', '_util.',
|
|
1100
|
+
];
|
|
1101
|
+
|
|
1102
|
+
const isUtilityName = utilityPatterns.some(pattern =>
|
|
1103
|
+
fileName?.includes(pattern)
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
// Check if file is in a utils/helpers directory
|
|
1107
|
+
const isUtilityPath = file.toLowerCase().includes('/utils/') ||
|
|
1108
|
+
file.toLowerCase().includes('/helpers/') ||
|
|
1109
|
+
file.toLowerCase().includes('/lib/') ||
|
|
1110
|
+
file.toLowerCase().includes('/common/');
|
|
1111
|
+
|
|
1112
|
+
// Check if file has many small utility-like exports
|
|
1113
|
+
const hasManySmallExports = exports.length >= 3 && exports.every(e =>
|
|
1114
|
+
e.type === 'function' || e.type === 'const'
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
return isUtilityName || isUtilityPath || hasManySmallExports;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Adjust fragmentation score based on file classification
|
|
1122
|
+
*
|
|
1123
|
+
* This reduces false positives by:
|
|
1124
|
+
* - Ignoring fragmentation for barrel exports (they're meant to aggregate)
|
|
1125
|
+
* - Ignoring fragmentation for type definitions (centralized types are good)
|
|
1126
|
+
* - Reducing fragmentation for cohesive modules (large but focused is OK)
|
|
1127
|
+
*/
|
|
1128
|
+
export function adjustFragmentationForClassification(
|
|
1129
|
+
baseFragmentation: number,
|
|
1130
|
+
classification: FileClassification
|
|
1131
|
+
): number {
|
|
1132
|
+
switch (classification) {
|
|
1133
|
+
case 'barrel-export':
|
|
1134
|
+
// Barrel exports are meant to have multiple domains - no fragmentation
|
|
1135
|
+
return 0;
|
|
1136
|
+
case 'type-definition':
|
|
1137
|
+
// Centralized type definitions are good practice - no fragmentation
|
|
1138
|
+
return 0;
|
|
1139
|
+
case 'cohesive-module':
|
|
1140
|
+
// Cohesive modules get a significant discount
|
|
1141
|
+
return baseFragmentation * 0.3;
|
|
1142
|
+
case 'mixed-concerns':
|
|
1143
|
+
// Mixed concerns keep full fragmentation score
|
|
1144
|
+
return baseFragmentation;
|
|
1145
|
+
default:
|
|
1146
|
+
// Unknown gets a small discount (benefit of doubt)
|
|
1147
|
+
return baseFragmentation * 0.7;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Get classification-specific recommendations
|
|
1153
|
+
*/
|
|
1154
|
+
export function getClassificationRecommendations(
|
|
1155
|
+
classification: FileClassification,
|
|
1156
|
+
file: string,
|
|
1157
|
+
issues: string[]
|
|
1158
|
+
): string[] {
|
|
1159
|
+
switch (classification) {
|
|
1160
|
+
case 'barrel-export':
|
|
1161
|
+
return [
|
|
1162
|
+
'Barrel export file detected - multiple domains are expected here',
|
|
1163
|
+
'Consider if this barrel export improves or hinders discoverability',
|
|
1164
|
+
];
|
|
1165
|
+
case 'type-definition':
|
|
1166
|
+
return [
|
|
1167
|
+
'Type definition file - centralized types improve consistency',
|
|
1168
|
+
'Consider splitting if file becomes too large (>500 lines)',
|
|
1169
|
+
];
|
|
1170
|
+
case 'cohesive-module':
|
|
1171
|
+
return [
|
|
1172
|
+
'Module has good cohesion despite its size',
|
|
1173
|
+
'Consider documenting the module boundaries for AI assistants',
|
|
1174
|
+
];
|
|
1175
|
+
case 'mixed-concerns':
|
|
1176
|
+
return [
|
|
1177
|
+
'Consider splitting this file by domain',
|
|
1178
|
+
'Identify independent responsibilities and extract them',
|
|
1179
|
+
'Review import dependencies to understand coupling',
|
|
1180
|
+
];
|
|
1181
|
+
default:
|
|
1182
|
+
return issues;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
detectModuleClusters,
|
|
12
12
|
calculatePathEntropy,
|
|
13
13
|
calculateDirectoryDistance,
|
|
14
|
+
classifyFile,
|
|
15
|
+
adjustFragmentationForClassification,
|
|
16
|
+
getClassificationRecommendations,
|
|
14
17
|
} from './analyzer';
|
|
15
18
|
import { calculateContextScore } from './scoring';
|
|
16
19
|
import type {
|
|
@@ -22,6 +25,7 @@ import type {
|
|
|
22
25
|
DomainSignals,
|
|
23
26
|
CoUsageData,
|
|
24
27
|
TypeDependency,
|
|
28
|
+
FileClassification,
|
|
25
29
|
} from './types';
|
|
26
30
|
import {
|
|
27
31
|
buildCoUsageMatrix,
|
|
@@ -42,6 +46,12 @@ export type {
|
|
|
42
46
|
DomainSignals,
|
|
43
47
|
CoUsageData,
|
|
44
48
|
TypeDependency,
|
|
49
|
+
FileClassification,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
classifyFile,
|
|
54
|
+
adjustFragmentationForClassification,
|
|
45
55
|
};
|
|
46
56
|
|
|
47
57
|
export {
|
|
@@ -196,6 +206,7 @@ export async function analyzeContext(
|
|
|
196
206
|
contextBudget: metric.contextBudget,
|
|
197
207
|
fragmentationScore: 0,
|
|
198
208
|
relatedFiles: [],
|
|
209
|
+
fileClassification: 'unknown' as const, // Python files not yet classified
|
|
199
210
|
severity,
|
|
200
211
|
issues,
|
|
201
212
|
recommendations,
|
|
@@ -275,6 +286,41 @@ export async function analyzeContext(
|
|
|
275
286
|
...new Set(node.exports.map((e) => e.inferredDomain || 'unknown')),
|
|
276
287
|
];
|
|
277
288
|
|
|
289
|
+
// Classify the file to help distinguish real issues from false positives
|
|
290
|
+
const fileClassification = classifyFile(node, cohesionScore, domains);
|
|
291
|
+
|
|
292
|
+
// Adjust fragmentation based on classification
|
|
293
|
+
const adjustedFragmentationScore = adjustFragmentationForClassification(
|
|
294
|
+
fragmentationScore,
|
|
295
|
+
fileClassification
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Get classification-specific recommendations
|
|
299
|
+
const classificationRecommendations = getClassificationRecommendations(
|
|
300
|
+
fileClassification,
|
|
301
|
+
file,
|
|
302
|
+
issues
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Re-analyze issues with adjusted fragmentation
|
|
306
|
+
const {
|
|
307
|
+
severity: adjustedSeverity,
|
|
308
|
+
issues: adjustedIssues,
|
|
309
|
+
recommendations: finalRecommendations,
|
|
310
|
+
potentialSavings: adjustedSavings,
|
|
311
|
+
} = analyzeIssues({
|
|
312
|
+
file,
|
|
313
|
+
importDepth,
|
|
314
|
+
contextBudget,
|
|
315
|
+
cohesionScore,
|
|
316
|
+
fragmentationScore: adjustedFragmentationScore,
|
|
317
|
+
maxDepth,
|
|
318
|
+
maxContextBudget,
|
|
319
|
+
minCohesion,
|
|
320
|
+
maxFragmentation,
|
|
321
|
+
circularDeps,
|
|
322
|
+
});
|
|
323
|
+
|
|
278
324
|
results.push({
|
|
279
325
|
file,
|
|
280
326
|
tokenCost: node.tokenCost,
|
|
@@ -287,12 +333,13 @@ export async function analyzeContext(
|
|
|
287
333
|
domains,
|
|
288
334
|
exportCount: node.exports.length,
|
|
289
335
|
contextBudget,
|
|
290
|
-
fragmentationScore,
|
|
336
|
+
fragmentationScore: adjustedFragmentationScore,
|
|
291
337
|
relatedFiles,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
338
|
+
fileClassification,
|
|
339
|
+
severity: adjustedSeverity,
|
|
340
|
+
issues: adjustedIssues,
|
|
341
|
+
recommendations: [...finalRecommendations, ...classificationRecommendations.slice(0, 1)],
|
|
342
|
+
potentialSavings: adjustedSavings,
|
|
296
343
|
});
|
|
297
344
|
}
|
|
298
345
|
|
package/src/types.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface ContextAnalysisResult {
|
|
|
32
32
|
fragmentationScore: number; // 0-1, how scattered is this domain (0 = well-grouped)
|
|
33
33
|
relatedFiles: string[]; // Files that should be loaded together
|
|
34
34
|
|
|
35
|
+
// File classification (NEW)
|
|
36
|
+
fileClassification: FileClassification; // Type of file for analysis context
|
|
37
|
+
|
|
35
38
|
// Recommendations
|
|
36
39
|
severity: 'critical' | 'major' | 'minor' | 'info';
|
|
37
40
|
issues: string[]; // List of specific problems
|
|
@@ -39,6 +42,17 @@ export interface ContextAnalysisResult {
|
|
|
39
42
|
potentialSavings: number; // Estimated token savings if fixed
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Classification of file type for analysis context
|
|
47
|
+
* Helps distinguish real issues from false positives
|
|
48
|
+
*/
|
|
49
|
+
export type FileClassification =
|
|
50
|
+
| 'barrel-export' // Re-exports from other modules (index.ts files)
|
|
51
|
+
| 'type-definition' // Primarily type/interface definitions
|
|
52
|
+
| 'cohesive-module' // Single domain, high cohesion (acceptable large files)
|
|
53
|
+
| 'mixed-concerns' // Multiple domains, potential refactoring candidate
|
|
54
|
+
| 'unknown'; // Unable to classify
|
|
55
|
+
|
|
42
56
|
export interface ModuleCluster {
|
|
43
57
|
domain: string; // e.g., "user-management", "auth"
|
|
44
58
|
files: string[];
|