@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/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 cohesive module (single domain + high cohesion)
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
- if (hasSingleDomain && hasHighCohesion) {
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
- // 4. Check for mixed concerns (multiple domains + low cohesion)
988
+ // 11. Check for mixed concerns (multiple domains + low cohesion)
944
989
  const hasMultipleDomains = uniqueDomains.length > 1;
945
- const hasLowCohesion = cohesionScore < 0.5;
990
+ const hasLowCohesion = cohesionScore < 0.4; // Lowered threshold
946
991
 
947
- if (hasMultipleDomains || hasLowCohesion) {
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
- return isTypesFile || mostlyTypes;
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',