@aiready/context-analyzer 0.9.20 → 0.9.23

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.
@@ -0,0 +1,216 @@
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 low cohesion as mixed-concerns', () => {
86
+ const node = createNode({
87
+ file: 'src/utils.ts',
88
+ exports: [
89
+ { name: 'formatDate', type: 'function', inferredDomain: 'date' },
90
+ { name: 'parseJSON', type: 'function', inferredDomain: 'json' },
91
+ { name: 'validateEmail', type: 'function', inferredDomain: 'email' },
92
+ ],
93
+ imports: [],
94
+ linesOfCode: 150,
95
+ });
96
+
97
+ const classification = classifyFile(node, 0.4, ['date', 'json', 'email']);
98
+ expect(classification).toBe('mixed-concerns');
99
+ });
100
+
101
+ it('should return unknown for files that do not fit other categories', () => {
102
+ const node = createNode({
103
+ file: 'src/component.ts',
104
+ exports: [
105
+ { name: 'Component', type: 'function', inferredDomain: 'ui' },
106
+ ],
107
+ imports: ['react'],
108
+ linesOfCode: 100,
109
+ });
110
+
111
+ // Medium cohesion (0.6), single domain - not quite cohesive-module (needs 0.7)
112
+ const classification = classifyFile(node, 0.6, ['ui']);
113
+ expect(classification).toBe('unknown');
114
+ });
115
+ });
116
+
117
+ describe('adjustFragmentationForClassification', () => {
118
+ it('should return 0 fragmentation for barrel exports', () => {
119
+ const result = adjustFragmentationForClassification(0.8, 'barrel-export');
120
+ expect(result).toBe(0);
121
+ });
122
+
123
+ it('should return 0 fragmentation for type definitions', () => {
124
+ const result = adjustFragmentationForClassification(0.9, 'type-definition');
125
+ expect(result).toBe(0);
126
+ });
127
+
128
+ it('should reduce fragmentation by 70% for cohesive modules', () => {
129
+ const result = adjustFragmentationForClassification(0.6, 'cohesive-module');
130
+ expect(result).toBeCloseTo(0.18, 2); // 0.6 * 0.3
131
+ });
132
+
133
+ it('should keep full fragmentation for mixed concerns', () => {
134
+ const result = adjustFragmentationForClassification(0.7, 'mixed-concerns');
135
+ expect(result).toBe(0.7);
136
+ });
137
+
138
+ it('should reduce fragmentation by 30% for unknown classification', () => {
139
+ const result = adjustFragmentationForClassification(0.5, 'unknown');
140
+ expect(result).toBeCloseTo(0.35, 2); // 0.5 * 0.7
141
+ });
142
+ });
143
+
144
+ describe('getClassificationRecommendations', () => {
145
+ it('should provide barrel export recommendations', () => {
146
+ const recommendations = getClassificationRecommendations(
147
+ 'barrel-export',
148
+ 'src/index.ts',
149
+ ['High fragmentation']
150
+ );
151
+ expect(recommendations).toContain('Barrel export file detected - multiple domains are expected here');
152
+ });
153
+
154
+ it('should provide type definition recommendations', () => {
155
+ const recommendations = getClassificationRecommendations(
156
+ 'type-definition',
157
+ 'src/types.ts',
158
+ ['High fragmentation']
159
+ );
160
+ expect(recommendations).toContain('Type definition file - centralized types improve consistency');
161
+ });
162
+
163
+ it('should provide cohesive module recommendations', () => {
164
+ const recommendations = getClassificationRecommendations(
165
+ 'cohesive-module',
166
+ 'src/calculator.ts',
167
+ []
168
+ );
169
+ expect(recommendations).toContain('Module has good cohesion despite its size');
170
+ });
171
+
172
+ it('should provide mixed concerns recommendations', () => {
173
+ const recommendations = getClassificationRecommendations(
174
+ 'mixed-concerns',
175
+ 'src/audit.ts',
176
+ ['Multiple domains detected']
177
+ );
178
+ expect(recommendations).toContain('Consider splitting this file by domain');
179
+ });
180
+ });
181
+
182
+ describe('integration: barrel export detection edge cases', () => {
183
+ it('should detect barrel export even for non-index files with re-export patterns', () => {
184
+ const node = createNode({
185
+ file: 'src/exports.ts',
186
+ imports: ['../module1', '../module2', '../module3', '../module4', '../module5'],
187
+ exports: [
188
+ { name: 'a', type: 'function' },
189
+ { name: 'b', type: 'function' },
190
+ { name: 'c', type: 'function' },
191
+ { name: 'd', type: 'function' },
192
+ { name: 'e', type: 'function' },
193
+ ],
194
+ linesOfCode: 25, // Very sparse - mostly re-exports
195
+ });
196
+
197
+ const classification = classifyFile(node, 0.5, ['module1', 'module2']);
198
+ expect(classification).toBe('barrel-export');
199
+ });
200
+
201
+ it('should not misclassify large component files as barrel exports', () => {
202
+ const node = createNode({
203
+ file: 'src/components/Calculator.tsx', // NOT an index file
204
+ imports: ['react', '../hooks', '../utils'],
205
+ exports: [
206
+ { name: 'Calculator', type: 'function' },
207
+ ],
208
+ linesOfCode: 346, // Substantial code
209
+ });
210
+
211
+ // Single domain, high cohesion
212
+ const classification = classifyFile(node, 0.9, ['calculator']);
213
+ expect(classification).toBe('cohesive-module');
214
+ });
215
+ });
216
+ });
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,189 @@ 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 } = 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 cohesive module (single domain + high cohesion)
935
+ const uniqueDomains = domains.filter(d => d !== 'unknown');
936
+ const hasSingleDomain = uniqueDomains.length <= 1;
937
+ const hasHighCohesion = cohesionScore >= 0.7;
938
+
939
+ if (hasSingleDomain && hasHighCohesion) {
940
+ return 'cohesive-module';
941
+ }
942
+
943
+ // 4. Check for mixed concerns (multiple domains + low cohesion)
944
+ const hasMultipleDomains = uniqueDomains.length > 1;
945
+ const hasLowCohesion = cohesionScore < 0.5;
946
+
947
+ if (hasMultipleDomains || hasLowCohesion) {
948
+ return 'mixed-concerns';
949
+ }
950
+
951
+ return 'unknown';
952
+ }
953
+
954
+ /**
955
+ * Detect if a file is a barrel export (re-exports from other modules)
956
+ *
957
+ * Characteristics of barrel exports:
958
+ * - Named "index.ts" or "index.js"
959
+ * - Many re-export statements (export * from, export { x } from)
960
+ * - Little to no actual implementation code
961
+ * - High export count relative to lines of code
962
+ */
963
+ function isBarrelExport(node: DependencyNode): boolean {
964
+ const { file, exports, imports, linesOfCode } = node;
965
+
966
+ // Check filename pattern
967
+ const fileName = file.split('/').pop()?.toLowerCase();
968
+ const isIndexFile = fileName === 'index.ts' || fileName === 'index.js' ||
969
+ fileName === 'index.tsx' || fileName === 'index.jsx';
970
+
971
+ // Calculate re-export ratio
972
+ // Re-exports typically have form: export { x } from 'module' or export * from 'module'
973
+ // They have imports AND exports, with exports coming from those imports
974
+ const hasReExports = exports.length > 0 && imports.length > 0;
975
+ const highExportToLinesRatio = exports.length > 3 && linesOfCode < exports.length * 5;
976
+
977
+ // Little actual code (mostly import/export statements)
978
+ const sparseCode = linesOfCode > 0 && linesOfCode < 50 && exports.length >= 2;
979
+
980
+ // Index files with re-export patterns
981
+ if (isIndexFile && hasReExports) {
982
+ return true;
983
+ }
984
+
985
+ // Non-index files that are clearly barrel exports
986
+ if (highExportToLinesRatio && imports.length >= exports.length * 0.5) {
987
+ return true;
988
+ }
989
+
990
+ // Very sparse files with multiple re-exports
991
+ if (sparseCode && imports.length > 0) {
992
+ return true;
993
+ }
994
+
995
+ return false;
996
+ }
997
+
998
+ /**
999
+ * Detect if a file is primarily a type definition file
1000
+ *
1001
+ * Characteristics:
1002
+ * - Mostly type/interface exports
1003
+ * - Little to no runtime code
1004
+ * - Often named *.d.ts or types.ts
1005
+ */
1006
+ function isTypeDefinitionFile(node: DependencyNode): boolean {
1007
+ const { file, exports } = node;
1008
+
1009
+ // Check filename pattern
1010
+ const fileName = file.split('/').pop()?.toLowerCase();
1011
+ const isTypesFile = fileName?.includes('types') || fileName?.includes('.d.ts') ||
1012
+ fileName === 'types.ts' || fileName === 'interfaces.ts';
1013
+
1014
+ // Count type exports vs other exports
1015
+ const typeExports = exports.filter(e => e.type === 'type' || e.type === 'interface');
1016
+ const runtimeExports = exports.filter(e => e.type === 'function' || e.type === 'class' || e.type === 'const');
1017
+
1018
+ // High ratio of type exports
1019
+ const mostlyTypes = exports.length > 0 &&
1020
+ typeExports.length > runtimeExports.length &&
1021
+ typeExports.length / exports.length > 0.7;
1022
+
1023
+ return isTypesFile || mostlyTypes;
1024
+ }
1025
+
1026
+ /**
1027
+ * Adjust fragmentation score based on file classification
1028
+ *
1029
+ * This reduces false positives by:
1030
+ * - Ignoring fragmentation for barrel exports (they're meant to aggregate)
1031
+ * - Ignoring fragmentation for type definitions (centralized types are good)
1032
+ * - Reducing fragmentation for cohesive modules (large but focused is OK)
1033
+ */
1034
+ export function adjustFragmentationForClassification(
1035
+ baseFragmentation: number,
1036
+ classification: FileClassification
1037
+ ): number {
1038
+ switch (classification) {
1039
+ case 'barrel-export':
1040
+ // Barrel exports are meant to have multiple domains - no fragmentation
1041
+ return 0;
1042
+ case 'type-definition':
1043
+ // Centralized type definitions are good practice - no fragmentation
1044
+ return 0;
1045
+ case 'cohesive-module':
1046
+ // Cohesive modules get a significant discount
1047
+ return baseFragmentation * 0.3;
1048
+ case 'mixed-concerns':
1049
+ // Mixed concerns keep full fragmentation score
1050
+ return baseFragmentation;
1051
+ default:
1052
+ // Unknown gets a small discount (benefit of doubt)
1053
+ return baseFragmentation * 0.7;
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Get classification-specific recommendations
1059
+ */
1060
+ export function getClassificationRecommendations(
1061
+ classification: FileClassification,
1062
+ file: string,
1063
+ issues: string[]
1064
+ ): string[] {
1065
+ switch (classification) {
1066
+ case 'barrel-export':
1067
+ return [
1068
+ 'Barrel export file detected - multiple domains are expected here',
1069
+ 'Consider if this barrel export improves or hinders discoverability',
1070
+ ];
1071
+ case 'type-definition':
1072
+ return [
1073
+ 'Type definition file - centralized types improve consistency',
1074
+ 'Consider splitting if file becomes too large (>500 lines)',
1075
+ ];
1076
+ case 'cohesive-module':
1077
+ return [
1078
+ 'Module has good cohesion despite its size',
1079
+ 'Consider documenting the module boundaries for AI assistants',
1080
+ ];
1081
+ case 'mixed-concerns':
1082
+ return [
1083
+ 'Consider splitting this file by domain',
1084
+ 'Identify independent responsibilities and extract them',
1085
+ 'Review import dependencies to understand coupling',
1086
+ ];
1087
+ default:
1088
+ return issues;
1089
+ }
1090
+ }
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
- severity,
293
- issues,
294
- recommendations,
295
- potentialSavings,
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[];