@aiready/context-analyzer 0.9.23 → 0.9.26
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 +21 -24
- package/dist/chunk-HOUDVRG2.mjs +1422 -0
- package/dist/chunk-PJD4VCIH.mjs +1722 -0
- package/dist/chunk-XZ645X5U.mjs +1425 -0
- package/dist/cli.js +368 -8
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +368 -8
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/__tests__/file-classification.test.ts +596 -10
- package/src/analyzer.ts +651 -8
- package/src/index.ts +11 -3
- package/src/types.ts +6 -0
package/src/analyzer.ts
CHANGED
|
@@ -911,6 +911,11 @@ function calculateDomainCohesion(exports: ExportInfo[]): number {
|
|
|
911
911
|
* - barrel-export: Re-exports from other modules (index.ts files)
|
|
912
912
|
* - type-definition: Primarily type/interface definitions
|
|
913
913
|
* - cohesive-module: Single domain, high cohesion (acceptable large files)
|
|
914
|
+
* - utility-module: Utility/helper files with cohesive purpose despite multi-domain
|
|
915
|
+
* - service-file: Service files orchestrating multiple dependencies
|
|
916
|
+
* - lambda-handler: Lambda/API handlers with single business purpose
|
|
917
|
+
* - email-template: Email templates/layouts with structural cohesion
|
|
918
|
+
* - parser-file: Parser/transformer files with single transformation purpose
|
|
914
919
|
* - mixed-concerns: Multiple domains, potential refactoring candidate
|
|
915
920
|
* - unknown: Unable to classify
|
|
916
921
|
*/
|
|
@@ -919,7 +924,7 @@ export function classifyFile(
|
|
|
919
924
|
cohesionScore: number,
|
|
920
925
|
domains: string[]
|
|
921
926
|
): FileClassification {
|
|
922
|
-
const { exports, imports, linesOfCode } = node;
|
|
927
|
+
const { exports, imports, linesOfCode, file } = node;
|
|
923
928
|
|
|
924
929
|
// 1. Check for barrel export (index file that re-exports)
|
|
925
930
|
if (isBarrelExport(node)) {
|
|
@@ -931,23 +936,69 @@ export function classifyFile(
|
|
|
931
936
|
return 'type-definition';
|
|
932
937
|
}
|
|
933
938
|
|
|
934
|
-
// 3. Check for
|
|
939
|
+
// 3. Check for config/schema file (special case - acceptable multi-domain)
|
|
940
|
+
if (isConfigOrSchemaFile(node)) {
|
|
941
|
+
return 'cohesive-module'; // Treat as cohesive since it's intentional
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// 4. Check for lambda handlers FIRST (they often look like mixed concerns)
|
|
945
|
+
if (isLambdaHandler(node)) {
|
|
946
|
+
return 'lambda-handler';
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// 5. Check for email templates (they reference multiple domains but serve one purpose)
|
|
950
|
+
if (isEmailTemplate(node)) {
|
|
951
|
+
return 'email-template';
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// 6. Check for parser/transformer files
|
|
955
|
+
if (isParserFile(node)) {
|
|
956
|
+
return 'parser-file';
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 7. Check for service files
|
|
960
|
+
if (isServiceFile(node)) {
|
|
961
|
+
return 'service-file';
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// 8. Check for session/state management files
|
|
965
|
+
if (isSessionFile(node)) {
|
|
966
|
+
return 'cohesive-module'; // Session files manage state cohesively
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// 9. Check for Next.js App Router pages (metadata + faqJsonLd + default export)
|
|
970
|
+
if (isNextJsPage(node)) {
|
|
971
|
+
return 'nextjs-page';
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// 10. Check for utility file pattern (multiple domains but utility purpose)
|
|
975
|
+
if (isUtilityFile(node)) {
|
|
976
|
+
return 'utility-module';
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// 10. Check for cohesive module (single domain + reasonable cohesion)
|
|
935
980
|
const uniqueDomains = domains.filter(d => d !== 'unknown');
|
|
936
981
|
const hasSingleDomain = uniqueDomains.length <= 1;
|
|
937
|
-
const hasHighCohesion = cohesionScore >= 0.7;
|
|
938
982
|
|
|
939
|
-
|
|
983
|
+
// Single domain files are almost always cohesive (even with lower cohesion score)
|
|
984
|
+
if (hasSingleDomain) {
|
|
940
985
|
return 'cohesive-module';
|
|
941
986
|
}
|
|
942
987
|
|
|
943
|
-
//
|
|
988
|
+
// 11. Check for mixed concerns (multiple domains + low cohesion)
|
|
944
989
|
const hasMultipleDomains = uniqueDomains.length > 1;
|
|
945
|
-
const hasLowCohesion = cohesionScore < 0.
|
|
990
|
+
const hasLowCohesion = cohesionScore < 0.4; // Lowered threshold
|
|
946
991
|
|
|
947
|
-
if (hasMultipleDomains
|
|
992
|
+
if (hasMultipleDomains && hasLowCohesion) {
|
|
948
993
|
return 'mixed-concerns';
|
|
949
994
|
}
|
|
950
995
|
|
|
996
|
+
// 12. Default to cohesive-module for files with reasonable cohesion
|
|
997
|
+
// This reduces false positives for legitimate files
|
|
998
|
+
if (cohesionScore >= 0.5) {
|
|
999
|
+
return 'cohesive-module';
|
|
1000
|
+
}
|
|
1001
|
+
|
|
951
1002
|
return 'unknown';
|
|
952
1003
|
}
|
|
953
1004
|
|
|
@@ -1002,6 +1053,7 @@ function isBarrelExport(node: DependencyNode): boolean {
|
|
|
1002
1053
|
* - Mostly type/interface exports
|
|
1003
1054
|
* - Little to no runtime code
|
|
1004
1055
|
* - Often named *.d.ts or types.ts
|
|
1056
|
+
* - Located in /types/, /typings/, or @types directories
|
|
1005
1057
|
*/
|
|
1006
1058
|
function isTypeDefinitionFile(node: DependencyNode): boolean {
|
|
1007
1059
|
const { file, exports } = node;
|
|
@@ -1011,6 +1063,14 @@ function isTypeDefinitionFile(node: DependencyNode): boolean {
|
|
|
1011
1063
|
const isTypesFile = fileName?.includes('types') || fileName?.includes('.d.ts') ||
|
|
1012
1064
|
fileName === 'types.ts' || fileName === 'interfaces.ts';
|
|
1013
1065
|
|
|
1066
|
+
// Check if file is in a types directory (path-based detection)
|
|
1067
|
+
const lowerPath = file.toLowerCase();
|
|
1068
|
+
const isTypesPath = lowerPath.includes('/types/') ||
|
|
1069
|
+
lowerPath.includes('/typings/') ||
|
|
1070
|
+
lowerPath.includes('/@types/') ||
|
|
1071
|
+
lowerPath.startsWith('types/') ||
|
|
1072
|
+
lowerPath.startsWith('typings/');
|
|
1073
|
+
|
|
1014
1074
|
// Count type exports vs other exports
|
|
1015
1075
|
const typeExports = exports.filter(e => e.type === 'type' || e.type === 'interface');
|
|
1016
1076
|
const runtimeExports = exports.filter(e => e.type === 'function' || e.type === 'class' || e.type === 'const');
|
|
@@ -1020,7 +1080,550 @@ function isTypeDefinitionFile(node: DependencyNode): boolean {
|
|
|
1020
1080
|
typeExports.length > runtimeExports.length &&
|
|
1021
1081
|
typeExports.length / exports.length > 0.7;
|
|
1022
1082
|
|
|
1023
|
-
|
|
1083
|
+
// Pure type files (only type/interface exports, no runtime code)
|
|
1084
|
+
const pureTypeFile = exports.length > 0 && typeExports.length === exports.length;
|
|
1085
|
+
|
|
1086
|
+
// Empty export file in types directory (might just be re-exports)
|
|
1087
|
+
const emptyOrReExportInTypesDir = isTypesPath && exports.length === 0;
|
|
1088
|
+
|
|
1089
|
+
return isTypesFile || isTypesPath || mostlyTypes || pureTypeFile || emptyOrReExportInTypesDir;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Detect if a file is a config/schema file
|
|
1094
|
+
*
|
|
1095
|
+
* Characteristics:
|
|
1096
|
+
* - Named with config, schema, or settings patterns
|
|
1097
|
+
* - Often defines database schemas, configuration objects
|
|
1098
|
+
* - Multiple domains are acceptable (centralized config)
|
|
1099
|
+
*/
|
|
1100
|
+
function isConfigOrSchemaFile(node: DependencyNode): boolean {
|
|
1101
|
+
const { file, exports } = node;
|
|
1102
|
+
|
|
1103
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1104
|
+
|
|
1105
|
+
// Check filename patterns for config/schema files
|
|
1106
|
+
const configPatterns = [
|
|
1107
|
+
'config', 'schema', 'settings', 'options', 'constants',
|
|
1108
|
+
'env', 'environment', '.config.', '-config.', '_config.',
|
|
1109
|
+
];
|
|
1110
|
+
|
|
1111
|
+
const isConfigName = configPatterns.some(pattern =>
|
|
1112
|
+
fileName?.includes(pattern) || fileName?.startsWith(pattern) || fileName?.endsWith(`${pattern}.ts`)
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
// Check if file is in a config/settings directory
|
|
1116
|
+
const isConfigPath = file.toLowerCase().includes('/config/') ||
|
|
1117
|
+
file.toLowerCase().includes('/schemas/') ||
|
|
1118
|
+
file.toLowerCase().includes('/settings/');
|
|
1119
|
+
|
|
1120
|
+
// Check for schema-like exports (often have table/model definitions)
|
|
1121
|
+
const hasSchemaExports = exports.some(e =>
|
|
1122
|
+
e.name.toLowerCase().includes('table') ||
|
|
1123
|
+
e.name.toLowerCase().includes('schema') ||
|
|
1124
|
+
e.name.toLowerCase().includes('config') ||
|
|
1125
|
+
e.name.toLowerCase().includes('setting')
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
return isConfigName || isConfigPath || hasSchemaExports;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Detect if a file is a utility/helper file
|
|
1133
|
+
*
|
|
1134
|
+
* Characteristics:
|
|
1135
|
+
* - Named with util, helper, or utility patterns
|
|
1136
|
+
* - Often contains mixed helper functions by design
|
|
1137
|
+
* - Multiple domains are acceptable (utility purpose)
|
|
1138
|
+
*/
|
|
1139
|
+
function isUtilityFile(node: DependencyNode): boolean {
|
|
1140
|
+
const { file, exports } = node;
|
|
1141
|
+
|
|
1142
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1143
|
+
|
|
1144
|
+
// Check filename patterns for utility files
|
|
1145
|
+
const utilityPatterns = [
|
|
1146
|
+
'util', 'utility', 'utilities', 'helper', 'helpers',
|
|
1147
|
+
'common', 'shared', 'toolbox', 'toolkit',
|
|
1148
|
+
'.util.', '-util.', '_util.', '-utils.', '.utils.',
|
|
1149
|
+
];
|
|
1150
|
+
|
|
1151
|
+
const isUtilityName = utilityPatterns.some(pattern =>
|
|
1152
|
+
fileName?.includes(pattern)
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
// Check if file is in a utils/helpers directory
|
|
1156
|
+
const isUtilityPath = file.toLowerCase().includes('/utils/') ||
|
|
1157
|
+
file.toLowerCase().includes('/helpers/') ||
|
|
1158
|
+
file.toLowerCase().includes('/common/') ||
|
|
1159
|
+
file.toLowerCase().endsWith('-utils.ts') ||
|
|
1160
|
+
file.toLowerCase().endsWith('-util.ts') ||
|
|
1161
|
+
file.toLowerCase().endsWith('-helper.ts') ||
|
|
1162
|
+
file.toLowerCase().endsWith('-helpers.ts');
|
|
1163
|
+
|
|
1164
|
+
// Only consider many small exports as utility pattern if also in utility-like path
|
|
1165
|
+
// This prevents false positives for regular modules with many functions
|
|
1166
|
+
const hasManySmallExportsInUtilityContext = exports.length >= 3 &&
|
|
1167
|
+
exports.every(e => e.type === 'function' || e.type === 'const') &&
|
|
1168
|
+
(isUtilityName || isUtilityPath);
|
|
1169
|
+
|
|
1170
|
+
return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Detect if a file is a Lambda/API handler
|
|
1175
|
+
*
|
|
1176
|
+
* Characteristics:
|
|
1177
|
+
* - Named with handler patterns or in handler directories
|
|
1178
|
+
* - Single entry point (handler function)
|
|
1179
|
+
* - Coordinates multiple services but has single business purpose
|
|
1180
|
+
*/
|
|
1181
|
+
function isLambdaHandler(node: DependencyNode): boolean {
|
|
1182
|
+
const { file, exports } = node;
|
|
1183
|
+
|
|
1184
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1185
|
+
|
|
1186
|
+
// Check filename patterns for lambda handlers
|
|
1187
|
+
const handlerPatterns = [
|
|
1188
|
+
'handler', '.handler.', '-handler.',
|
|
1189
|
+
'lambda', '.lambda.', '-lambda.',
|
|
1190
|
+
];
|
|
1191
|
+
|
|
1192
|
+
const isHandlerName = handlerPatterns.some(pattern =>
|
|
1193
|
+
fileName?.includes(pattern)
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
// Check if file is in a handlers/lambdas/functions directory
|
|
1197
|
+
// Exclude /api/ unless it has handler-specific naming
|
|
1198
|
+
const isHandlerPath = file.toLowerCase().includes('/handlers/') ||
|
|
1199
|
+
file.toLowerCase().includes('/lambdas/') ||
|
|
1200
|
+
file.toLowerCase().includes('/functions/');
|
|
1201
|
+
|
|
1202
|
+
// Check for typical lambda handler exports (handler, main, etc.)
|
|
1203
|
+
const hasHandlerExport = exports.some(e =>
|
|
1204
|
+
e.name.toLowerCase() === 'handler' ||
|
|
1205
|
+
e.name.toLowerCase() === 'main' ||
|
|
1206
|
+
e.name.toLowerCase() === 'lambdahandler' ||
|
|
1207
|
+
e.name.toLowerCase().endsWith('handler')
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
// Only consider single export as lambda handler if it's in a handler-like context
|
|
1211
|
+
// (either in handler directory OR has handler naming)
|
|
1212
|
+
const hasSingleEntryInHandlerContext = exports.length === 1 &&
|
|
1213
|
+
(exports[0].type === 'function' || exports[0].name === 'default') &&
|
|
1214
|
+
(isHandlerPath || isHandlerName);
|
|
1215
|
+
|
|
1216
|
+
return isHandlerName || isHandlerPath || hasHandlerExport || hasSingleEntryInHandlerContext;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Detect if a file is a service file
|
|
1221
|
+
*
|
|
1222
|
+
* Characteristics:
|
|
1223
|
+
* - Named with service pattern
|
|
1224
|
+
* - Often a class or object with multiple methods
|
|
1225
|
+
* - Orchestrates multiple dependencies but serves single purpose
|
|
1226
|
+
*/
|
|
1227
|
+
function isServiceFile(node: DependencyNode): boolean {
|
|
1228
|
+
const { file, exports } = node;
|
|
1229
|
+
|
|
1230
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1231
|
+
|
|
1232
|
+
// Check filename patterns for service files
|
|
1233
|
+
const servicePatterns = [
|
|
1234
|
+
'service', '.service.', '-service.', '_service.',
|
|
1235
|
+
];
|
|
1236
|
+
|
|
1237
|
+
const isServiceName = servicePatterns.some(pattern =>
|
|
1238
|
+
fileName?.includes(pattern)
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
// Check if file is in a services directory
|
|
1242
|
+
const isServicePath = file.toLowerCase().includes('/services/');
|
|
1243
|
+
|
|
1244
|
+
// Check for service-like exports (class with "Service" in the name)
|
|
1245
|
+
const hasServiceNamedExport = exports.some(e =>
|
|
1246
|
+
e.name.toLowerCase().includes('service') ||
|
|
1247
|
+
e.name.toLowerCase().endsWith('service')
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
// Check for typical service pattern (class export with service in name)
|
|
1251
|
+
const hasClassExport = exports.some(e => e.type === 'class');
|
|
1252
|
+
|
|
1253
|
+
// Service files need either:
|
|
1254
|
+
// 1. Service in filename/path, OR
|
|
1255
|
+
// 2. Class with "Service" in the class name
|
|
1256
|
+
return isServiceName || isServicePath || (hasServiceNamedExport && hasClassExport);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Detect if a file is an email template/layout
|
|
1261
|
+
*
|
|
1262
|
+
* Characteristics:
|
|
1263
|
+
* - Named with email/template patterns
|
|
1264
|
+
* - Contains render/template logic
|
|
1265
|
+
* - References multiple domains (user, order, product) but serves single template purpose
|
|
1266
|
+
*/
|
|
1267
|
+
function isEmailTemplate(node: DependencyNode): boolean {
|
|
1268
|
+
const { file, exports } = node;
|
|
1269
|
+
|
|
1270
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1271
|
+
|
|
1272
|
+
// Check filename patterns for email templates (more specific patterns)
|
|
1273
|
+
const emailTemplatePatterns = [
|
|
1274
|
+
'-email-', '.email.', '_email_',
|
|
1275
|
+
'-template', '.template.', '_template',
|
|
1276
|
+
'-mail.', '.mail.',
|
|
1277
|
+
];
|
|
1278
|
+
|
|
1279
|
+
const isEmailTemplateName = emailTemplatePatterns.some(pattern =>
|
|
1280
|
+
fileName?.includes(pattern)
|
|
1281
|
+
);
|
|
1282
|
+
|
|
1283
|
+
// Specific template file names
|
|
1284
|
+
const isSpecificTemplateName =
|
|
1285
|
+
fileName?.includes('receipt') ||
|
|
1286
|
+
fileName?.includes('invoice-email') ||
|
|
1287
|
+
fileName?.includes('welcome-email') ||
|
|
1288
|
+
fileName?.includes('notification-email') ||
|
|
1289
|
+
fileName?.includes('writer') && fileName.includes('receipt');
|
|
1290
|
+
|
|
1291
|
+
// Check if file is in emails/templates directory (high confidence)
|
|
1292
|
+
const isEmailPath = file.toLowerCase().includes('/emails/') ||
|
|
1293
|
+
file.toLowerCase().includes('/mail/') ||
|
|
1294
|
+
file.toLowerCase().includes('/notifications/');
|
|
1295
|
+
|
|
1296
|
+
// Check for template patterns (function that returns string/HTML)
|
|
1297
|
+
// More specific: must have render/generate in the function name
|
|
1298
|
+
const hasTemplateFunction = exports.some(e =>
|
|
1299
|
+
e.type === 'function' && (
|
|
1300
|
+
e.name.toLowerCase().startsWith('render') ||
|
|
1301
|
+
e.name.toLowerCase().startsWith('generate') ||
|
|
1302
|
+
(e.name.toLowerCase().includes('template') && e.name.toLowerCase().includes('email'))
|
|
1303
|
+
)
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
// Check for email-related exports (but not service classes)
|
|
1307
|
+
const hasEmailExport = exports.some(e =>
|
|
1308
|
+
(e.name.toLowerCase().includes('template') && e.type === 'function') ||
|
|
1309
|
+
(e.name.toLowerCase().includes('render') && e.type === 'function') ||
|
|
1310
|
+
(e.name.toLowerCase().includes('email') && e.type !== 'class')
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
// Require path-based match OR combination of name and export patterns
|
|
1314
|
+
return isEmailPath || isEmailTemplateName || isSpecificTemplateName ||
|
|
1315
|
+
(hasTemplateFunction && hasEmailExport);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Detect if a file is a parser/transformer
|
|
1320
|
+
*
|
|
1321
|
+
* Characteristics:
|
|
1322
|
+
* - Named with parser/transform patterns
|
|
1323
|
+
* - Contains parse/transform logic
|
|
1324
|
+
* - Single transformation purpose despite touching multiple domains
|
|
1325
|
+
*/
|
|
1326
|
+
function isParserFile(node: DependencyNode): boolean {
|
|
1327
|
+
const { file, exports } = node;
|
|
1328
|
+
|
|
1329
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1330
|
+
|
|
1331
|
+
// Check filename patterns for parser files
|
|
1332
|
+
const parserPatterns = [
|
|
1333
|
+
'parser', '.parser.', '-parser.', '_parser.',
|
|
1334
|
+
'transform', '.transform.', '-transform.',
|
|
1335
|
+
'converter', '.converter.', '-converter.',
|
|
1336
|
+
'mapper', '.mapper.', '-mapper.',
|
|
1337
|
+
'serializer', '.serializer.',
|
|
1338
|
+
'deterministic', // For base-parser-deterministic.ts pattern
|
|
1339
|
+
];
|
|
1340
|
+
|
|
1341
|
+
const isParserName = parserPatterns.some(pattern =>
|
|
1342
|
+
fileName?.includes(pattern)
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
// Check if file is in parsers/transformers directory
|
|
1346
|
+
const isParserPath = file.toLowerCase().includes('/parsers/') ||
|
|
1347
|
+
file.toLowerCase().includes('/transformers/') ||
|
|
1348
|
+
file.toLowerCase().includes('/converters/') ||
|
|
1349
|
+
file.toLowerCase().includes('/mappers/');
|
|
1350
|
+
|
|
1351
|
+
// Check for parser-related exports
|
|
1352
|
+
const hasParserExport = exports.some(e =>
|
|
1353
|
+
e.name.toLowerCase().includes('parse') ||
|
|
1354
|
+
e.name.toLowerCase().includes('transform') ||
|
|
1355
|
+
e.name.toLowerCase().includes('convert') ||
|
|
1356
|
+
e.name.toLowerCase().includes('map') ||
|
|
1357
|
+
e.name.toLowerCase().includes('serialize') ||
|
|
1358
|
+
e.name.toLowerCase().includes('deserialize')
|
|
1359
|
+
);
|
|
1360
|
+
|
|
1361
|
+
// Check for function patterns typical of parsers
|
|
1362
|
+
const hasParseFunction = exports.some(e =>
|
|
1363
|
+
e.type === 'function' && (
|
|
1364
|
+
e.name.toLowerCase().startsWith('parse') ||
|
|
1365
|
+
e.name.toLowerCase().startsWith('transform') ||
|
|
1366
|
+
e.name.toLowerCase().startsWith('convert') ||
|
|
1367
|
+
e.name.toLowerCase().startsWith('map') ||
|
|
1368
|
+
e.name.toLowerCase().startsWith('extract')
|
|
1369
|
+
)
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1372
|
+
return isParserName || isParserPath || hasParserExport || hasParseFunction;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Detect if a file is a session/state management file
|
|
1377
|
+
*
|
|
1378
|
+
* Characteristics:
|
|
1379
|
+
* - Named with session/state patterns
|
|
1380
|
+
* - Manages state across operations
|
|
1381
|
+
* - Single purpose despite potentially touching multiple domains
|
|
1382
|
+
*/
|
|
1383
|
+
function isSessionFile(node: DependencyNode): boolean {
|
|
1384
|
+
const { file, exports } = node;
|
|
1385
|
+
|
|
1386
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1387
|
+
|
|
1388
|
+
// Check filename patterns for session files
|
|
1389
|
+
const sessionPatterns = [
|
|
1390
|
+
'session', '.session.', '-session.',
|
|
1391
|
+
'state', '.state.', '-state.',
|
|
1392
|
+
'context', '.context.', '-context.',
|
|
1393
|
+
'store', '.store.', '-store.',
|
|
1394
|
+
];
|
|
1395
|
+
|
|
1396
|
+
const isSessionName = sessionPatterns.some(pattern =>
|
|
1397
|
+
fileName?.includes(pattern)
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
// Check if file is in sessions/state directory
|
|
1401
|
+
const isSessionPath = file.toLowerCase().includes('/sessions/') ||
|
|
1402
|
+
file.toLowerCase().includes('/state/') ||
|
|
1403
|
+
file.toLowerCase().includes('/context/') ||
|
|
1404
|
+
file.toLowerCase().includes('/store/');
|
|
1405
|
+
|
|
1406
|
+
// Check for session-related exports
|
|
1407
|
+
const hasSessionExport = exports.some(e =>
|
|
1408
|
+
e.name.toLowerCase().includes('session') ||
|
|
1409
|
+
e.name.toLowerCase().includes('state') ||
|
|
1410
|
+
e.name.toLowerCase().includes('context') ||
|
|
1411
|
+
e.name.toLowerCase().includes('manager') ||
|
|
1412
|
+
e.name.toLowerCase().includes('store')
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1415
|
+
return isSessionName || isSessionPath || hasSessionExport;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Detect if a file is a Next.js App Router page
|
|
1420
|
+
*
|
|
1421
|
+
* Characteristics:
|
|
1422
|
+
* - Located in /app/ directory (Next.js App Router)
|
|
1423
|
+
* - Named page.tsx or page.ts
|
|
1424
|
+
* - Exports: metadata (SEO), default (page component), and optionally:
|
|
1425
|
+
* - faqJsonLd, jsonLd (structured data)
|
|
1426
|
+
* - icon (for tool cards)
|
|
1427
|
+
* - generateMetadata (dynamic SEO)
|
|
1428
|
+
*
|
|
1429
|
+
* This is the canonical Next.js pattern for SEO-optimized pages.
|
|
1430
|
+
* Multiple exports are COHESIVE - they all serve the page's purpose.
|
|
1431
|
+
*/
|
|
1432
|
+
function isNextJsPage(node: DependencyNode): boolean {
|
|
1433
|
+
const { file, exports } = node;
|
|
1434
|
+
|
|
1435
|
+
const lowerPath = file.toLowerCase();
|
|
1436
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1437
|
+
|
|
1438
|
+
// Must be in /app/ directory (Next.js App Router)
|
|
1439
|
+
const isInAppDir = lowerPath.includes('/app/') || lowerPath.startsWith('app/');
|
|
1440
|
+
|
|
1441
|
+
// Must be named page.tsx or page.ts
|
|
1442
|
+
const isPageFile = fileName === 'page.tsx' || fileName === 'page.ts';
|
|
1443
|
+
|
|
1444
|
+
if (!isInAppDir || !isPageFile) {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// Check for Next.js page export patterns
|
|
1449
|
+
const exportNames = exports.map(e => e.name.toLowerCase());
|
|
1450
|
+
|
|
1451
|
+
// Must have default export (the page component)
|
|
1452
|
+
const hasDefaultExport = exports.some(e => e.type === 'default');
|
|
1453
|
+
|
|
1454
|
+
// Common Next.js page exports
|
|
1455
|
+
const nextJsExports = ['metadata', 'generatemetadata', 'faqjsonld', 'jsonld', 'icon', 'viewport', 'dynamic'];
|
|
1456
|
+
const hasNextJsExports = exportNames.some(name =>
|
|
1457
|
+
nextJsExports.includes(name) || name.includes('jsonld')
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
// A Next.js page typically has:
|
|
1461
|
+
// 1. Default export (page component) - required
|
|
1462
|
+
// 2. Metadata or other Next.js-specific exports - optional but indicative
|
|
1463
|
+
return hasDefaultExport || hasNextJsExports;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* Adjust cohesion score based on file classification.
|
|
1468
|
+
*
|
|
1469
|
+
* This reduces false positives by recognizing that certain file types
|
|
1470
|
+
* have inherently different cohesion patterns:
|
|
1471
|
+
* - Utility modules may touch multiple domains but serve one purpose
|
|
1472
|
+
* - Service files orchestrate multiple dependencies
|
|
1473
|
+
* - Lambda handlers coordinate multiple services
|
|
1474
|
+
* - Email templates reference multiple domains for rendering
|
|
1475
|
+
* - Parser files transform data across domains
|
|
1476
|
+
*
|
|
1477
|
+
* @param baseCohesion - The calculated cohesion score (0-1)
|
|
1478
|
+
* @param classification - The file classification
|
|
1479
|
+
* @param node - Optional node for additional heuristics
|
|
1480
|
+
* @returns Adjusted cohesion score (0-1)
|
|
1481
|
+
*/
|
|
1482
|
+
export function adjustCohesionForClassification(
|
|
1483
|
+
baseCohesion: number,
|
|
1484
|
+
classification: FileClassification,
|
|
1485
|
+
node?: DependencyNode
|
|
1486
|
+
): number {
|
|
1487
|
+
switch (classification) {
|
|
1488
|
+
case 'barrel-export':
|
|
1489
|
+
// Barrel exports re-export from multiple modules by design
|
|
1490
|
+
return 1;
|
|
1491
|
+
case 'type-definition':
|
|
1492
|
+
// Type definitions centralize types - high cohesion by nature
|
|
1493
|
+
return 1;
|
|
1494
|
+
case 'utility-module': {
|
|
1495
|
+
// Utility modules serve a functional purpose despite multi-domain
|
|
1496
|
+
// Check if exports have related naming patterns
|
|
1497
|
+
if (node) {
|
|
1498
|
+
const exportNames = node.exports.map(e => e.name.toLowerCase());
|
|
1499
|
+
const hasRelatedNames = hasRelatedExportNames(exportNames);
|
|
1500
|
+
if (hasRelatedNames) {
|
|
1501
|
+
// Related utility functions = boost cohesion significantly
|
|
1502
|
+
return Math.min(1, baseCohesion + 0.45);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
// Default utility boost
|
|
1506
|
+
return Math.min(1, baseCohesion + 0.35);
|
|
1507
|
+
}
|
|
1508
|
+
case 'service-file': {
|
|
1509
|
+
// Services orchestrate dependencies - this is their purpose
|
|
1510
|
+
// Check for class-based service (methods work together)
|
|
1511
|
+
if (node?.exports.some(e => e.type === 'class')) {
|
|
1512
|
+
return Math.min(1, baseCohesion + 0.40);
|
|
1513
|
+
}
|
|
1514
|
+
// Default service boost
|
|
1515
|
+
return Math.min(1, baseCohesion + 0.30);
|
|
1516
|
+
}
|
|
1517
|
+
case 'lambda-handler': {
|
|
1518
|
+
// Lambda handlers have single business purpose
|
|
1519
|
+
// They coordinate multiple services but are entry points
|
|
1520
|
+
if (node) {
|
|
1521
|
+
// Single entry point = cohesive purpose
|
|
1522
|
+
const hasSingleEntry = node.exports.length === 1 ||
|
|
1523
|
+
node.exports.some(e => e.name.toLowerCase() === 'handler');
|
|
1524
|
+
if (hasSingleEntry) {
|
|
1525
|
+
return Math.min(1, baseCohesion + 0.45);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return Math.min(1, baseCohesion + 0.35);
|
|
1529
|
+
}
|
|
1530
|
+
case 'email-template': {
|
|
1531
|
+
// Email templates render data from multiple domains
|
|
1532
|
+
// This is structural cohesion (template purpose)
|
|
1533
|
+
if (node) {
|
|
1534
|
+
// Check for render/generate functions (template purpose)
|
|
1535
|
+
const hasTemplateFunc = node.exports.some(e =>
|
|
1536
|
+
e.name.toLowerCase().includes('render') ||
|
|
1537
|
+
e.name.toLowerCase().includes('generate') ||
|
|
1538
|
+
e.name.toLowerCase().includes('template')
|
|
1539
|
+
);
|
|
1540
|
+
if (hasTemplateFunc) {
|
|
1541
|
+
return Math.min(1, baseCohesion + 0.40);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return Math.min(1, baseCohesion + 0.30);
|
|
1545
|
+
}
|
|
1546
|
+
case 'parser-file': {
|
|
1547
|
+
// Parsers transform data - single transformation purpose
|
|
1548
|
+
if (node) {
|
|
1549
|
+
// Check for parse/transform functions
|
|
1550
|
+
const hasParseFunc = node.exports.some(e =>
|
|
1551
|
+
e.name.toLowerCase().startsWith('parse') ||
|
|
1552
|
+
e.name.toLowerCase().startsWith('transform') ||
|
|
1553
|
+
e.name.toLowerCase().startsWith('convert')
|
|
1554
|
+
);
|
|
1555
|
+
if (hasParseFunc) {
|
|
1556
|
+
return Math.min(1, baseCohesion + 0.40);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return Math.min(1, baseCohesion + 0.30);
|
|
1560
|
+
}
|
|
1561
|
+
case 'nextjs-page':
|
|
1562
|
+
// Next.js pages have multiple exports by design (metadata, jsonLd, page component)
|
|
1563
|
+
// All serve the single purpose of rendering an SEO-optimized page
|
|
1564
|
+
return 1;
|
|
1565
|
+
case 'cohesive-module':
|
|
1566
|
+
// Already recognized as cohesive
|
|
1567
|
+
return Math.max(baseCohesion, 0.7);
|
|
1568
|
+
case 'mixed-concerns':
|
|
1569
|
+
// Keep original score - this is a real issue
|
|
1570
|
+
return baseCohesion;
|
|
1571
|
+
default:
|
|
1572
|
+
// Unknown - give benefit of doubt with small boost
|
|
1573
|
+
return Math.min(1, baseCohesion + 0.10);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Check if export names suggest related functionality
|
|
1579
|
+
*
|
|
1580
|
+
* Examples of related patterns:
|
|
1581
|
+
* - formatDate, parseDate, validateDate (date utilities)
|
|
1582
|
+
* - getUser, saveUser, deleteUser (user utilities)
|
|
1583
|
+
* - DynamoDB, S3, SQS (AWS utilities)
|
|
1584
|
+
*/
|
|
1585
|
+
function hasRelatedExportNames(exportNames: string[]): boolean {
|
|
1586
|
+
if (exportNames.length < 2) return true;
|
|
1587
|
+
|
|
1588
|
+
// Extract common prefixes/suffixes
|
|
1589
|
+
const stems = new Set<string>();
|
|
1590
|
+
const domains = new Set<string>();
|
|
1591
|
+
|
|
1592
|
+
for (const name of exportNames) {
|
|
1593
|
+
// Check for common verb prefixes
|
|
1594
|
+
const verbs = ['get', 'set', 'create', 'update', 'delete', 'fetch', 'save', 'load', 'parse', 'format', 'validate', 'convert', 'transform', 'build', 'generate', 'render', 'send', 'receive'];
|
|
1595
|
+
for (const verb of verbs) {
|
|
1596
|
+
if (name.startsWith(verb) && name.length > verb.length) {
|
|
1597
|
+
stems.add(name.slice(verb.length).toLowerCase());
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Check for domain suffixes (User, Order, etc.)
|
|
1602
|
+
const domainPatterns = ['user', 'order', 'product', 'session', 'email', 'file', 'db', 's3', 'dynamo', 'api', 'config'];
|
|
1603
|
+
for (const domain of domainPatterns) {
|
|
1604
|
+
if (name.includes(domain)) {
|
|
1605
|
+
domains.add(domain);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// If exports share common stems or domains, they're related
|
|
1611
|
+
if (stems.size === 1 && exportNames.length >= 2) return true;
|
|
1612
|
+
if (domains.size === 1 && exportNames.length >= 2) return true;
|
|
1613
|
+
|
|
1614
|
+
// Check for utilities with same service prefix (e.g., dynamodbGet, dynamodbPut)
|
|
1615
|
+
const prefixes = exportNames.map(name => {
|
|
1616
|
+
// Extract prefix before first capital letter or common separator
|
|
1617
|
+
const match = name.match(/^([a-z]+)/);
|
|
1618
|
+
return match ? match[1] : '';
|
|
1619
|
+
}).filter(p => p.length >= 3);
|
|
1620
|
+
|
|
1621
|
+
if (prefixes.length >= 2) {
|
|
1622
|
+
const uniquePrefixes = new Set(prefixes);
|
|
1623
|
+
if (uniquePrefixes.size === 1) return true;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
return false;
|
|
1024
1627
|
}
|
|
1025
1628
|
|
|
1026
1629
|
/**
|
|
@@ -1030,6 +1633,7 @@ function isTypeDefinitionFile(node: DependencyNode): boolean {
|
|
|
1030
1633
|
* - Ignoring fragmentation for barrel exports (they're meant to aggregate)
|
|
1031
1634
|
* - Ignoring fragmentation for type definitions (centralized types are good)
|
|
1032
1635
|
* - Reducing fragmentation for cohesive modules (large but focused is OK)
|
|
1636
|
+
* - Reducing fragmentation for utility/service/handler/template files
|
|
1033
1637
|
*/
|
|
1034
1638
|
export function adjustFragmentationForClassification(
|
|
1035
1639
|
baseFragmentation: number,
|
|
@@ -1042,6 +1646,15 @@ export function adjustFragmentationForClassification(
|
|
|
1042
1646
|
case 'type-definition':
|
|
1043
1647
|
// Centralized type definitions are good practice - no fragmentation
|
|
1044
1648
|
return 0;
|
|
1649
|
+
case 'utility-module':
|
|
1650
|
+
case 'service-file':
|
|
1651
|
+
case 'lambda-handler':
|
|
1652
|
+
case 'email-template':
|
|
1653
|
+
case 'parser-file':
|
|
1654
|
+
case 'nextjs-page':
|
|
1655
|
+
// These file types have structural reasons for touching multiple domains
|
|
1656
|
+
// Reduce fragmentation significantly
|
|
1657
|
+
return baseFragmentation * 0.2;
|
|
1045
1658
|
case 'cohesive-module':
|
|
1046
1659
|
// Cohesive modules get a significant discount
|
|
1047
1660
|
return baseFragmentation * 0.3;
|
|
@@ -1078,6 +1691,36 @@ export function getClassificationRecommendations(
|
|
|
1078
1691
|
'Module has good cohesion despite its size',
|
|
1079
1692
|
'Consider documenting the module boundaries for AI assistants',
|
|
1080
1693
|
];
|
|
1694
|
+
case 'utility-module':
|
|
1695
|
+
return [
|
|
1696
|
+
'Utility module detected - multiple domains are acceptable here',
|
|
1697
|
+
'Consider grouping related utilities by prefix or domain for better discoverability',
|
|
1698
|
+
];
|
|
1699
|
+
case 'service-file':
|
|
1700
|
+
return [
|
|
1701
|
+
'Service file detected - orchestration of multiple dependencies is expected',
|
|
1702
|
+
'Consider documenting service boundaries and dependencies',
|
|
1703
|
+
];
|
|
1704
|
+
case 'lambda-handler':
|
|
1705
|
+
return [
|
|
1706
|
+
'Lambda handler detected - coordination of services is expected',
|
|
1707
|
+
'Ensure handler has clear single responsibility',
|
|
1708
|
+
];
|
|
1709
|
+
case 'email-template':
|
|
1710
|
+
return [
|
|
1711
|
+
'Email template detected - references multiple domains for rendering',
|
|
1712
|
+
'Template structure is cohesive by design',
|
|
1713
|
+
];
|
|
1714
|
+
case 'parser-file':
|
|
1715
|
+
return [
|
|
1716
|
+
'Parser/transformer file detected - handles multiple data sources',
|
|
1717
|
+
'Consider documenting input/output schemas',
|
|
1718
|
+
];
|
|
1719
|
+
case 'nextjs-page':
|
|
1720
|
+
return [
|
|
1721
|
+
'Next.js App Router page detected - metadata/JSON-LD/component pattern is cohesive',
|
|
1722
|
+
'Multiple exports (metadata, faqJsonLd, default) serve single page purpose',
|
|
1723
|
+
];
|
|
1081
1724
|
case 'mixed-concerns':
|
|
1082
1725
|
return [
|
|
1083
1726
|
'Consider splitting this file by domain',
|