@aiready/context-analyzer 0.9.41 → 0.9.42

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.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/.turbo/turbo-test.log +19 -19
  3. package/dist/chunk-4SYIJ7CU.mjs +1538 -0
  4. package/dist/chunk-4XQVYYPC.mjs +1470 -0
  5. package/dist/chunk-5CLU3HYU.mjs +1475 -0
  6. package/dist/chunk-5K73Q3OQ.mjs +1520 -0
  7. package/dist/chunk-6AVS4KTM.mjs +1536 -0
  8. package/dist/chunk-6I4552YB.mjs +1467 -0
  9. package/dist/chunk-6LPITDKG.mjs +1539 -0
  10. package/dist/chunk-AECWO7NQ.mjs +1539 -0
  11. package/dist/chunk-AJC3FR6G.mjs +1509 -0
  12. package/dist/chunk-CVGIDSMN.mjs +1522 -0
  13. package/dist/chunk-DXG5NIYL.mjs +1527 -0
  14. package/dist/chunk-G3CCJCBI.mjs +1521 -0
  15. package/dist/chunk-GFADGYXZ.mjs +1752 -0
  16. package/dist/chunk-GTRIBVS6.mjs +1467 -0
  17. package/dist/chunk-H4HWBQU6.mjs +1530 -0
  18. package/dist/chunk-JH535NPP.mjs +1619 -0
  19. package/dist/chunk-KGFWKSGJ.mjs +1442 -0
  20. package/dist/chunk-N2GQWNFG.mjs +1527 -0
  21. package/dist/chunk-NQA3F2HJ.mjs +1532 -0
  22. package/dist/chunk-NXXQ2U73.mjs +1467 -0
  23. package/dist/chunk-QDGPR3L6.mjs +1518 -0
  24. package/dist/chunk-SAVOSPM3.mjs +1522 -0
  25. package/dist/chunk-SIX4KMF2.mjs +1468 -0
  26. package/dist/chunk-SPAM2YJE.mjs +1537 -0
  27. package/dist/chunk-UG7OPVHB.mjs +1521 -0
  28. package/dist/chunk-VIJTZPBI.mjs +1470 -0
  29. package/dist/chunk-W37E7MW5.mjs +1403 -0
  30. package/dist/chunk-W76FEISE.mjs +1538 -0
  31. package/dist/chunk-WCFQYXQA.mjs +1532 -0
  32. package/dist/chunk-XY77XABG.mjs +1545 -0
  33. package/dist/chunk-YCGDIGOG.mjs +1467 -0
  34. package/dist/cli.js +768 -1160
  35. package/dist/cli.mjs +1 -1
  36. package/dist/index.d.mts +196 -64
  37. package/dist/index.d.ts +196 -64
  38. package/dist/index.js +937 -1209
  39. package/dist/index.mjs +65 -3
  40. package/package.json +2 -2
  41. package/src/analyzer.ts +143 -2177
  42. package/src/ast-utils.ts +94 -0
  43. package/src/classifier.ts +497 -0
  44. package/src/cluster-detector.ts +100 -0
  45. package/src/defaults.ts +59 -0
  46. package/src/graph-builder.ts +272 -0
  47. package/src/index.ts +30 -519
  48. package/src/metrics.ts +231 -0
  49. package/src/remediation.ts +139 -0
  50. package/src/scoring.ts +12 -34
  51. package/src/semantic-analysis.ts +192 -126
  52. package/src/summary.ts +168 -0
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ __export(python_context_exports, {
37
37
  });
38
38
  async function analyzePythonContext(files, rootDir) {
39
39
  const results = [];
40
- const parser = (0, import_core3.getParser)("dummy.py");
40
+ const parser = (0, import_core6.getParser)("dummy.py");
41
41
  if (!parser) {
42
42
  console.warn("Python parser not available");
43
43
  return results;
@@ -101,7 +101,7 @@ async function analyzePythonContext(files, rootDir) {
101
101
  }
102
102
  async function buildPythonDependencyGraph(files, rootDir) {
103
103
  const graph = /* @__PURE__ */ new Map();
104
- const parser = (0, import_core3.getParser)("dummy.py");
104
+ const parser = (0, import_core6.getParser)("dummy.py");
105
105
  if (!parser) return graph;
106
106
  for (const file of files) {
107
107
  try {
@@ -181,7 +181,7 @@ async function calculatePythonImportDepth(file, dependencyGraph, visited, depth
181
181
  }
182
182
  function estimateContextBudget(code, imports, dependencyGraph) {
183
183
  void dependencyGraph;
184
- let budget = (0, import_core3.estimateTokens)(code);
184
+ let budget = (0, import_core6.estimateTokens)(code);
185
185
  const avgTokensPerDep = 500;
186
186
  budget += imports.length * avgTokensPerDep;
187
187
  return budget;
@@ -231,11 +231,11 @@ function detectCircularDependencies2(file, dependencyGraph) {
231
231
  dfs(file, []);
232
232
  return [...new Set(circular)];
233
233
  }
234
- var import_core3, import_path, import_fs;
234
+ var import_core6, import_path, import_fs;
235
235
  var init_python_context = __esm({
236
236
  "src/analyzers/python-context.ts"() {
237
237
  "use strict";
238
- import_core3 = require("@aiready/core");
238
+ import_core6 = require("@aiready/core");
239
239
  import_path = require("path");
240
240
  import_fs = __toESM(require("fs"));
241
241
  }
@@ -244,44 +244,73 @@ var init_python_context = __esm({
244
244
  // src/index.ts
245
245
  var index_exports = {};
246
246
  __export(index_exports, {
247
+ adjustCohesionForClassification: () => adjustCohesionForClassification,
247
248
  adjustFragmentationForClassification: () => adjustFragmentationForClassification,
248
249
  analyzeContext: () => analyzeContext,
250
+ analyzeIssues: () => analyzeIssues,
249
251
  buildCoUsageMatrix: () => buildCoUsageMatrix,
252
+ buildDependencyGraph: () => buildDependencyGraph,
250
253
  buildTypeGraph: () => buildTypeGraph,
254
+ calculateCohesion: () => calculateCohesion,
255
+ calculateContextBudget: () => calculateContextBudget,
251
256
  calculateContextScore: () => calculateContextScore,
257
+ calculateDirectoryDistance: () => calculateDirectoryDistance,
252
258
  calculateDomainConfidence: () => calculateDomainConfidence,
259
+ calculateEnhancedCohesion: () => calculateEnhancedCohesion,
260
+ calculateFragmentation: () => calculateFragmentation,
261
+ calculateImportDepth: () => calculateImportDepth,
262
+ calculatePathEntropy: () => calculatePathEntropy,
263
+ calculateStructuralCohesionFromCoUsage: () => calculateStructuralCohesionFromCoUsage,
253
264
  classifyFile: () => classifyFile,
265
+ detectCircularDependencies: () => detectCircularDependencies,
266
+ detectModuleClusters: () => detectModuleClusters,
267
+ extractDomainKeywordsFromPaths: () => extractDomainKeywordsFromPaths,
268
+ extractExports: () => extractExports,
269
+ extractImportsFromContent: () => extractImportsFromContent,
254
270
  findConsolidationCandidates: () => findConsolidationCandidates,
255
271
  findSemanticClusters: () => findSemanticClusters,
256
272
  generateSummary: () => generateSummary,
273
+ getClassificationRecommendations: () => getClassificationRecommendations,
257
274
  getCoUsageData: () => getCoUsageData,
275
+ getGeneralRecommendations: () => getGeneralRecommendations,
258
276
  getSmartDefaults: () => getSmartDefaults,
259
- inferDomainFromSemantics: () => inferDomainFromSemantics
277
+ getTransitiveDependencies: () => getTransitiveDependencies,
278
+ inferDomain: () => inferDomain,
279
+ inferDomainFromSemantics: () => inferDomainFromSemantics,
280
+ isBarrelExport: () => isBarrelExport,
281
+ isConfigFile: () => isConfigFile,
282
+ isEmailTemplate: () => isEmailTemplate,
283
+ isLambdaHandler: () => isLambdaHandler,
284
+ isNextJsPage: () => isNextJsPage,
285
+ isParserFile: () => isParserFile,
286
+ isServiceFile: () => isServiceFile,
287
+ isSessionFile: () => isSessionFile,
288
+ isTypeDefinition: () => isTypeDefinition,
289
+ isUtilityModule: () => isUtilityModule,
290
+ mapScoreToRating: () => mapScoreToRating
260
291
  });
261
292
  module.exports = __toCommonJS(index_exports);
262
- var import_core4 = require("@aiready/core");
293
+ var import_core7 = require("@aiready/core");
263
294
 
264
- // src/analyzer.ts
295
+ // src/metrics.ts
296
+ var import_core2 = require("@aiready/core");
297
+
298
+ // src/ast-utils.ts
265
299
  var import_core = require("@aiready/core");
266
300
 
267
301
  // src/semantic-analysis.ts
268
302
  function buildCoUsageMatrix(graph) {
269
303
  const coUsageMatrix = /* @__PURE__ */ new Map();
270
- for (const [sourceFile, node] of graph.nodes) {
271
- void sourceFile;
304
+ for (const [, node] of graph.nodes) {
272
305
  const imports = node.imports;
273
306
  for (let i = 0; i < imports.length; i++) {
274
307
  const fileA = imports[i];
275
- if (!coUsageMatrix.has(fileA)) {
276
- coUsageMatrix.set(fileA, /* @__PURE__ */ new Map());
277
- }
308
+ if (!coUsageMatrix.has(fileA)) coUsageMatrix.set(fileA, /* @__PURE__ */ new Map());
278
309
  for (let j = i + 1; j < imports.length; j++) {
279
310
  const fileB = imports[j];
280
311
  const fileAUsage = coUsageMatrix.get(fileA);
281
312
  fileAUsage.set(fileB, (fileAUsage.get(fileB) || 0) + 1);
282
- if (!coUsageMatrix.has(fileB)) {
283
- coUsageMatrix.set(fileB, /* @__PURE__ */ new Map());
284
- }
313
+ if (!coUsageMatrix.has(fileB)) coUsageMatrix.set(fileB, /* @__PURE__ */ new Map());
285
314
  const fileBUsage = coUsageMatrix.get(fileB);
286
315
  fileBUsage.set(fileA, (fileBUsage.get(fileA) || 0) + 1);
287
316
  }
@@ -295,9 +324,7 @@ function buildTypeGraph(graph) {
295
324
  for (const exp of node.exports) {
296
325
  if (exp.typeReferences) {
297
326
  for (const typeRef of exp.typeReferences) {
298
- if (!typeGraph.has(typeRef)) {
299
- typeGraph.set(typeRef, /* @__PURE__ */ new Set());
300
- }
327
+ if (!typeGraph.has(typeRef)) typeGraph.set(typeRef, /* @__PURE__ */ new Set());
301
328
  typeGraph.get(typeRef).add(file);
302
329
  }
303
330
  }
@@ -318,35 +345,11 @@ function findSemanticClusters(coUsageMatrix, minCoUsage = 3) {
318
345
  visited.add(relatedFile);
319
346
  }
320
347
  }
321
- if (cluster.length > 1) {
322
- clusters.set(file, cluster);
323
- }
348
+ if (cluster.length > 1) clusters.set(file, cluster);
324
349
  }
325
350
  return clusters;
326
351
  }
327
- function calculateDomainConfidence(signals) {
328
- const weights = {
329
- coUsage: 0.35,
330
- // Strongest signal: actual usage patterns
331
- typeReference: 0.3,
332
- // Strong signal: shared types
333
- exportName: 0.15,
334
- // Medium signal: identifier semantics
335
- importPath: 0.1,
336
- // Weaker signal: path structure
337
- folderStructure: 0.1
338
- // Weakest signal: organization convention
339
- };
340
- let confidence = 0;
341
- if (signals.coUsage) confidence += weights.coUsage;
342
- if (signals.typeReference) confidence += weights.typeReference;
343
- if (signals.exportName) confidence += weights.exportName;
344
- if (signals.importPath) confidence += weights.importPath;
345
- if (signals.folderStructure) confidence += weights.folderStructure;
346
- return confidence;
347
- }
348
352
  function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGraph, exportTypeRefs) {
349
- const assignments = [];
350
353
  const domainSignals = /* @__PURE__ */ new Map();
351
354
  const coUsages = coUsageMatrix.get(file) || /* @__PURE__ */ new Map();
352
355
  const strongCoUsages = Array.from(coUsages.entries()).filter(([, count]) => count >= 3).map(([coFile]) => coFile);
@@ -375,23 +378,22 @@ function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGr
375
378
  const filesWithType = typeGraph.get(typeRef);
376
379
  if (filesWithType) {
377
380
  for (const typeFile of filesWithType) {
378
- if (typeFile !== file) {
379
- const typeNode = graph.nodes.get(typeFile);
380
- if (typeNode) {
381
- for (const exp of typeNode.exports) {
382
- if (exp.inferredDomain && exp.inferredDomain !== "unknown") {
383
- const domain = exp.inferredDomain;
384
- if (!domainSignals.has(domain)) {
385
- domainSignals.set(domain, {
386
- coUsage: false,
387
- typeReference: false,
388
- exportName: false,
389
- importPath: false,
390
- folderStructure: false
391
- });
392
- }
393
- domainSignals.get(domain).typeReference = true;
381
+ if (typeFile === file) continue;
382
+ const typeNode = graph.nodes.get(typeFile);
383
+ if (typeNode) {
384
+ for (const exp of typeNode.exports) {
385
+ if (exp.inferredDomain && exp.inferredDomain !== "unknown") {
386
+ const domain = exp.inferredDomain;
387
+ if (!domainSignals.has(domain)) {
388
+ domainSignals.set(domain, {
389
+ coUsage: false,
390
+ typeReference: false,
391
+ exportName: false,
392
+ importPath: false,
393
+ folderStructure: false
394
+ });
394
395
  }
396
+ domainSignals.get(domain).typeReference = true;
395
397
  }
396
398
  }
397
399
  }
@@ -399,22 +401,140 @@ function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGr
399
401
  }
400
402
  }
401
403
  }
404
+ const assignments = [];
402
405
  for (const [domain, signals] of domainSignals) {
403
406
  const confidence = calculateDomainConfidence(signals);
404
- if (confidence >= 0.3) {
405
- assignments.push({ domain, confidence, signals });
406
- }
407
+ if (confidence >= 0.3) assignments.push({ domain, confidence, signals });
407
408
  }
408
409
  assignments.sort((a, b) => b.confidence - a.confidence);
409
410
  return assignments;
410
411
  }
412
+ function calculateDomainConfidence(signals) {
413
+ const weights = {
414
+ coUsage: 0.35,
415
+ typeReference: 0.3,
416
+ exportName: 0.15,
417
+ importPath: 0.1,
418
+ folderStructure: 0.1
419
+ };
420
+ let confidence = 0;
421
+ if (signals.coUsage) confidence += weights.coUsage;
422
+ if (signals.typeReference) confidence += weights.typeReference;
423
+ if (signals.exportName) confidence += weights.exportName;
424
+ if (signals.importPath) confidence += weights.importPath;
425
+ if (signals.folderStructure) confidence += weights.folderStructure;
426
+ return confidence;
427
+ }
428
+ function extractExports(content, filePath, domainOptions, fileImports) {
429
+ const exports2 = [];
430
+ const patterns = [
431
+ /export\s+function\s+(\w+)/g,
432
+ /export\s+class\s+(\w+)/g,
433
+ /export\s+const\s+(\w+)/g,
434
+ /export\s+type\s+(\w+)/g,
435
+ /export\s+interface\s+(\w+)/g,
436
+ /export\s+default/g
437
+ ];
438
+ const types = [
439
+ "function",
440
+ "class",
441
+ "const",
442
+ "type",
443
+ "interface",
444
+ "default"
445
+ ];
446
+ patterns.forEach((pattern, index) => {
447
+ let match;
448
+ while ((match = pattern.exec(content)) !== null) {
449
+ const name = match[1] || "default";
450
+ const type = types[index];
451
+ const inferredDomain = inferDomain(
452
+ name,
453
+ filePath,
454
+ domainOptions,
455
+ fileImports
456
+ );
457
+ exports2.push({ name, type, inferredDomain });
458
+ }
459
+ });
460
+ return exports2;
461
+ }
462
+ function inferDomain(name, filePath, domainOptions, fileImports) {
463
+ const lower = name.toLowerCase();
464
+ const tokens = Array.from(
465
+ new Set(
466
+ lower.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-z0-9]+/gi, " ").split(" ").filter(Boolean)
467
+ )
468
+ );
469
+ const defaultKeywords = [
470
+ "authentication",
471
+ "authorization",
472
+ "payment",
473
+ "invoice",
474
+ "customer",
475
+ "product",
476
+ "order",
477
+ "cart",
478
+ "user",
479
+ "admin",
480
+ "repository",
481
+ "controller",
482
+ "service",
483
+ "config",
484
+ "model",
485
+ "view",
486
+ "auth"
487
+ ];
488
+ const domainKeywords = domainOptions?.domainKeywords?.length ? [...domainOptions.domainKeywords, ...defaultKeywords] : defaultKeywords;
489
+ for (const keyword of domainKeywords) {
490
+ if (tokens.includes(keyword)) return keyword;
491
+ }
492
+ for (const keyword of domainKeywords) {
493
+ if (lower.includes(keyword)) return keyword;
494
+ }
495
+ if (fileImports) {
496
+ for (const importPath of fileImports) {
497
+ const segments = importPath.split("/");
498
+ for (const segment of segments) {
499
+ const segLower = segment.toLowerCase();
500
+ const singularSegment = singularize(segLower);
501
+ for (const keyword of domainKeywords) {
502
+ if (singularSegment === keyword || segLower === keyword || segLower.includes(keyword))
503
+ return keyword;
504
+ }
505
+ }
506
+ }
507
+ }
508
+ if (filePath) {
509
+ const segments = filePath.split("/");
510
+ for (const segment of segments) {
511
+ const segLower = segment.toLowerCase();
512
+ const singularSegment = singularize(segLower);
513
+ for (const keyword of domainKeywords) {
514
+ if (singularSegment === keyword || segLower === keyword) return keyword;
515
+ }
516
+ }
517
+ }
518
+ return "unknown";
519
+ }
520
+ function singularize(word) {
521
+ const irregulars = {
522
+ people: "person",
523
+ children: "child",
524
+ men: "man",
525
+ women: "woman"
526
+ };
527
+ if (irregulars[word]) return irregulars[word];
528
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
529
+ if (word.endsWith("ses")) return word.slice(0, -2);
530
+ if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
531
+ return word;
532
+ }
411
533
  function getCoUsageData(file, coUsageMatrix) {
412
- const coImportedWith = coUsageMatrix.get(file) || /* @__PURE__ */ new Map();
413
- const sharedImporters = [];
414
534
  return {
415
535
  file,
416
- coImportedWith,
417
- sharedImporters
536
+ coImportedWith: coUsageMatrix.get(file) || /* @__PURE__ */ new Map(),
537
+ sharedImporters: []
418
538
  };
419
539
  }
420
540
  function findConsolidationCandidates(graph, coUsageMatrix, typeGraph, minCoUsage = 5, minSharedTypes = 2) {
@@ -422,9 +542,8 @@ function findConsolidationCandidates(graph, coUsageMatrix, typeGraph, minCoUsage
422
542
  for (const [fileA, coUsages] of coUsageMatrix) {
423
543
  const nodeA = graph.nodes.get(fileA);
424
544
  if (!nodeA) continue;
425
- for (const [fileB, coUsageCount] of coUsages) {
426
- if (fileB <= fileA) continue;
427
- if (coUsageCount < minCoUsage) continue;
545
+ for (const [fileB, count] of coUsages) {
546
+ if (fileB <= fileA || count < minCoUsage) continue;
428
547
  const nodeB = graph.nodes.get(fileB);
429
548
  if (!nodeB) continue;
430
549
  const typesA = new Set(
@@ -434,28 +553,190 @@ function findConsolidationCandidates(graph, coUsageMatrix, typeGraph, minCoUsage
434
553
  nodeB.exports.flatMap((e) => e.typeReferences || [])
435
554
  );
436
555
  const sharedTypes = Array.from(typesA).filter((t) => typesB.has(t));
437
- if (sharedTypes.length >= minSharedTypes) {
438
- const strength = coUsageCount / 10 + sharedTypes.length / 5;
439
- candidates.push({
440
- files: [fileA, fileB],
441
- reason: `High co-usage (${coUsageCount}x) and ${sharedTypes.length} shared types`,
442
- strength
443
- });
444
- } else if (coUsageCount >= minCoUsage * 2) {
445
- const strength = coUsageCount / 10;
556
+ if (sharedTypes.length >= minSharedTypes || count >= minCoUsage * 2) {
446
557
  candidates.push({
447
558
  files: [fileA, fileB],
448
- reason: `Very high co-usage (${coUsageCount}x)`,
449
- strength
559
+ reason: `High co-usage (${count}x)`,
560
+ strength: count / 10
450
561
  });
451
562
  }
452
563
  }
453
564
  }
454
- candidates.sort((a, b) => b.strength - a.strength);
455
- return candidates;
565
+ return candidates.sort((a, b) => b.strength - a.strength);
456
566
  }
457
567
 
458
- // src/analyzer.ts
568
+ // src/ast-utils.ts
569
+ function extractExportsWithAST(content, filePath, domainOptions, fileImports) {
570
+ try {
571
+ const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
572
+ return astExports.map((exp) => ({
573
+ name: exp.name,
574
+ type: exp.type,
575
+ inferredDomain: inferDomain(
576
+ exp.name,
577
+ filePath,
578
+ domainOptions,
579
+ fileImports
580
+ ),
581
+ imports: exp.imports,
582
+ dependencies: exp.dependencies,
583
+ typeReferences: exp.typeReferences
584
+ }));
585
+ } catch (error) {
586
+ void error;
587
+ return extractExports(content, filePath, domainOptions, fileImports);
588
+ }
589
+ }
590
+ function isTestFile(filePath) {
591
+ const lower = filePath.toLowerCase();
592
+ return lower.includes(".test.") || lower.includes(".spec.") || lower.includes("/__tests__/") || lower.includes("/tests/") || lower.includes("/test/") || lower.includes("test-") || lower.includes("-test") || lower.includes("/__mocks__/") || lower.includes("/mocks/") || lower.includes("/fixtures/") || lower.includes(".mock.") || lower.includes(".fixture.") || lower.includes("/test-utils/");
593
+ }
594
+
595
+ // src/metrics.ts
596
+ function calculateEnhancedCohesion(exports2, filePath, options) {
597
+ if (exports2.length <= 1) return 1;
598
+ if (filePath && isTestFile(filePath)) return 1;
599
+ const domains = exports2.map((e) => e.inferredDomain || "unknown");
600
+ const domainCounts = /* @__PURE__ */ new Map();
601
+ for (const d of domains) domainCounts.set(d, (domainCounts.get(d) || 0) + 1);
602
+ if (domainCounts.size === 1 && domains[0] !== "unknown") {
603
+ if (!options?.weights) return 1;
604
+ }
605
+ const probs = Array.from(domainCounts.values()).map(
606
+ (c) => c / exports2.length
607
+ );
608
+ let domainEntropy = 0;
609
+ for (const p of probs) {
610
+ if (p > 0) domainEntropy -= p * Math.log2(p);
611
+ }
612
+ const maxEntropy = Math.log2(Math.max(2, domainCounts.size));
613
+ const domainScore = 1 - domainEntropy / maxEntropy;
614
+ let importScoreTotal = 0;
615
+ let pairsWithData = 0;
616
+ let anyImportData = false;
617
+ for (let i = 0; i < exports2.length; i++) {
618
+ for (let j = i + 1; j < exports2.length; j++) {
619
+ const exp1Imports = exports2[i].imports;
620
+ const exp2Imports = exports2[j].imports;
621
+ if (exp1Imports || exp2Imports) {
622
+ anyImportData = true;
623
+ const sim = (0, import_core2.calculateImportSimilarity)(
624
+ { ...exports2[i], imports: exp1Imports || [] },
625
+ { ...exports2[j], imports: exp2Imports || [] }
626
+ );
627
+ importScoreTotal += sim;
628
+ pairsWithData++;
629
+ }
630
+ }
631
+ }
632
+ const avgImportScore = pairsWithData > 0 ? importScoreTotal / pairsWithData : 0;
633
+ let score = 0;
634
+ if (anyImportData) {
635
+ score = domainScore * 0.4 + avgImportScore * 0.6;
636
+ if (score === 0 && domainScore === 0) score = 0.1;
637
+ } else {
638
+ score = domainScore;
639
+ }
640
+ let structuralScore = 0;
641
+ for (const exp of exports2) {
642
+ if (exp.dependencies && exp.dependencies.length > 0) {
643
+ structuralScore += 1;
644
+ }
645
+ }
646
+ if (structuralScore > 0) {
647
+ score = Math.min(1, score + 0.1);
648
+ }
649
+ if (!options?.weights && !anyImportData && domainCounts.size === 1) return 1;
650
+ return score;
651
+ }
652
+ function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
653
+ if (!coUsageMatrix) return 1;
654
+ const coUsages = coUsageMatrix.get(file);
655
+ if (!coUsages || coUsages.size === 0) return 1;
656
+ let total = 0;
657
+ for (const count of coUsages.values()) total += count;
658
+ if (total === 0) return 1;
659
+ const probs = [];
660
+ for (const count of coUsages.values()) {
661
+ if (count > 0) probs.push(count / total);
662
+ }
663
+ if (probs.length <= 1) return 1;
664
+ let entropy = 0;
665
+ for (const prob of probs) {
666
+ entropy -= prob * Math.log2(prob);
667
+ }
668
+ const maxEntropy = Math.log2(probs.length);
669
+ return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
670
+ }
671
+ function calculateFragmentation(files, domain, options) {
672
+ if (files.length <= 1) return 0;
673
+ const directories = new Set(
674
+ files.map((f) => f.split("/").slice(0, -1).join("/"))
675
+ );
676
+ const uniqueDirs = directories.size;
677
+ let score = 0;
678
+ if (options?.useLogScale) {
679
+ if (uniqueDirs <= 1) score = 0;
680
+ else {
681
+ const total = files.length;
682
+ const base = options.logBase || Math.E;
683
+ const num = Math.log(uniqueDirs) / Math.log(base);
684
+ const den = Math.log(total) / Math.log(base);
685
+ score = den > 0 ? num / den : 0;
686
+ }
687
+ } else {
688
+ score = (uniqueDirs - 1) / (files.length - 1);
689
+ }
690
+ if (options?.sharedImportRatio && options.sharedImportRatio > 0.5) {
691
+ const discount = (options.sharedImportRatio - 0.5) * 0.4;
692
+ score = score * (1 - discount);
693
+ }
694
+ return score;
695
+ }
696
+ function calculatePathEntropy(files) {
697
+ if (!files || files.length === 0) return 0;
698
+ const dirCounts = /* @__PURE__ */ new Map();
699
+ for (const f of files) {
700
+ const dir = f.split("/").slice(0, -1).join("/") || ".";
701
+ dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
702
+ }
703
+ const counts = Array.from(dirCounts.values());
704
+ if (counts.length <= 1) return 0;
705
+ const total = counts.reduce((s, v) => s + v, 0);
706
+ let entropy = 0;
707
+ for (const count of counts) {
708
+ const prob = count / total;
709
+ entropy -= prob * Math.log2(prob);
710
+ }
711
+ const maxEntropy = Math.log2(counts.length);
712
+ return maxEntropy > 0 ? entropy / maxEntropy : 0;
713
+ }
714
+ function calculateDirectoryDistance(files) {
715
+ if (!files || files.length <= 1) return 0;
716
+ const pathSegments = (p) => p.split("/").filter(Boolean);
717
+ const commonAncestorDepth = (a, b) => {
718
+ const minLen = Math.min(a.length, b.length);
719
+ let i = 0;
720
+ while (i < minLen && a[i] === b[i]) i++;
721
+ return i;
722
+ };
723
+ let totalNormalized = 0;
724
+ let comparisons = 0;
725
+ for (let i = 0; i < files.length; i++) {
726
+ for (let j = i + 1; j < files.length; j++) {
727
+ const segA = pathSegments(files[i]);
728
+ const segB = pathSegments(files[j]);
729
+ const shared = commonAncestorDepth(segA, segB);
730
+ const maxDepth = Math.max(segA.length, segB.length);
731
+ totalNormalized += 1 - (maxDepth > 0 ? shared / maxDepth : 0);
732
+ comparisons++;
733
+ }
734
+ }
735
+ return comparisons > 0 ? totalNormalized / comparisons : 0;
736
+ }
737
+
738
+ // src/graph-builder.ts
739
+ var import_core3 = require("@aiready/core");
459
740
  function extractDomainKeywordsFromPaths(files) {
460
741
  const folderNames = /* @__PURE__ */ new Set();
461
742
  for (const { file } of files) {
@@ -483,39 +764,29 @@ function extractDomainKeywordsFromPaths(files) {
483
764
  for (const segment of segments) {
484
765
  const normalized = segment.toLowerCase();
485
766
  if (normalized && !skipFolders.has(normalized) && !normalized.includes(".")) {
486
- const singular = singularize(normalized);
487
- folderNames.add(singular);
767
+ folderNames.add(singularize2(normalized));
488
768
  }
489
769
  }
490
770
  }
491
771
  return Array.from(folderNames);
492
772
  }
493
- function singularize(word) {
773
+ function singularize2(word) {
494
774
  const irregulars = {
495
775
  people: "person",
496
776
  children: "child",
497
777
  men: "man",
498
778
  women: "woman"
499
779
  };
500
- if (irregulars[word]) {
501
- return irregulars[word];
502
- }
503
- if (word.endsWith("ies")) {
504
- return word.slice(0, -3) + "y";
505
- }
506
- if (word.endsWith("ses")) {
507
- return word.slice(0, -2);
508
- }
509
- if (word.endsWith("s") && word.length > 3) {
510
- return word.slice(0, -1);
511
- }
780
+ if (irregulars[word]) return irregulars[word];
781
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
782
+ if (word.endsWith("ses")) return word.slice(0, -2);
783
+ if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
512
784
  return word;
513
785
  }
514
786
  function buildDependencyGraph(files, options) {
515
787
  const nodes = /* @__PURE__ */ new Map();
516
788
  const edges = /* @__PURE__ */ new Map();
517
789
  const autoDetectedKeywords = options?.domainKeywords ?? extractDomainKeywordsFromPaths(files);
518
- void import_core.calculateImportSimilarity;
519
790
  for (const { file, content } of files) {
520
791
  const imports = extractImportsFromContent(content);
521
792
  const exports2 = extractExportsWithAST(
@@ -524,15 +795,9 @@ function buildDependencyGraph(files, options) {
524
795
  { domainKeywords: autoDetectedKeywords },
525
796
  imports
526
797
  );
527
- const tokenCost = (0, import_core.estimateTokens)(content);
798
+ const tokenCost = (0, import_core3.estimateTokens)(content);
528
799
  const linesOfCode = content.split("\n").length;
529
- nodes.set(file, {
530
- file,
531
- imports,
532
- exports: exports2,
533
- tokenCost,
534
- linesOfCode
535
- });
800
+ nodes.set(file, { file, imports, exports: exports2, tokenCost, linesOfCode });
536
801
  edges.set(file, new Set(imports));
537
802
  }
538
803
  const graph = { nodes, edges };
@@ -562,11 +827,8 @@ function extractImportsFromContent(content) {
562
827
  const imports = [];
563
828
  const patterns = [
564
829
  /import\s+.*?\s+from\s+['"](.+?)['"]/g,
565
- // import ... from '...'
566
830
  /import\s+['"](.+?)['"]/g,
567
- // import '...'
568
831
  /require\(['"](.+?)['"]\)/g
569
- // require('...')
570
832
  ];
571
833
  for (const pattern of patterns) {
572
834
  let match;
@@ -580,31 +842,25 @@ function extractImportsFromContent(content) {
580
842
  return [...new Set(imports)];
581
843
  }
582
844
  function calculateImportDepth(file, graph, visited = /* @__PURE__ */ new Set(), depth = 0) {
583
- if (visited.has(file)) {
584
- return depth;
585
- }
845
+ if (visited.has(file)) return depth;
586
846
  const dependencies = graph.edges.get(file);
587
- if (!dependencies || dependencies.size === 0) {
588
- return depth;
589
- }
847
+ if (!dependencies || dependencies.size === 0) return depth;
590
848
  visited.add(file);
591
849
  let maxDepth = depth;
592
850
  for (const dep of dependencies) {
593
- const depDepth = calculateImportDepth(dep, graph, visited, depth + 1);
594
- maxDepth = Math.max(maxDepth, depDepth);
851
+ maxDepth = Math.max(
852
+ maxDepth,
853
+ calculateImportDepth(dep, graph, visited, depth + 1)
854
+ );
595
855
  }
596
856
  visited.delete(file);
597
857
  return maxDepth;
598
858
  }
599
859
  function getTransitiveDependencies(file, graph, visited = /* @__PURE__ */ new Set()) {
600
- if (visited.has(file)) {
601
- return [];
602
- }
860
+ if (visited.has(file)) return [];
603
861
  visited.add(file);
604
862
  const dependencies = graph.edges.get(file);
605
- if (!dependencies || dependencies.size === 0) {
606
- return [];
607
- }
863
+ if (!dependencies || dependencies.size === 0) return [];
608
864
  const allDeps = [];
609
865
  for (const dep of dependencies) {
610
866
  allDeps.push(dep);
@@ -637,9 +893,7 @@ function detectCircularDependencies(graph) {
637
893
  }
638
894
  return;
639
895
  }
640
- if (visited.has(file)) {
641
- return;
642
- }
896
+ if (visited.has(file)) return;
643
897
  visited.add(file);
644
898
  recursionStack.add(file);
645
899
  path.push(file);
@@ -658,404 +912,23 @@ function detectCircularDependencies(graph) {
658
912
  }
659
913
  return cycles;
660
914
  }
661
- function calculateCohesion(exports2, filePath, options) {
662
- return calculateEnhancedCohesion(exports2, filePath, options);
663
- }
664
- function isTestFile(filePath) {
665
- const lower = filePath.toLowerCase();
666
- return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
667
- }
668
- function calculateFragmentation(files, domain, options) {
669
- if (files.length <= 1) return 0;
670
- const directories = new Set(
671
- files.map((f) => f.split("/").slice(0, -1).join("/"))
672
- );
673
- const uniqueDirs = directories.size;
674
- if (options?.useLogScale) {
675
- if (uniqueDirs <= 1) return 0;
676
- const total = files.length;
677
- const base = options.logBase || Math.E;
678
- const num = Math.log(uniqueDirs) / Math.log(base);
679
- const den = Math.log(total) / Math.log(base);
680
- return den > 0 ? num / den : 0;
681
- }
682
- return (uniqueDirs - 1) / (files.length - 1);
683
- }
684
- function calculatePathEntropy(files) {
685
- if (!files || files.length === 0) return 0;
686
- const dirCounts = /* @__PURE__ */ new Map();
687
- for (const f of files) {
688
- const dir = f.split("/").slice(0, -1).join("/") || ".";
689
- dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
690
- }
691
- const counts = Array.from(dirCounts.values());
692
- if (counts.length <= 1) return 0;
693
- const total = counts.reduce((s, v) => s + v, 0);
694
- let entropy = 0;
695
- for (const count of counts) {
696
- const prob = count / total;
697
- entropy -= prob * Math.log2(prob);
698
- }
699
- const maxEntropy = Math.log2(counts.length);
700
- return maxEntropy > 0 ? entropy / maxEntropy : 0;
701
- }
702
- function calculateDirectoryDistance(files) {
703
- if (!files || files.length <= 1) return 0;
704
- function pathSegments(p) {
705
- return p.split("/").filter(Boolean);
706
- }
707
- function commonAncestorDepth(a, b) {
708
- const minLen = Math.min(a.length, b.length);
709
- let i = 0;
710
- while (i < minLen && a[i] === b[i]) i++;
711
- return i;
712
- }
713
- let totalNormalized = 0;
714
- let comparisons = 0;
715
- for (let i = 0; i < files.length; i++) {
716
- for (let j = i + 1; j < files.length; j++) {
717
- const segA = pathSegments(files[i]);
718
- const segB = pathSegments(files[j]);
719
- const shared = commonAncestorDepth(segA, segB);
720
- const maxDepth = Math.max(segA.length, segB.length);
721
- const normalizedShared = maxDepth > 0 ? shared / maxDepth : 0;
722
- totalNormalized += 1 - normalizedShared;
723
- comparisons++;
724
- }
725
- }
726
- return comparisons > 0 ? totalNormalized / comparisons : 0;
727
- }
728
- function detectModuleClusters(graph, options) {
729
- const domainMap = /* @__PURE__ */ new Map();
730
- for (const [file, node] of graph.nodes.entries()) {
731
- const domains = node.exports.map((e) => e.inferredDomain || "unknown");
732
- const primaryDomain = domains[0] || "unknown";
733
- if (!domainMap.has(primaryDomain)) {
734
- domainMap.set(primaryDomain, []);
735
- }
736
- domainMap.get(primaryDomain).push(file);
737
- }
738
- const clusters = [];
739
- for (const [domain, files] of domainMap.entries()) {
740
- if (files.length < 2) continue;
741
- const totalTokens = files.reduce((sum, file) => {
742
- const node = graph.nodes.get(file);
743
- return sum + (node?.tokenCost || 0);
744
- }, 0);
745
- const baseFragmentation = calculateFragmentation(files, domain, {
746
- useLogScale: !!options?.useLogScale
747
- });
748
- let importSimilarityTotal = 0;
749
- let importComparisons = 0;
750
- for (let i = 0; i < files.length; i++) {
751
- for (let j = i + 1; j < files.length; j++) {
752
- const f1 = files[i];
753
- const f2 = files[j];
754
- const n1 = graph.nodes.get(f1)?.imports || [];
755
- const n2 = graph.nodes.get(f2)?.imports || [];
756
- const similarity = n1.length === 0 && n2.length === 0 ? 0 : calculateJaccardSimilarity(n1, n2);
757
- importSimilarityTotal += similarity;
758
- importComparisons++;
759
- }
760
- }
761
- const importCohesion = importComparisons > 0 ? importSimilarityTotal / importComparisons : 0;
762
- const couplingDiscountFactor = 1 - 0.2 * importCohesion;
763
- const fragmentationScore = baseFragmentation * couplingDiscountFactor;
764
- const pathEntropy = calculatePathEntropy(files);
765
- const directoryDistance = calculateDirectoryDistance(files);
766
- const avgCohesion = files.reduce((sum, file) => {
767
- const node = graph.nodes.get(file);
768
- return sum + (node ? calculateCohesion(node.exports, file, {
769
- coUsageMatrix: graph.coUsageMatrix
770
- }) : 0);
771
- }, 0) / files.length;
772
- const targetFiles = Math.max(1, Math.ceil(files.length / 3));
773
- const consolidationPlan = generateConsolidationPlan(
774
- domain,
775
- files,
776
- targetFiles
777
- );
778
- clusters.push({
779
- domain,
780
- files,
781
- totalTokens,
782
- fragmentationScore,
783
- pathEntropy,
784
- directoryDistance,
785
- importCohesion,
786
- avgCohesion,
787
- suggestedStructure: {
788
- targetFiles,
789
- consolidationPlan
790
- }
791
- });
792
- }
793
- return clusters.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
794
- }
795
- function extractExports(content, filePath, domainOptions, fileImports) {
796
- const exports2 = [];
797
- const patterns = [
798
- /export\s+function\s+(\w+)/g,
799
- /export\s+class\s+(\w+)/g,
800
- /export\s+const\s+(\w+)/g,
801
- /export\s+type\s+(\w+)/g,
802
- /export\s+interface\s+(\w+)/g,
803
- /export\s+default/g
804
- ];
805
- const types = [
806
- "function",
807
- "class",
808
- "const",
809
- "type",
810
- "interface",
811
- "default"
812
- ];
813
- patterns.forEach((pattern, index) => {
814
- let match;
815
- while ((match = pattern.exec(content)) !== null) {
816
- const name = match[1] || "default";
817
- const type = types[index];
818
- const inferredDomain = inferDomain(
819
- name,
820
- filePath,
821
- domainOptions,
822
- fileImports
823
- );
824
- exports2.push({ name, type, inferredDomain });
825
- }
826
- });
827
- return exports2;
828
- }
829
- function inferDomain(name, filePath, domainOptions, fileImports) {
830
- const lower = name.toLowerCase();
831
- const tokens = Array.from(
832
- new Set(
833
- lower.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-z0-9]+/gi, " ").split(" ").filter(Boolean)
834
- )
835
- );
836
- const defaultKeywords = [
837
- "authentication",
838
- "authorization",
839
- "payment",
840
- "invoice",
841
- "customer",
842
- "product",
843
- "order",
844
- "cart",
845
- "user",
846
- "admin",
847
- "repository",
848
- "controller",
849
- "service",
850
- "config",
851
- "model",
852
- "view",
853
- "auth"
854
- ];
855
- const domainKeywords = domainOptions?.domainKeywords && domainOptions.domainKeywords.length ? [...domainOptions.domainKeywords, ...defaultKeywords] : defaultKeywords;
856
- for (const keyword of domainKeywords) {
857
- if (tokens.includes(keyword)) {
858
- return keyword;
859
- }
860
- }
861
- for (const keyword of domainKeywords) {
862
- if (lower.includes(keyword)) {
863
- return keyword;
864
- }
865
- }
866
- if (fileImports && fileImports.length > 0) {
867
- for (const importPath of fileImports) {
868
- const allSegments = importPath.split("/");
869
- const relevantSegments = allSegments.filter((s) => {
870
- if (!s) return false;
871
- if (s === "." || s === "..") return false;
872
- if (s.startsWith("@") && s.length === 1) return false;
873
- return true;
874
- }).map((s) => s.startsWith("@") ? s.slice(1) : s);
875
- for (const segment of relevantSegments) {
876
- const segLower = segment.toLowerCase();
877
- const singularSegment = singularize(segLower);
878
- for (const keyword of domainKeywords) {
879
- if (singularSegment === keyword || segLower === keyword || segLower.includes(keyword)) {
880
- return keyword;
881
- }
882
- }
883
- }
884
- }
885
- }
886
- if (filePath) {
887
- const pathSegments = filePath.toLowerCase().split("/");
888
- for (const segment of pathSegments) {
889
- const singularSegment = singularize(segment);
890
- for (const keyword of domainKeywords) {
891
- if (singularSegment === keyword || segment === keyword || segment.includes(keyword)) {
892
- return keyword;
893
- }
894
- }
895
- }
896
- }
897
- return "unknown";
898
- }
899
- function generateConsolidationPlan(domain, files, targetFiles) {
900
- const plan = [];
901
- if (files.length <= targetFiles) {
902
- return [`No consolidation needed for ${domain}`];
903
- }
904
- plan.push(
905
- `Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s):`
906
- );
907
- const dirGroups = /* @__PURE__ */ new Map();
908
- for (const file of files) {
909
- const dir = file.split("/").slice(0, -1).join("/");
910
- if (!dirGroups.has(dir)) {
911
- dirGroups.set(dir, []);
912
- }
913
- dirGroups.get(dir).push(file);
914
- }
915
- plan.push(`1. Create unified ${domain} module file`);
916
- plan.push(
917
- `2. Move related functionality from ${files.length} scattered files`
918
- );
919
- plan.push(`3. Update imports in dependent files`);
920
- plan.push(
921
- `4. Remove old files after consolidation (verify with tests first)`
922
- );
923
- return plan;
924
- }
925
- function extractExportsWithAST(content, filePath, domainOptions, fileImports) {
926
- try {
927
- const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
928
- return astExports.map((exp) => ({
929
- name: exp.name,
930
- type: exp.type,
931
- inferredDomain: inferDomain(
932
- exp.name,
933
- filePath,
934
- domainOptions,
935
- fileImports
936
- ),
937
- imports: exp.imports,
938
- dependencies: exp.dependencies
939
- }));
940
- } catch (error) {
941
- void error;
942
- return extractExports(content, filePath, domainOptions, fileImports);
943
- }
944
- }
945
- function calculateEnhancedCohesion(exports2, filePath, options) {
946
- if (exports2.length === 0) return 1;
947
- if (exports2.length === 1) return 1;
948
- if (filePath && isTestFile(filePath)) {
949
- return 1;
950
- }
951
- const domainCohesion = calculateDomainCohesion(exports2);
952
- const hasImportData = exports2.some((e) => e.imports && e.imports.length > 0);
953
- const importCohesion = hasImportData ? calculateImportBasedCohesion(exports2) : void 0;
954
- const coUsageMatrix = options?.coUsageMatrix;
955
- const structuralCohesion = filePath && coUsageMatrix ? calculateStructuralCohesionFromCoUsage(filePath, coUsageMatrix) : void 0;
956
- const defaultWeights = {
957
- importBased: 0.5,
958
- structural: 0.3,
959
- domainBased: 0.2
960
- };
961
- const weights = { ...defaultWeights, ...options?.weights || {} };
962
- const signals = [];
963
- if (importCohesion !== void 0)
964
- signals.push({ score: importCohesion, weight: weights.importBased });
965
- if (structuralCohesion !== void 0)
966
- signals.push({ score: structuralCohesion, weight: weights.structural });
967
- signals.push({ score: domainCohesion, weight: weights.domainBased });
968
- const totalWeight = signals.reduce((s, el) => s + el.weight, 0);
969
- if (totalWeight === 0) return domainCohesion;
970
- const combined = signals.reduce(
971
- (sum, el) => sum + el.score * (el.weight / totalWeight),
972
- 0
973
- );
974
- return combined;
975
- }
976
- function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
977
- if (!coUsageMatrix) return 1;
978
- const coUsages = coUsageMatrix.get(file);
979
- if (!coUsages || coUsages.size === 0) return 1;
980
- let total = 0;
981
- for (const count of coUsages.values()) total += count;
982
- if (total === 0) return 1;
983
- const probs = [];
984
- for (const count of coUsages.values()) {
985
- if (count > 0) probs.push(count / total);
986
- }
987
- if (probs.length <= 1) return 1;
988
- let entropy = 0;
989
- for (const prob of probs) {
990
- entropy -= prob * Math.log2(prob);
991
- }
992
- const maxEntropy = Math.log2(probs.length);
993
- return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
994
- }
995
- function calculateImportBasedCohesion(exports2) {
996
- const exportsWithImports = exports2.filter(
997
- (e) => e.imports && e.imports.length > 0
998
- );
999
- if (exportsWithImports.length < 2) {
1000
- return 1;
1001
- }
1002
- let totalSimilarity = 0;
1003
- let comparisons = 0;
1004
- for (let i = 0; i < exportsWithImports.length; i++) {
1005
- for (let j = i + 1; j < exportsWithImports.length; j++) {
1006
- const exp1 = exportsWithImports[i];
1007
- const exp2 = exportsWithImports[j];
1008
- const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
1009
- totalSimilarity += similarity;
1010
- comparisons++;
1011
- }
1012
- }
1013
- return comparisons > 0 ? totalSimilarity / comparisons : 1;
1014
- }
1015
- function calculateJaccardSimilarity(arr1, arr2) {
1016
- if (arr1.length === 0 && arr2.length === 0) return 1;
1017
- if (arr1.length === 0 || arr2.length === 0) return 0;
1018
- const set1 = new Set(arr1);
1019
- const set2 = new Set(arr2);
1020
- const intersection = new Set([...set1].filter((x) => set2.has(x)));
1021
- const union = /* @__PURE__ */ new Set([...set1, ...set2]);
1022
- return intersection.size / union.size;
1023
- }
1024
- function calculateDomainCohesion(exports2) {
1025
- const domains = exports2.map((e) => e.inferredDomain || "unknown");
1026
- const domainCounts = /* @__PURE__ */ new Map();
1027
- for (const domain of domains) {
1028
- domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
1029
- }
1030
- const total = domains.length;
1031
- let entropy = 0;
1032
- for (const domainCount of domainCounts.values()) {
1033
- const prob = domainCount / total;
1034
- if (prob > 0) {
1035
- entropy -= prob * Math.log2(prob);
1036
- }
1037
- }
1038
- const maxEntropy = Math.log2(total);
1039
- return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
1040
- }
1041
- function classifyFile(node, cohesionScore, domains) {
1042
- const { exports: exports2, imports, linesOfCode, file } = node;
1043
- void imports;
1044
- void linesOfCode;
915
+
916
+ // src/classifier.ts
917
+ function classifyFile(node, cohesionScore = 1, domains = []) {
1045
918
  if (isBarrelExport(node)) {
1046
919
  return "barrel-export";
1047
920
  }
1048
- if (isTypeDefinitionFile(node)) {
921
+ if (isTypeDefinition(node)) {
1049
922
  return "type-definition";
1050
923
  }
1051
- if (isConfigOrSchemaFile(node)) {
1052
- return "cohesive-module";
924
+ if (isNextJsPage(node)) {
925
+ return "nextjs-page";
1053
926
  }
1054
927
  if (isLambdaHandler(node)) {
1055
928
  return "lambda-handler";
1056
929
  }
1057
- if (isDataAccessFile(node)) {
1058
- return "cohesive-module";
930
+ if (isServiceFile(node)) {
931
+ return "service-file";
1059
932
  }
1060
933
  if (isEmailTemplate(node)) {
1061
934
  return "email-template";
@@ -1063,224 +936,53 @@ function classifyFile(node, cohesionScore, domains) {
1063
936
  if (isParserFile(node)) {
1064
937
  return "parser-file";
1065
938
  }
1066
- if (isServiceFile(node)) {
1067
- return "service-file";
1068
- }
1069
939
  if (isSessionFile(node)) {
1070
- return "cohesive-module";
1071
- }
1072
- if (isNextJsPage(node)) {
1073
- return "nextjs-page";
1074
- }
1075
- if (isUtilityFile(node)) {
940
+ if (cohesionScore >= 0.25 && domains.length <= 1) return "cohesive-module";
1076
941
  return "utility-module";
1077
942
  }
1078
- if (file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/")) {
943
+ if (isUtilityModule(node)) {
1079
944
  return "utility-module";
1080
945
  }
1081
- const uniqueDomains = domains.filter((d) => d !== "unknown");
1082
- const hasSingleDomain = uniqueDomains.length <= 1;
1083
- if (hasSingleDomain) {
946
+ if (isConfigFile(node)) {
1084
947
  return "cohesive-module";
1085
948
  }
1086
- if (allExportsShareEntityNoun(exports2)) {
949
+ if (domains.length <= 1 && domains[0] !== "unknown") {
1087
950
  return "cohesive-module";
1088
951
  }
1089
- const hasMultipleDomains = uniqueDomains.length > 1;
1090
- const hasLowCohesion = cohesionScore < 0.4;
1091
- if (hasMultipleDomains && hasLowCohesion) {
952
+ if (domains.length > 1 && cohesionScore < 0.4) {
1092
953
  return "mixed-concerns";
1093
954
  }
1094
- if (cohesionScore >= 0.5) {
955
+ if (cohesionScore >= 0.7) {
1095
956
  return "cohesive-module";
1096
957
  }
1097
958
  return "unknown";
1098
959
  }
1099
960
  function isBarrelExport(node) {
1100
- const { file, exports: exports2, imports, linesOfCode } = node;
1101
- const fileName = file.split("/").pop()?.toLowerCase();
1102
- const isIndexFile = fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx";
1103
- const hasReExports = exports2.length > 0 && imports.length > 0;
1104
- const highExportToLinesRatio = exports2.length > 3 && linesOfCode < exports2.length * 5;
1105
- const sparseCode = linesOfCode > 0 && linesOfCode < 50 && exports2.length >= 2;
1106
- if (isIndexFile && hasReExports) {
1107
- return true;
1108
- }
1109
- if (highExportToLinesRatio && imports.length >= exports2.length * 0.5) {
1110
- return true;
1111
- }
1112
- if (sparseCode && imports.length > 0) {
1113
- return true;
1114
- }
1115
- return false;
1116
- }
1117
- function isTypeDefinitionFile(node) {
1118
961
  const { file, exports: exports2 } = node;
1119
962
  const fileName = file.split("/").pop()?.toLowerCase();
1120
- const isTypesFile = fileName?.includes("types") || fileName?.includes(".d.ts") || fileName === "types.ts" || fileName === "interfaces.ts";
1121
- const lowerPath = file.toLowerCase();
1122
- const isTypesPath = lowerPath.includes("/types/") || lowerPath.includes("/typings/") || lowerPath.includes("/@types/") || lowerPath.startsWith("types/") || lowerPath.startsWith("typings/");
1123
- const typeExports = exports2.filter(
1124
- (e) => e.type === "type" || e.type === "interface"
963
+ const isIndexFile = fileName === "index.ts" || fileName === "index.js";
964
+ const isSmallAndManyExports = node.tokenCost < 1e3 && (exports2 || []).length > 5;
965
+ const isReexportPattern = (exports2 || []).length >= 5 && (exports2 || []).every(
966
+ (e) => e.type === "const" || e.type === "function" || e.type === "type" || e.type === "interface"
1125
967
  );
1126
- const runtimeExports = exports2.filter(
1127
- (e) => e.type === "function" || e.type === "class" || e.type === "const"
1128
- );
1129
- const mostlyTypes = exports2.length > 0 && typeExports.length > runtimeExports.length && typeExports.length / exports2.length > 0.7;
1130
- const pureTypeFile = exports2.length > 0 && typeExports.length === exports2.length;
1131
- const emptyOrReExportInTypesDir = isTypesPath && exports2.length === 0;
1132
- return isTypesFile || isTypesPath || mostlyTypes || pureTypeFile || emptyOrReExportInTypesDir;
1133
- }
1134
- function isConfigOrSchemaFile(node) {
1135
- const { file, exports: exports2 } = node;
968
+ return !!isIndexFile || !!isSmallAndManyExports || !!isReexportPattern;
969
+ }
970
+ function isTypeDefinition(node) {
971
+ const { file } = node;
972
+ if (file.endsWith(".d.ts")) return true;
973
+ const nodeExports = node.exports || [];
974
+ const hasExports = nodeExports.length > 0;
975
+ const areAllTypes = hasExports && nodeExports.every((e) => e.type === "type" || e.type === "interface");
976
+ const allTypes = !!areAllTypes;
977
+ const isTypePath = file.toLowerCase().includes("/types/") || file.toLowerCase().includes("/interfaces/") || file.toLowerCase().includes("/models/");
978
+ return allTypes || isTypePath && hasExports;
979
+ }
980
+ function isUtilityModule(node) {
981
+ const { file } = node;
982
+ const isUtilPath = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/") || file.toLowerCase().includes("/util/") || file.toLowerCase().includes("/helper/");
1136
983
  const fileName = file.split("/").pop()?.toLowerCase();
1137
- const configPatterns = [
1138
- "config",
1139
- "schema",
1140
- "settings",
1141
- "options",
1142
- "constants",
1143
- "env",
1144
- "environment",
1145
- ".config.",
1146
- "-config.",
1147
- "_config."
1148
- ];
1149
- const isConfigName = configPatterns.some(
1150
- (pattern) => fileName?.includes(pattern) || fileName?.startsWith(pattern) || fileName?.endsWith(`${pattern}.ts`)
1151
- );
1152
- const isConfigPath = file.toLowerCase().includes("/config/") || file.toLowerCase().includes("/schemas/") || file.toLowerCase().includes("/settings/");
1153
- const hasSchemaExports = exports2.some(
1154
- (e) => e.name.toLowerCase().includes("table") || e.name.toLowerCase().includes("schema") || e.name.toLowerCase().includes("config") || e.name.toLowerCase().includes("setting")
1155
- );
1156
- return isConfigName || isConfigPath || hasSchemaExports;
1157
- }
1158
- function isUtilityFile(node) {
1159
- const { file, exports: exports2 } = node;
1160
- const fileName = file.split("/").pop()?.toLowerCase();
1161
- const utilityPatterns = [
1162
- "util",
1163
- "utility",
1164
- "utilities",
1165
- "helper",
1166
- "helpers",
1167
- "common",
1168
- "shared",
1169
- "toolbox",
1170
- "toolkit",
1171
- ".util.",
1172
- "-util.",
1173
- "_util.",
1174
- "-utils.",
1175
- ".utils."
1176
- ];
1177
- const isUtilityName = utilityPatterns.some(
1178
- (pattern) => fileName?.includes(pattern)
1179
- );
1180
- const isUtilityPath = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/") || file.toLowerCase().includes("/common/") || file.toLowerCase().endsWith("-utils.ts") || file.toLowerCase().endsWith("-util.ts") || file.toLowerCase().endsWith("-helper.ts") || file.toLowerCase().endsWith("-helpers.ts");
1181
- const hasManySmallExportsInUtilityContext = exports2.length >= 3 && exports2.every((e) => e.type === "function" || e.type === "const") && (isUtilityName || isUtilityPath);
1182
- return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
1183
- }
1184
- function splitCamelCase(name) {
1185
- return name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
1186
- }
1187
- var SKIP_WORDS = /* @__PURE__ */ new Set([
1188
- "get",
1189
- "set",
1190
- "create",
1191
- "update",
1192
- "delete",
1193
- "fetch",
1194
- "save",
1195
- "load",
1196
- "parse",
1197
- "format",
1198
- "validate",
1199
- "convert",
1200
- "transform",
1201
- "build",
1202
- "generate",
1203
- "render",
1204
- "send",
1205
- "receive",
1206
- "find",
1207
- "list",
1208
- "add",
1209
- "remove",
1210
- "insert",
1211
- "upsert",
1212
- "put",
1213
- "read",
1214
- "write",
1215
- "check",
1216
- "handle",
1217
- "process",
1218
- "compute",
1219
- "calculate",
1220
- "init",
1221
- "reset",
1222
- "clear",
1223
- "pending",
1224
- "active",
1225
- "current",
1226
- "new",
1227
- "old",
1228
- "all",
1229
- "by",
1230
- "with",
1231
- "from",
1232
- "to",
1233
- "and",
1234
- "or",
1235
- "is",
1236
- "has",
1237
- "in",
1238
- "on",
1239
- "of",
1240
- "the"
1241
- ]);
1242
- function simpleSingularize(word) {
1243
- if (word.endsWith("ies") && word.length > 3) return word.slice(0, -3) + "y";
1244
- if (word.endsWith("ses") && word.length > 4) return word.slice(0, -2);
1245
- if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
1246
- return word;
1247
- }
1248
- function extractEntityNouns(name) {
1249
- return splitCamelCase(name).filter((token) => !SKIP_WORDS.has(token) && token.length > 2).map(simpleSingularize);
1250
- }
1251
- function allExportsShareEntityNoun(exports2) {
1252
- if (exports2.length < 2 || exports2.length > 30) return false;
1253
- const nounSets = exports2.map((e) => new Set(extractEntityNouns(e.name)));
1254
- if (nounSets.some((s) => s.size === 0)) return false;
1255
- const [first, ...rest] = nounSets;
1256
- const commonNouns = Array.from(first).filter(
1257
- (noun) => rest.every((s) => s.has(noun))
1258
- );
1259
- return commonNouns.length > 0;
1260
- }
1261
- function isDataAccessFile(node) {
1262
- const { file, exports: exports2 } = node;
1263
- const fileName = file.split("/").pop()?.toLowerCase();
1264
- const dalPatterns = [
1265
- "dynamo",
1266
- "database",
1267
- "repository",
1268
- "repo",
1269
- "dao",
1270
- "firestore",
1271
- "postgres",
1272
- "mysql",
1273
- "mongo",
1274
- "redis",
1275
- "sqlite",
1276
- "supabase",
1277
- "prisma"
1278
- ];
1279
- const isDalName = dalPatterns.some((p) => fileName?.includes(p));
1280
- const isDalPath = file.toLowerCase().includes("/repositories/") || file.toLowerCase().includes("/dao/") || file.toLowerCase().includes("/data/");
1281
- const hasDalExportPattern = exports2.length >= 1 && exports2.length <= 10 && allExportsShareEntityNoun(exports2);
1282
- const isUtilityPathLocal = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/");
1283
- return isDalPath || isDalName && hasDalExportPattern && !isUtilityPathLocal;
984
+ const isUtilName = fileName?.includes("utils.") || fileName?.includes("helpers.") || fileName?.includes("util.") || fileName?.includes("helper.");
985
+ return !!isUtilPath || !!isUtilName;
1284
986
  }
1285
987
  function isLambdaHandler(node) {
1286
988
  const { file, exports: exports2 } = node;
@@ -1297,11 +999,10 @@ function isLambdaHandler(node) {
1297
999
  (pattern) => fileName?.includes(pattern)
1298
1000
  );
1299
1001
  const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/lambda/") || file.toLowerCase().includes("/functions/");
1300
- const hasHandlerExport = exports2.some(
1002
+ const hasHandlerExport = (exports2 || []).some(
1301
1003
  (e) => e.name.toLowerCase() === "handler" || e.name.toLowerCase() === "main" || e.name.toLowerCase() === "lambdahandler" || e.name.toLowerCase().endsWith("handler")
1302
1004
  );
1303
- const hasSingleEntryInHandlerContext = exports2.length === 1 && (exports2[0].type === "function" || exports2[0].name === "default") && (isHandlerPath || isHandlerName);
1304
- return isHandlerName || isHandlerPath || hasHandlerExport || hasSingleEntryInHandlerContext;
1005
+ return !!isHandlerName || !!isHandlerPath || !!hasHandlerExport;
1305
1006
  }
1306
1007
  function isServiceFile(node) {
1307
1008
  const { file, exports: exports2 } = node;
@@ -1311,11 +1012,11 @@ function isServiceFile(node) {
1311
1012
  (pattern) => fileName?.includes(pattern)
1312
1013
  );
1313
1014
  const isServicePath = file.toLowerCase().includes("/services/");
1314
- const hasServiceNamedExport = exports2.some(
1015
+ const hasServiceNamedExport = (exports2 || []).some(
1315
1016
  (e) => e.name.toLowerCase().includes("service") || e.name.toLowerCase().endsWith("service")
1316
1017
  );
1317
- const hasClassExport = exports2.some((e) => e.type === "class");
1318
- return isServiceName || isServicePath || hasServiceNamedExport && hasClassExport;
1018
+ const hasClassExport = (exports2 || []).some((e) => e.type === "class");
1019
+ return !!isServiceName || !!isServicePath || !!hasServiceNamedExport && !!hasClassExport;
1319
1020
  }
1320
1021
  function isEmailTemplate(node) {
1321
1022
  const { file, exports: exports2 } = node;
@@ -1333,15 +1034,11 @@ function isEmailTemplate(node) {
1333
1034
  const isEmailTemplateName = emailTemplatePatterns.some(
1334
1035
  (pattern) => fileName?.includes(pattern)
1335
1036
  );
1336
- const isSpecificTemplateName = fileName?.includes("receipt") || fileName?.includes("invoice-email") || fileName?.includes("welcome-email") || fileName?.includes("notification-email") || fileName?.includes("writer") && fileName.includes("receipt");
1337
1037
  const isEmailPath = file.toLowerCase().includes("/emails/") || file.toLowerCase().includes("/mail/") || file.toLowerCase().includes("/notifications/");
1338
- const hasTemplateFunction = exports2.some(
1038
+ const hasTemplateFunction = (exports2 || []).some(
1339
1039
  (e) => e.type === "function" && (e.name.toLowerCase().startsWith("render") || e.name.toLowerCase().startsWith("generate") || e.name.toLowerCase().includes("template") && e.name.toLowerCase().includes("email"))
1340
1040
  );
1341
- const hasEmailExport = exports2.some(
1342
- (e) => e.name.toLowerCase().includes("template") && e.type === "function" || e.name.toLowerCase().includes("render") && e.type === "function" || e.name.toLowerCase().includes("email") && e.type !== "class"
1343
- );
1344
- return isEmailPath || isEmailTemplateName || isSpecificTemplateName || hasTemplateFunction && hasEmailExport;
1041
+ return !!isEmailPath || !!isEmailTemplateName || !!hasTemplateFunction;
1345
1042
  }
1346
1043
  function isParserFile(node) {
1347
1044
  const { file, exports: exports2 } = node;
@@ -1353,55 +1050,51 @@ function isParserFile(node) {
1353
1050
  "_parser.",
1354
1051
  "transform",
1355
1052
  ".transform.",
1356
- "-transform.",
1357
1053
  "converter",
1358
- ".converter.",
1359
- "-converter.",
1360
1054
  "mapper",
1361
- ".mapper.",
1362
- "-mapper.",
1363
- "serializer",
1364
- ".serializer.",
1365
- "deterministic"
1366
- // For base-parser-deterministic.ts pattern
1055
+ "serializer"
1367
1056
  ];
1368
1057
  const isParserName = parserPatterns.some(
1369
1058
  (pattern) => fileName?.includes(pattern)
1370
1059
  );
1371
- const isParserPath = file.toLowerCase().includes("/parsers/") || file.toLowerCase().includes("/transformers/") || file.toLowerCase().includes("/converters/") || file.toLowerCase().includes("/mappers/");
1372
- const hasParserExport = exports2.some(
1373
- (e) => e.name.toLowerCase().includes("parse") || e.name.toLowerCase().includes("transform") || e.name.toLowerCase().includes("convert") || e.name.toLowerCase().includes("map") || e.name.toLowerCase().includes("serialize") || e.name.toLowerCase().includes("deserialize")
1060
+ const isParserPath = file.toLowerCase().includes("/parsers/") || file.toLowerCase().includes("/transformers/");
1061
+ const hasParseFunction = (exports2 || []).some(
1062
+ (e) => e.type === "function" && (e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("extract"))
1374
1063
  );
1375
- const hasParseFunction = exports2.some(
1376
- (e) => e.type === "function" && (e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert") || e.name.toLowerCase().startsWith("map") || e.name.toLowerCase().startsWith("extract"))
1377
- );
1378
- return isParserName || isParserPath || hasParserExport || hasParseFunction;
1064
+ return !!isParserName || !!isParserPath || !!hasParseFunction;
1379
1065
  }
1380
1066
  function isSessionFile(node) {
1381
1067
  const { file, exports: exports2 } = node;
1382
1068
  const fileName = file.split("/").pop()?.toLowerCase();
1383
- const sessionPatterns = [
1384
- "session",
1385
- ".session.",
1386
- "-session.",
1387
- "state",
1388
- ".state.",
1389
- "-state.",
1390
- "context",
1391
- ".context.",
1392
- "-context.",
1393
- "store",
1394
- ".store.",
1395
- "-store."
1396
- ];
1069
+ const sessionPatterns = ["session", "state", "context", "store"];
1397
1070
  const isSessionName = sessionPatterns.some(
1398
1071
  (pattern) => fileName?.includes(pattern)
1399
1072
  );
1400
- const isSessionPath = file.toLowerCase().includes("/sessions/") || file.toLowerCase().includes("/state/") || file.toLowerCase().includes("/context/") || file.toLowerCase().includes("/store/");
1401
- const hasSessionExport = exports2.some(
1402
- (e) => e.name.toLowerCase().includes("session") || e.name.toLowerCase().includes("state") || e.name.toLowerCase().includes("context") || e.name.toLowerCase().includes("manager") || e.name.toLowerCase().includes("store")
1073
+ const isSessionPath = file.toLowerCase().includes("/sessions/") || file.toLowerCase().includes("/state/");
1074
+ const hasSessionExport = (exports2 || []).some(
1075
+ (e) => e.name.toLowerCase().includes("session") || e.name.toLowerCase().includes("state") || e.name.toLowerCase().includes("store")
1403
1076
  );
1404
- return isSessionName || isSessionPath || hasSessionExport;
1077
+ return !!isSessionName || !!isSessionPath || !!hasSessionExport;
1078
+ }
1079
+ function isConfigFile(node) {
1080
+ const { file, exports: exports2 } = node;
1081
+ const lowerPath = file.toLowerCase();
1082
+ const fileName = file.split("/").pop()?.toLowerCase();
1083
+ const configPatterns = [
1084
+ ".config.",
1085
+ "tsconfig",
1086
+ "jest.config",
1087
+ "package.json",
1088
+ "aiready.json",
1089
+ "next.config",
1090
+ "sst.config"
1091
+ ];
1092
+ const isConfigName = configPatterns.some((p) => fileName?.includes(p));
1093
+ const isConfigPath = lowerPath.includes("/config/") || lowerPath.includes("/settings/") || lowerPath.includes("/schemas/");
1094
+ const hasSchemaExports = (exports2 || []).some(
1095
+ (e) => e.name.toLowerCase().includes("schema") || e.name.toLowerCase().includes("config") || e.name.toLowerCase().includes("setting")
1096
+ );
1097
+ return !!isConfigName || !!isConfigPath || !!hasSchemaExports;
1405
1098
  }
1406
1099
  function isNextJsPage(node) {
1407
1100
  const { file, exports: exports2 } = node;
@@ -1409,24 +1102,19 @@ function isNextJsPage(node) {
1409
1102
  const fileName = file.split("/").pop()?.toLowerCase();
1410
1103
  const isInAppDir = lowerPath.includes("/app/") || lowerPath.startsWith("app/");
1411
1104
  const isPageFile = fileName === "page.tsx" || fileName === "page.ts";
1412
- if (!isInAppDir || !isPageFile) {
1413
- return false;
1414
- }
1415
- const exportNames = exports2.map((e) => e.name.toLowerCase());
1416
- const hasDefaultExport = exports2.some((e) => e.type === "default");
1105
+ if (!isInAppDir || !isPageFile) return false;
1106
+ const hasDefaultExport = (exports2 || []).some((e) => e.type === "default");
1417
1107
  const nextJsExports = [
1418
1108
  "metadata",
1419
1109
  "generatemetadata",
1420
1110
  "faqjsonld",
1421
1111
  "jsonld",
1422
- "icon",
1423
- "viewport",
1424
- "dynamic"
1112
+ "icon"
1425
1113
  ];
1426
- const hasNextJsExports = exportNames.some(
1427
- (name) => nextJsExports.includes(name) || name.includes("jsonld")
1114
+ const hasNextJsExports = (exports2 || []).some(
1115
+ (e) => nextJsExports.includes(e.name.toLowerCase())
1428
1116
  );
1429
- return hasDefaultExport || hasNextJsExports;
1117
+ return !!hasDefaultExport || !!hasNextJsExports;
1430
1118
  }
1431
1119
  function adjustCohesionForClassification(baseCohesion, classification, node) {
1432
1120
  switch (classification) {
@@ -1434,55 +1122,24 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
1434
1122
  return 1;
1435
1123
  case "type-definition":
1436
1124
  return 1;
1125
+ case "nextjs-page":
1126
+ return 1;
1437
1127
  case "utility-module": {
1438
- if (node) {
1439
- const exportNames = node.exports.map((e) => e.name.toLowerCase());
1440
- const hasRelatedNames = hasRelatedExportNames(exportNames);
1441
- if (hasRelatedNames) {
1442
- return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
1443
- }
1128
+ if (node && hasRelatedExportNames(
1129
+ (node.exports || []).map((e) => e.name.toLowerCase())
1130
+ )) {
1131
+ return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
1444
1132
  }
1445
1133
  return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
1446
1134
  }
1447
- case "service-file": {
1448
- if (node?.exports.some((e) => e.type === "class")) {
1449
- return Math.max(0.78, Math.min(1, baseCohesion + 0.4));
1450
- }
1135
+ case "service-file":
1451
1136
  return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
1452
- }
1453
- case "lambda-handler": {
1454
- if (node) {
1455
- const hasSingleEntry = node.exports.length === 1 || node.exports.some((e) => e.name.toLowerCase() === "handler");
1456
- if (hasSingleEntry) {
1457
- return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
1458
- }
1459
- }
1137
+ case "lambda-handler":
1460
1138
  return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
1461
- }
1462
- case "email-template": {
1463
- if (node) {
1464
- const hasTemplateFunc = node.exports.some(
1465
- (e) => e.name.toLowerCase().includes("render") || e.name.toLowerCase().includes("generate") || e.name.toLowerCase().includes("template")
1466
- );
1467
- if (hasTemplateFunc) {
1468
- return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
1469
- }
1470
- }
1139
+ case "email-template":
1471
1140
  return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
1472
- }
1473
- case "parser-file": {
1474
- if (node) {
1475
- const hasParseFunc = node.exports.some(
1476
- (e) => e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert")
1477
- );
1478
- if (hasParseFunc) {
1479
- return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
1480
- }
1481
- }
1141
+ case "parser-file":
1482
1142
  return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
1483
- }
1484
- case "nextjs-page":
1485
- return 1;
1486
1143
  case "cohesive-module":
1487
1144
  return Math.max(baseCohesion, 0.7);
1488
1145
  case "mixed-concerns":
@@ -1495,136 +1152,138 @@ function hasRelatedExportNames(exportNames) {
1495
1152
  if (exportNames.length < 2) return true;
1496
1153
  const stems = /* @__PURE__ */ new Set();
1497
1154
  const domains = /* @__PURE__ */ new Set();
1155
+ const verbs = [
1156
+ "get",
1157
+ "set",
1158
+ "create",
1159
+ "update",
1160
+ "delete",
1161
+ "fetch",
1162
+ "save",
1163
+ "load",
1164
+ "parse",
1165
+ "format",
1166
+ "validate"
1167
+ ];
1168
+ const domainPatterns = [
1169
+ "user",
1170
+ "order",
1171
+ "product",
1172
+ "session",
1173
+ "email",
1174
+ "file",
1175
+ "db",
1176
+ "api",
1177
+ "config"
1178
+ ];
1498
1179
  for (const name of exportNames) {
1499
- const verbs = [
1500
- "get",
1501
- "set",
1502
- "create",
1503
- "update",
1504
- "delete",
1505
- "fetch",
1506
- "save",
1507
- "load",
1508
- "parse",
1509
- "format",
1510
- "validate",
1511
- "convert",
1512
- "transform",
1513
- "build",
1514
- "generate",
1515
- "render",
1516
- "send",
1517
- "receive"
1518
- ];
1519
1180
  for (const verb of verbs) {
1520
1181
  if (name.startsWith(verb) && name.length > verb.length) {
1521
1182
  stems.add(name.slice(verb.length).toLowerCase());
1522
1183
  }
1523
1184
  }
1524
- const domainPatterns = [
1525
- "user",
1526
- "order",
1527
- "product",
1528
- "session",
1529
- "email",
1530
- "file",
1531
- "db",
1532
- "s3",
1533
- "dynamo",
1534
- "api",
1535
- "config"
1536
- ];
1537
1185
  for (const domain of domainPatterns) {
1538
- if (name.includes(domain)) {
1539
- domains.add(domain);
1186
+ if (name.includes(domain)) domains.add(domain);
1187
+ }
1188
+ }
1189
+ if (stems.size === 1 || domains.size === 1) return true;
1190
+ return false;
1191
+ }
1192
+ function adjustFragmentationForClassification(baseFragmentation, classification) {
1193
+ switch (classification) {
1194
+ case "barrel-export":
1195
+ return 0;
1196
+ case "type-definition":
1197
+ return 0;
1198
+ case "utility-module":
1199
+ case "service-file":
1200
+ case "lambda-handler":
1201
+ case "email-template":
1202
+ case "parser-file":
1203
+ case "nextjs-page":
1204
+ return baseFragmentation * 0.2;
1205
+ case "cohesive-module":
1206
+ return baseFragmentation * 0.3;
1207
+ case "mixed-concerns":
1208
+ return baseFragmentation;
1209
+ default:
1210
+ return baseFragmentation * 0.7;
1211
+ }
1212
+ }
1213
+
1214
+ // src/cluster-detector.ts
1215
+ function detectModuleClusters(graph, options) {
1216
+ const domainMap = /* @__PURE__ */ new Map();
1217
+ for (const [file, node] of graph.nodes.entries()) {
1218
+ const primaryDomain = node.exports[0]?.inferredDomain || "unknown";
1219
+ if (!domainMap.has(primaryDomain)) {
1220
+ domainMap.set(primaryDomain, []);
1221
+ }
1222
+ domainMap.get(primaryDomain).push(file);
1223
+ }
1224
+ const clusters = [];
1225
+ for (const [domain, files] of domainMap.entries()) {
1226
+ if (files.length < 2 || domain === "unknown") continue;
1227
+ const totalTokens = files.reduce((sum, file) => {
1228
+ const node = graph.nodes.get(file);
1229
+ return sum + (node?.tokenCost || 0);
1230
+ }, 0);
1231
+ let sharedImportRatio = 0;
1232
+ if (files.length >= 2) {
1233
+ const allImportSets = files.map(
1234
+ (f) => new Set(graph.nodes.get(f)?.imports || [])
1235
+ );
1236
+ let intersection = new Set(allImportSets[0]);
1237
+ let union = new Set(allImportSets[0]);
1238
+ for (let i = 1; i < allImportSets.length; i++) {
1239
+ const nextSet = allImportSets[i];
1240
+ intersection = new Set([...intersection].filter((x) => nextSet.has(x)));
1241
+ for (const x of nextSet) union.add(x);
1540
1242
  }
1243
+ sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
1541
1244
  }
1245
+ const fragmentation = calculateFragmentation(files, domain, {
1246
+ ...options,
1247
+ sharedImportRatio
1248
+ });
1249
+ let totalCohesion = 0;
1250
+ files.forEach((f) => {
1251
+ const node = graph.nodes.get(f);
1252
+ if (node) totalCohesion += calculateEnhancedCohesion(node.exports);
1253
+ });
1254
+ const avgCohesion = totalCohesion / files.length;
1255
+ clusters.push({
1256
+ domain,
1257
+ files,
1258
+ totalTokens,
1259
+ fragmentationScore: fragmentation,
1260
+ avgCohesion,
1261
+ suggestedStructure: generateSuggestedStructure(
1262
+ files,
1263
+ totalTokens,
1264
+ fragmentation
1265
+ )
1266
+ });
1542
1267
  }
1543
- if (stems.size === 1 && exportNames.length >= 2) return true;
1544
- if (domains.size === 1 && exportNames.length >= 2) return true;
1545
- const prefixes = exportNames.map((name) => {
1546
- const match = name.match(/^([a-z]+)/);
1547
- return match ? match[1] : "";
1548
- }).filter((p) => p.length >= 3);
1549
- if (prefixes.length >= 2) {
1550
- const uniquePrefixes = new Set(prefixes);
1551
- if (uniquePrefixes.size === 1) return true;
1552
- }
1553
- const nounSets = exportNames.map((name) => {
1554
- const tokens = name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
1555
- const skip = /* @__PURE__ */ new Set([
1556
- "get",
1557
- "set",
1558
- "create",
1559
- "update",
1560
- "delete",
1561
- "fetch",
1562
- "save",
1563
- "load",
1564
- "parse",
1565
- "format",
1566
- "validate",
1567
- "convert",
1568
- "transform",
1569
- "build",
1570
- "generate",
1571
- "render",
1572
- "send",
1573
- "receive",
1574
- "find",
1575
- "list",
1576
- "add",
1577
- "remove",
1578
- "insert",
1579
- "upsert",
1580
- "put",
1581
- "read",
1582
- "write",
1583
- "check",
1584
- "handle",
1585
- "process",
1586
- "pending",
1587
- "active",
1588
- "current",
1589
- "new",
1590
- "old",
1591
- "all"
1592
- ]);
1593
- const singularize2 = (w) => w.endsWith("s") && w.length > 3 ? w.slice(0, -1) : w;
1594
- return new Set(
1595
- tokens.filter((t) => !skip.has(t) && t.length > 2).map(singularize2)
1596
- );
1597
- });
1598
- if (nounSets.length >= 2 && nounSets.every((s) => s.size > 0)) {
1599
- const [first, ...rest] = nounSets;
1600
- const commonNouns = Array.from(first).filter(
1601
- (n) => rest.every((s) => s.has(n))
1268
+ return clusters;
1269
+ }
1270
+ function generateSuggestedStructure(files, tokens, fragmentation) {
1271
+ const targetFiles = Math.max(1, Math.ceil(tokens / 1e4));
1272
+ const plan = [];
1273
+ if (fragmentation > 0.5) {
1274
+ plan.push(
1275
+ `Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
1602
1276
  );
1603
- if (commonNouns.length > 0) return true;
1604
1277
  }
1605
- return false;
1606
- }
1607
- function adjustFragmentationForClassification(baseFragmentation, classification) {
1608
- switch (classification) {
1609
- case "barrel-export":
1610
- return 0;
1611
- case "type-definition":
1612
- return 0;
1613
- case "utility-module":
1614
- case "service-file":
1615
- case "lambda-handler":
1616
- case "email-template":
1617
- case "parser-file":
1618
- case "nextjs-page":
1619
- return baseFragmentation * 0.2;
1620
- case "cohesive-module":
1621
- return baseFragmentation * 0.3;
1622
- case "mixed-concerns":
1623
- return baseFragmentation;
1624
- default:
1625
- return baseFragmentation * 0.7;
1278
+ if (tokens > 2e4) {
1279
+ plan.push(
1280
+ `Domain logic is very large (${Math.round(tokens / 1e3)}k tokens). Ensure clear sub-domain boundaries.`
1281
+ );
1626
1282
  }
1283
+ return { targetFiles, consolidationPlan: plan };
1627
1284
  }
1285
+
1286
+ // src/remediation.ts
1628
1287
  function getClassificationRecommendations(classification, file, issues) {
1629
1288
  switch (classification) {
1630
1289
  case "barrel-export":
@@ -1682,9 +1341,167 @@ function getClassificationRecommendations(classification, file, issues) {
1682
1341
  return issues;
1683
1342
  }
1684
1343
  }
1344
+ function getGeneralRecommendations(metrics, thresholds) {
1345
+ const recommendations = [];
1346
+ const issues = [];
1347
+ let severity = "info";
1348
+ if (metrics.contextBudget > thresholds.maxContextBudget) {
1349
+ issues.push(
1350
+ `High context budget: ${Math.round(metrics.contextBudget / 1e3)}k tokens`
1351
+ );
1352
+ recommendations.push(
1353
+ "Reduce dependencies or split the file to lower context window requirements"
1354
+ );
1355
+ severity = "major";
1356
+ }
1357
+ if (metrics.importDepth > thresholds.maxDepth) {
1358
+ issues.push(`Deep import chain: ${metrics.importDepth} levels`);
1359
+ recommendations.push("Flatten the dependency graph by reducing nesting");
1360
+ if (severity !== "critical") severity = "major";
1361
+ }
1362
+ if (metrics.circularDeps.length > 0) {
1363
+ issues.push(
1364
+ `Circular dependencies detected: ${metrics.circularDeps.length}`
1365
+ );
1366
+ recommendations.push(
1367
+ "Refactor to remove circular imports (use dependency injection or interfaces)"
1368
+ );
1369
+ severity = "critical";
1370
+ }
1371
+ if (metrics.cohesionScore < thresholds.minCohesion) {
1372
+ issues.push(`Low cohesion score: ${metrics.cohesionScore.toFixed(2)}`);
1373
+ recommendations.push(
1374
+ "Extract unrelated exports into separate domain-specific modules"
1375
+ );
1376
+ if (severity === "info") severity = "minor";
1377
+ }
1378
+ if (metrics.fragmentationScore > thresholds.maxFragmentation) {
1379
+ issues.push(
1380
+ `High domain fragmentation: ${metrics.fragmentationScore.toFixed(2)}`
1381
+ );
1382
+ recommendations.push(
1383
+ "Consolidate domain-related files into fewer directories"
1384
+ );
1385
+ if (severity === "info") severity = "minor";
1386
+ }
1387
+ return { recommendations, issues, severity };
1388
+ }
1389
+
1390
+ // src/analyzer.ts
1391
+ function calculateCohesion(exports2, filePath, options) {
1392
+ if (exports2.length <= 1) return 1;
1393
+ if (filePath && isTestFile(filePath)) return 1;
1394
+ const domains = exports2.map((e) => e.inferredDomain || "unknown");
1395
+ const uniqueDomains = new Set(domains.filter((d) => d !== "unknown"));
1396
+ const hasImports = exports2.some((e) => !!e.imports);
1397
+ if (!hasImports && !options?.weights) {
1398
+ if (uniqueDomains.size <= 1) return 1;
1399
+ return 0.4;
1400
+ }
1401
+ return calculateEnhancedCohesion(exports2, filePath, options);
1402
+ }
1403
+ function analyzeIssues(params) {
1404
+ const {
1405
+ file,
1406
+ importDepth,
1407
+ contextBudget,
1408
+ cohesionScore,
1409
+ fragmentationScore,
1410
+ maxDepth,
1411
+ maxContextBudget,
1412
+ minCohesion,
1413
+ maxFragmentation,
1414
+ circularDeps
1415
+ } = params;
1416
+ const issues = [];
1417
+ const recommendations = [];
1418
+ let severity = "info";
1419
+ let potentialSavings = 0;
1420
+ if (circularDeps.length > 0) {
1421
+ severity = "critical";
1422
+ issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
1423
+ recommendations.push(
1424
+ "Break circular dependencies by extracting interfaces or using dependency injection"
1425
+ );
1426
+ potentialSavings += contextBudget * 0.2;
1427
+ }
1428
+ if (importDepth > maxDepth * 1.5) {
1429
+ severity = "critical";
1430
+ issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
1431
+ recommendations.push("Flatten dependency tree or use facade pattern");
1432
+ potentialSavings += contextBudget * 0.3;
1433
+ } else if (importDepth > maxDepth) {
1434
+ if (severity !== "critical") severity = "major";
1435
+ issues.push(
1436
+ `Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
1437
+ );
1438
+ recommendations.push("Consider reducing dependency depth");
1439
+ potentialSavings += contextBudget * 0.15;
1440
+ }
1441
+ if (contextBudget > maxContextBudget * 1.5) {
1442
+ severity = "critical";
1443
+ issues.push(
1444
+ `Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
1445
+ );
1446
+ recommendations.push(
1447
+ "Split into smaller modules or reduce dependency tree"
1448
+ );
1449
+ potentialSavings += contextBudget * 0.4;
1450
+ } else if (contextBudget > maxContextBudget) {
1451
+ if (severity !== "critical") severity = "major";
1452
+ issues.push(
1453
+ `Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
1454
+ );
1455
+ recommendations.push("Reduce file size or dependencies");
1456
+ potentialSavings += contextBudget * 0.2;
1457
+ }
1458
+ if (cohesionScore < minCohesion * 0.5) {
1459
+ if (severity !== "critical") severity = "major";
1460
+ issues.push(
1461
+ `Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
1462
+ );
1463
+ recommendations.push(
1464
+ "Split file by domain - separate unrelated functionality"
1465
+ );
1466
+ potentialSavings += contextBudget * 0.25;
1467
+ } else if (cohesionScore < minCohesion) {
1468
+ if (severity === "info") severity = "minor";
1469
+ issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
1470
+ recommendations.push("Consider grouping related exports together");
1471
+ potentialSavings += contextBudget * 0.1;
1472
+ }
1473
+ if (fragmentationScore > maxFragmentation) {
1474
+ if (severity === "info" || severity === "minor") severity = "minor";
1475
+ issues.push(
1476
+ `High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
1477
+ );
1478
+ recommendations.push("Consolidate with related files in same domain");
1479
+ potentialSavings += contextBudget * 0.3;
1480
+ }
1481
+ if (issues.length === 0) {
1482
+ issues.push("No significant issues detected");
1483
+ recommendations.push("File is well-structured for AI context usage");
1484
+ }
1485
+ if (isBuildArtifact(file)) {
1486
+ issues.push("Detected build artifact (bundled/output file)");
1487
+ recommendations.push("Exclude build outputs from analysis");
1488
+ severity = "info";
1489
+ potentialSavings = 0;
1490
+ }
1491
+ return {
1492
+ severity,
1493
+ issues,
1494
+ recommendations,
1495
+ potentialSavings: Math.floor(potentialSavings)
1496
+ };
1497
+ }
1498
+ function isBuildArtifact(filePath) {
1499
+ const lower = filePath.toLowerCase();
1500
+ return lower.includes("/node_modules/") || lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/out/") || lower.includes("/.next/");
1501
+ }
1685
1502
 
1686
1503
  // src/scoring.ts
1687
- var import_core2 = require("@aiready/core");
1504
+ var import_core4 = require("@aiready/core");
1688
1505
  function calculateContextScore(summary, costConfig) {
1689
1506
  const {
1690
1507
  avgContextBudget,
@@ -1780,14 +1597,16 @@ function calculateContextScore(summary, costConfig) {
1780
1597
  priority: "high"
1781
1598
  });
1782
1599
  }
1783
- const cfg = { ...import_core2.DEFAULT_COST_CONFIG, ...costConfig };
1784
- const totalContextBudget = avgContextBudget * summary.totalFiles;
1785
- const estimatedMonthlyCost = (0, import_core2.calculateMonthlyCost)(totalContextBudget, cfg);
1600
+ const cfg = { ...import_core4.DEFAULT_COST_CONFIG, ...costConfig };
1601
+ const estimatedMonthlyCost = (0, import_core4.calculateMonthlyCost)(
1602
+ avgContextBudget * (summary.totalFiles || 1),
1603
+ cfg
1604
+ );
1786
1605
  const issues = [
1787
1606
  ...Array(criticalIssues).fill({ severity: "critical" }),
1788
1607
  ...Array(majorIssues).fill({ severity: "major" })
1789
1608
  ];
1790
- const productivityImpact = (0, import_core2.calculateProductivityImpact)(issues);
1609
+ const productivityImpact = (0, import_core4.calculateProductivityImpact)(issues);
1791
1610
  return {
1792
1611
  toolName: "context-analyzer",
1793
1612
  score,
@@ -1799,7 +1618,6 @@ function calculateContextScore(summary, costConfig) {
1799
1618
  avgFragmentation: Math.round(avgFragmentation * 100) / 100,
1800
1619
  criticalIssues,
1801
1620
  majorIssues,
1802
- // Business value metrics
1803
1621
  estimatedMonthlyCost,
1804
1622
  estimatedDeveloperHours: productivityImpact.totalHours
1805
1623
  },
@@ -1807,10 +1625,18 @@ function calculateContextScore(summary, costConfig) {
1807
1625
  recommendations
1808
1626
  };
1809
1627
  }
1628
+ function mapScoreToRating(score) {
1629
+ if (score >= 90) return "excellent";
1630
+ if (score >= 75) return "good";
1631
+ if (score >= 60) return "fair";
1632
+ if (score >= 40) return "needs work";
1633
+ return "critical";
1634
+ }
1810
1635
 
1811
- // src/index.ts
1636
+ // src/defaults.ts
1637
+ var import_core5 = require("@aiready/core");
1812
1638
  async function getSmartDefaults(directory, userOptions) {
1813
- const files = await (0, import_core4.scanFiles)({
1639
+ const files = await (0, import_core5.scanFiles)({
1814
1640
  rootDir: directory,
1815
1641
  include: userOptions.include,
1816
1642
  exclude: userOptions.exclude
@@ -1842,17 +1668,146 @@ async function getSmartDefaults(directory, userOptions) {
1842
1668
  maxFragmentation = 0.8;
1843
1669
  }
1844
1670
  return {
1845
- maxDepth,
1671
+ maxDepth,
1672
+ maxContextBudget,
1673
+ minCohesion,
1674
+ maxFragmentation,
1675
+ focus: "all",
1676
+ includeNodeModules: false,
1677
+ rootDir: userOptions.rootDir || directory,
1678
+ include: userOptions.include,
1679
+ exclude: userOptions.exclude
1680
+ };
1681
+ }
1682
+
1683
+ // src/summary.ts
1684
+ function generateSummary(results) {
1685
+ if (results.length === 0) {
1686
+ return {
1687
+ totalFiles: 0,
1688
+ totalTokens: 0,
1689
+ avgContextBudget: 0,
1690
+ maxContextBudget: 0,
1691
+ avgImportDepth: 0,
1692
+ maxImportDepth: 0,
1693
+ deepFiles: [],
1694
+ avgFragmentation: 0,
1695
+ fragmentedModules: [],
1696
+ avgCohesion: 0,
1697
+ lowCohesionFiles: [],
1698
+ criticalIssues: 0,
1699
+ majorIssues: 0,
1700
+ minorIssues: 0,
1701
+ totalPotentialSavings: 0,
1702
+ topExpensiveFiles: []
1703
+ };
1704
+ }
1705
+ const totalFiles = results.length;
1706
+ const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
1707
+ const totalContextBudget = results.reduce(
1708
+ (sum, r) => sum + r.contextBudget,
1709
+ 0
1710
+ );
1711
+ const avgContextBudget = totalContextBudget / totalFiles;
1712
+ const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
1713
+ const avgImportDepth = results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
1714
+ const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
1715
+ const deepFiles = results.filter((r) => r.importDepth >= 5).map((r) => ({ file: r.file, depth: r.importDepth })).sort((a, b) => b.depth - a.depth).slice(0, 10);
1716
+ const avgFragmentation = results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
1717
+ const moduleMap = /* @__PURE__ */ new Map();
1718
+ for (const result of results) {
1719
+ for (const domain of result.domains) {
1720
+ if (!moduleMap.has(domain)) moduleMap.set(domain, []);
1721
+ moduleMap.get(domain).push(result);
1722
+ }
1723
+ }
1724
+ const fragmentedModules = [];
1725
+ for (const [domain, files] of moduleMap.entries()) {
1726
+ let jaccard2 = function(a, b) {
1727
+ const s1 = new Set(a || []);
1728
+ const s2 = new Set(b || []);
1729
+ if (s1.size === 0 && s2.size === 0) return 0;
1730
+ const inter = new Set([...s1].filter((x) => s2.has(x)));
1731
+ const uni = /* @__PURE__ */ new Set([...s1, ...s2]);
1732
+ return uni.size === 0 ? 0 : inter.size / uni.size;
1733
+ };
1734
+ var jaccard = jaccard2;
1735
+ if (files.length < 2) continue;
1736
+ const fragmentationScore = files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
1737
+ if (fragmentationScore < 0.3) continue;
1738
+ const totalTokens2 = files.reduce((sum, f) => sum + f.tokenCost, 0);
1739
+ const avgCohesion2 = files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
1740
+ const targetFiles = Math.max(1, Math.ceil(files.length / 3));
1741
+ const filePaths = files.map((f) => f.file);
1742
+ const pathEntropy = calculatePathEntropy(filePaths);
1743
+ const directoryDistance = calculateDirectoryDistance(filePaths);
1744
+ let importSimTotal = 0;
1745
+ let importPairs = 0;
1746
+ for (let i = 0; i < files.length; i++) {
1747
+ for (let j = i + 1; j < files.length; j++) {
1748
+ importSimTotal += jaccard2(
1749
+ files[i].dependencyList || [],
1750
+ files[j].dependencyList || []
1751
+ );
1752
+ importPairs++;
1753
+ }
1754
+ }
1755
+ const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
1756
+ fragmentedModules.push({
1757
+ domain,
1758
+ files: files.map((f) => f.file),
1759
+ totalTokens: totalTokens2,
1760
+ fragmentationScore,
1761
+ avgCohesion: avgCohesion2,
1762
+ importCohesion,
1763
+ pathEntropy,
1764
+ directoryDistance,
1765
+ suggestedStructure: {
1766
+ targetFiles,
1767
+ consolidationPlan: [
1768
+ `Consolidate ${files.length} files across ${new Set(files.map((f) => f.file.split("/").slice(0, -1).join("/"))).size} directories`,
1769
+ `Target ~${targetFiles} core modules to reduce context switching`
1770
+ ]
1771
+ }
1772
+ });
1773
+ }
1774
+ const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
1775
+ const lowCohesionFiles = results.filter((r) => r.cohesionScore < 0.4).map((r) => ({ file: r.file, score: r.cohesionScore })).sort((a, b) => a.score - b.score).slice(0, 10);
1776
+ const criticalIssues = results.filter(
1777
+ (r) => r.severity === "critical"
1778
+ ).length;
1779
+ const majorIssues = results.filter((r) => r.severity === "major").length;
1780
+ const minorIssues = results.filter((r) => r.severity === "minor").length;
1781
+ const totalPotentialSavings = results.reduce(
1782
+ (sum, r) => sum + r.potentialSavings,
1783
+ 0
1784
+ );
1785
+ const topExpensiveFiles = results.sort((a, b) => b.contextBudget - a.contextBudget).slice(0, 10).map((r) => ({
1786
+ file: r.file,
1787
+ contextBudget: r.contextBudget,
1788
+ severity: r.severity
1789
+ }));
1790
+ return {
1791
+ totalFiles,
1792
+ totalTokens,
1793
+ avgContextBudget,
1846
1794
  maxContextBudget,
1847
- minCohesion,
1848
- maxFragmentation,
1849
- focus: "all",
1850
- includeNodeModules: false,
1851
- rootDir: userOptions.rootDir || directory,
1852
- include: userOptions.include,
1853
- exclude: userOptions.exclude
1795
+ avgImportDepth,
1796
+ maxImportDepth,
1797
+ deepFiles,
1798
+ avgFragmentation,
1799
+ fragmentedModules,
1800
+ avgCohesion,
1801
+ lowCohesionFiles,
1802
+ criticalIssues,
1803
+ majorIssues,
1804
+ minorIssues,
1805
+ totalPotentialSavings,
1806
+ topExpensiveFiles
1854
1807
  };
1855
1808
  }
1809
+
1810
+ // src/index.ts
1856
1811
  async function analyzeContext(options) {
1857
1812
  const {
1858
1813
  maxDepth = 5,
@@ -1863,22 +1818,17 @@ async function analyzeContext(options) {
1863
1818
  includeNodeModules = false,
1864
1819
  ...scanOptions
1865
1820
  } = options;
1866
- const files = await (0, import_core4.scanFiles)({
1821
+ const files = await (0, import_core7.scanFiles)({
1867
1822
  ...scanOptions,
1868
- // Only add node_modules to exclude if includeNodeModules is false
1869
- // The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
1870
- // if user overrides the default exclude list
1871
1823
  exclude: includeNodeModules && scanOptions.exclude ? scanOptions.exclude.filter(
1872
1824
  (pattern) => pattern !== "**/node_modules/**"
1873
1825
  ) : scanOptions.exclude
1874
1826
  });
1875
1827
  const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
1876
- const tsJsFiles = files.filter((f) => !f.toLowerCase().endsWith(".py"));
1877
- void tsJsFiles;
1878
1828
  const fileContents = await Promise.all(
1879
1829
  files.map(async (file) => ({
1880
1830
  file,
1881
- content: await (0, import_core4.readFileContent)(file)
1831
+ content: await (0, import_core7.readFileContent)(file)
1882
1832
  }))
1883
1833
  );
1884
1834
  const graph = buildDependencyGraph(
@@ -1898,7 +1848,6 @@ async function analyzeContext(options) {
1898
1848
  contextBudget: metric.contextBudget,
1899
1849
  cohesionScore: metric.cohesion,
1900
1850
  fragmentationScore: 0,
1901
- // Python analyzer doesn't calculate fragmentation yet
1902
1851
  maxDepth,
1903
1852
  maxContextBudget,
1904
1853
  minCohesion,
@@ -1912,7 +1861,6 @@ async function analyzeContext(options) {
1912
1861
  tokenCost: Math.floor(
1913
1862
  metric.contextBudget / (1 + metric.imports.length || 1)
1914
1863
  ),
1915
- // Estimate
1916
1864
  linesOfCode: metric.metrics.linesOfCode,
1917
1865
  importDepth: metric.importDepth,
1918
1866
  dependencyCount: metric.imports.length,
@@ -1924,13 +1872,11 @@ async function analyzeContext(options) {
1924
1872
  ),
1925
1873
  cohesionScore: metric.cohesion,
1926
1874
  domains: ["python"],
1927
- // Generic for now
1928
1875
  exportCount: metric.exports.length,
1929
1876
  contextBudget: metric.contextBudget,
1930
1877
  fragmentationScore: 0,
1931
1878
  relatedFiles: [],
1932
1879
  fileClassification: "unknown",
1933
- // Python files not yet classified
1934
1880
  severity,
1935
1881
  issues,
1936
1882
  recommendations,
@@ -1965,7 +1911,7 @@ async function analyzeContext(options) {
1965
1911
  break;
1966
1912
  }
1967
1913
  }
1968
- const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
1914
+ const { issues } = analyzeIssues({
1969
1915
  file,
1970
1916
  importDepth,
1971
1917
  contextBudget,
@@ -1977,14 +1923,10 @@ async function analyzeContext(options) {
1977
1923
  maxFragmentation,
1978
1924
  circularDeps
1979
1925
  });
1980
- void severity;
1981
- void issues;
1982
- void recommendations;
1983
- void potentialSavings;
1984
1926
  const domains = [
1985
1927
  ...new Set(node.exports.map((e) => e.inferredDomain || "unknown"))
1986
1928
  ];
1987
- const fileClassification = classifyFile(node, cohesionScore, domains);
1929
+ const fileClassification = classifyFile(node);
1988
1930
  const adjustedCohesionScore = adjustCohesionForClassification(
1989
1931
  cohesionScore,
1990
1932
  fileClassification,
@@ -2009,7 +1951,6 @@ async function analyzeContext(options) {
2009
1951
  importDepth,
2010
1952
  contextBudget,
2011
1953
  cohesionScore: adjustedCohesionScore,
2012
- // Use adjusted cohesion
2013
1954
  fragmentationScore: adjustedFragmentationScore,
2014
1955
  maxDepth,
2015
1956
  maxContextBudget,
@@ -2026,7 +1967,6 @@ async function analyzeContext(options) {
2026
1967
  dependencyList,
2027
1968
  circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
2028
1969
  cohesionScore: adjustedCohesionScore,
2029
- // Report adjusted cohesion
2030
1970
  domains,
2031
1971
  exportCount: node.exports.length,
2032
1972
  contextBudget,
@@ -2043,269 +1983,57 @@ async function analyzeContext(options) {
2043
1983
  });
2044
1984
  }
2045
1985
  const allResults = [...results, ...pythonResults];
2046
- const sorted = allResults.sort((a, b) => {
1986
+ return allResults.sort((a, b) => {
2047
1987
  const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
2048
1988
  const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
2049
1989
  if (severityDiff !== 0) return severityDiff;
2050
1990
  return b.contextBudget - a.contextBudget;
2051
1991
  });
2052
- return sorted;
2053
- }
2054
- function generateSummary(results) {
2055
- if (results.length === 0) {
2056
- return {
2057
- totalFiles: 0,
2058
- totalTokens: 0,
2059
- avgContextBudget: 0,
2060
- maxContextBudget: 0,
2061
- avgImportDepth: 0,
2062
- maxImportDepth: 0,
2063
- deepFiles: [],
2064
- avgFragmentation: 0,
2065
- fragmentedModules: [],
2066
- avgCohesion: 0,
2067
- lowCohesionFiles: [],
2068
- criticalIssues: 0,
2069
- majorIssues: 0,
2070
- minorIssues: 0,
2071
- totalPotentialSavings: 0,
2072
- topExpensiveFiles: []
2073
- };
2074
- }
2075
- const totalFiles = results.length;
2076
- const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
2077
- const totalContextBudget = results.reduce(
2078
- (sum, r) => sum + r.contextBudget,
2079
- 0
2080
- );
2081
- const avgContextBudget = totalContextBudget / totalFiles;
2082
- const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
2083
- const avgImportDepth = results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
2084
- const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
2085
- const deepFiles = results.filter((r) => r.importDepth >= 5).map((r) => ({ file: r.file, depth: r.importDepth })).sort((a, b) => b.depth - a.depth).slice(0, 10);
2086
- const avgFragmentation = results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
2087
- const moduleMap = /* @__PURE__ */ new Map();
2088
- for (const result of results) {
2089
- for (const domain of result.domains) {
2090
- if (!moduleMap.has(domain)) {
2091
- moduleMap.set(domain, []);
2092
- }
2093
- moduleMap.get(domain).push(result);
2094
- }
2095
- }
2096
- const fragmentedModules = [];
2097
- for (const [domain, files] of moduleMap.entries()) {
2098
- let jaccard2 = function(a, b) {
2099
- const s1 = new Set(a || []);
2100
- const s2 = new Set(b || []);
2101
- if (s1.size === 0 && s2.size === 0) return 0;
2102
- const inter = new Set([...s1].filter((x) => s2.has(x)));
2103
- const uni = /* @__PURE__ */ new Set([...s1, ...s2]);
2104
- return uni.size === 0 ? 0 : inter.size / uni.size;
2105
- };
2106
- var jaccard = jaccard2;
2107
- if (files.length < 2) continue;
2108
- const fragmentationScore = files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
2109
- if (fragmentationScore < 0.3) continue;
2110
- const totalTokens2 = files.reduce((sum, f) => sum + f.tokenCost, 0);
2111
- const avgCohesion2 = files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
2112
- const targetFiles = Math.max(1, Math.ceil(files.length / 3));
2113
- const filePaths = files.map((f) => f.file);
2114
- const pathEntropy = calculatePathEntropy(filePaths);
2115
- const directoryDistance = calculateDirectoryDistance(filePaths);
2116
- let importSimTotal = 0;
2117
- let importPairs = 0;
2118
- for (let i = 0; i < files.length; i++) {
2119
- for (let j = i + 1; j < files.length; j++) {
2120
- importSimTotal += jaccard2(
2121
- files[i].dependencyList || [],
2122
- files[j].dependencyList || []
2123
- );
2124
- importPairs++;
2125
- }
2126
- }
2127
- const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
2128
- fragmentedModules.push({
2129
- domain,
2130
- files: files.map((f) => f.file),
2131
- totalTokens: totalTokens2,
2132
- fragmentationScore,
2133
- pathEntropy,
2134
- directoryDistance,
2135
- importCohesion,
2136
- avgCohesion: avgCohesion2,
2137
- suggestedStructure: {
2138
- targetFiles,
2139
- consolidationPlan: [
2140
- `Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s)`,
2141
- `Current token cost: ${totalTokens2.toLocaleString()}`,
2142
- `Estimated savings: ${Math.floor(totalTokens2 * 0.3).toLocaleString()} tokens (30%)`
2143
- ]
2144
- }
2145
- });
2146
- }
2147
- fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
2148
- const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
2149
- const lowCohesionFiles = results.filter((r) => r.cohesionScore < 0.6).map((r) => ({ file: r.file, score: r.cohesionScore })).sort((a, b) => a.score - b.score).slice(0, 10);
2150
- const criticalIssues = results.filter(
2151
- (r) => r.severity === "critical"
2152
- ).length;
2153
- const majorIssues = results.filter((r) => r.severity === "major").length;
2154
- const minorIssues = results.filter((r) => r.severity === "minor").length;
2155
- const totalPotentialSavings = results.reduce(
2156
- (sum, r) => sum + r.potentialSavings,
2157
- 0
2158
- );
2159
- const topExpensiveFiles = results.sort((a, b) => b.contextBudget - a.contextBudget).slice(0, 10).map((r) => ({
2160
- file: r.file,
2161
- contextBudget: r.contextBudget,
2162
- severity: r.severity
2163
- }));
2164
- return {
2165
- totalFiles,
2166
- totalTokens,
2167
- avgContextBudget,
2168
- maxContextBudget,
2169
- avgImportDepth,
2170
- maxImportDepth,
2171
- deepFiles,
2172
- avgFragmentation,
2173
- fragmentedModules: fragmentedModules.slice(0, 10),
2174
- avgCohesion,
2175
- lowCohesionFiles,
2176
- criticalIssues,
2177
- majorIssues,
2178
- minorIssues,
2179
- totalPotentialSavings,
2180
- topExpensiveFiles
2181
- };
2182
- }
2183
- function analyzeIssues(params) {
2184
- const {
2185
- file,
2186
- importDepth,
2187
- contextBudget,
2188
- cohesionScore,
2189
- fragmentationScore,
2190
- maxDepth,
2191
- maxContextBudget,
2192
- minCohesion,
2193
- maxFragmentation,
2194
- circularDeps
2195
- } = params;
2196
- const issues = [];
2197
- const recommendations = [];
2198
- let severity = "info";
2199
- let potentialSavings = 0;
2200
- if (circularDeps.length > 0) {
2201
- severity = "critical";
2202
- issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
2203
- recommendations.push(
2204
- "Break circular dependencies by extracting interfaces or using dependency injection"
2205
- );
2206
- potentialSavings += contextBudget * 0.2;
2207
- }
2208
- if (importDepth > maxDepth * 1.5) {
2209
- severity = severity === "critical" ? "critical" : "critical";
2210
- issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
2211
- recommendations.push("Flatten dependency tree or use facade pattern");
2212
- potentialSavings += contextBudget * 0.3;
2213
- } else if (importDepth > maxDepth) {
2214
- severity = severity === "critical" ? "critical" : "major";
2215
- issues.push(
2216
- `Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
2217
- );
2218
- recommendations.push("Consider reducing dependency depth");
2219
- potentialSavings += contextBudget * 0.15;
2220
- }
2221
- if (contextBudget > maxContextBudget * 1.5) {
2222
- severity = severity === "critical" ? "critical" : "critical";
2223
- issues.push(
2224
- `Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
2225
- );
2226
- recommendations.push(
2227
- "Split into smaller modules or reduce dependency tree"
2228
- );
2229
- potentialSavings += contextBudget * 0.4;
2230
- } else if (contextBudget > maxContextBudget) {
2231
- severity = severity === "critical" || severity === "major" ? severity : "major";
2232
- issues.push(
2233
- `Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
2234
- );
2235
- recommendations.push("Reduce file size or dependencies");
2236
- potentialSavings += contextBudget * 0.2;
2237
- }
2238
- if (cohesionScore < minCohesion * 0.5) {
2239
- severity = severity === "critical" ? "critical" : "major";
2240
- issues.push(
2241
- `Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
2242
- );
2243
- recommendations.push(
2244
- "Split file by domain - separate unrelated functionality"
2245
- );
2246
- potentialSavings += contextBudget * 0.25;
2247
- } else if (cohesionScore < minCohesion) {
2248
- severity = severity === "critical" || severity === "major" ? severity : "minor";
2249
- issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
2250
- recommendations.push("Consider grouping related exports together");
2251
- potentialSavings += contextBudget * 0.1;
2252
- }
2253
- if (fragmentationScore > maxFragmentation) {
2254
- severity = severity === "critical" || severity === "major" ? severity : "minor";
2255
- issues.push(
2256
- `High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
2257
- );
2258
- recommendations.push("Consolidate with related files in same domain");
2259
- potentialSavings += contextBudget * 0.3;
2260
- }
2261
- if (issues.length === 0) {
2262
- issues.push("No significant issues detected");
2263
- recommendations.push("File is well-structured for AI context usage");
2264
- }
2265
- if (isBuildArtifact(file)) {
2266
- issues.push("Detected build artifact (bundled/output file)");
2267
- recommendations.push(
2268
- "Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis"
2269
- );
2270
- severity = downgradeSeverity(severity);
2271
- potentialSavings = 0;
2272
- }
2273
- return {
2274
- severity,
2275
- issues,
2276
- recommendations,
2277
- potentialSavings: Math.floor(potentialSavings)
2278
- };
2279
- }
2280
- function isBuildArtifact(filePath) {
2281
- const lower = filePath.toLowerCase();
2282
- return lower.includes("/node_modules/") || lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/out/") || lower.includes("/output/") || lower.includes("/cdk.out/") || lower.includes("/.next/") || /\/asset\.[^/]+\//.test(lower);
2283
- }
2284
- function downgradeSeverity(s) {
2285
- switch (s) {
2286
- case "critical":
2287
- return "minor";
2288
- case "major":
2289
- return "minor";
2290
- case "minor":
2291
- return "info";
2292
- default:
2293
- return "info";
2294
- }
2295
1992
  }
2296
1993
  // Annotate the CommonJS export names for ESM import in node:
2297
1994
  0 && (module.exports = {
1995
+ adjustCohesionForClassification,
2298
1996
  adjustFragmentationForClassification,
2299
1997
  analyzeContext,
1998
+ analyzeIssues,
2300
1999
  buildCoUsageMatrix,
2000
+ buildDependencyGraph,
2301
2001
  buildTypeGraph,
2002
+ calculateCohesion,
2003
+ calculateContextBudget,
2302
2004
  calculateContextScore,
2005
+ calculateDirectoryDistance,
2303
2006
  calculateDomainConfidence,
2007
+ calculateEnhancedCohesion,
2008
+ calculateFragmentation,
2009
+ calculateImportDepth,
2010
+ calculatePathEntropy,
2011
+ calculateStructuralCohesionFromCoUsage,
2304
2012
  classifyFile,
2013
+ detectCircularDependencies,
2014
+ detectModuleClusters,
2015
+ extractDomainKeywordsFromPaths,
2016
+ extractExports,
2017
+ extractImportsFromContent,
2305
2018
  findConsolidationCandidates,
2306
2019
  findSemanticClusters,
2307
2020
  generateSummary,
2021
+ getClassificationRecommendations,
2308
2022
  getCoUsageData,
2023
+ getGeneralRecommendations,
2309
2024
  getSmartDefaults,
2310
- inferDomainFromSemantics
2025
+ getTransitiveDependencies,
2026
+ inferDomain,
2027
+ inferDomainFromSemantics,
2028
+ isBarrelExport,
2029
+ isConfigFile,
2030
+ isEmailTemplate,
2031
+ isLambdaHandler,
2032
+ isNextJsPage,
2033
+ isParserFile,
2034
+ isServiceFile,
2035
+ isSessionFile,
2036
+ isTypeDefinition,
2037
+ isUtilityModule,
2038
+ mapScoreToRating
2311
2039
  });