All files cluster-detector.ts

100% Statements 43/43
77.27% Branches 17/22
100% Functions 6/6
100% Lines 38/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101                    6x   6x 14x 14x 7x   14x     6x   6x 7x   5x 12x 12x       5x 5x 5x 12x   5x 5x   5x 7x 7x 7x     7x     5x           5x 5x 12x 12x   5x   5x                           6x               5x 5x   5x 3x         5x 1x         5x    
import type { DependencyGraph, ModuleCluster } from './types';
import { calculateFragmentation, calculateEnhancedCohesion } from './metrics';
 
/**
 * Group files by domain to detect module clusters
 */
export function detectModuleClusters(
  graph: DependencyGraph,
  options?: { useLogScale?: boolean }
): ModuleCluster[] {
  const domainMap = new Map<string, string[]>();
 
  for (const [file, node] of graph.nodes.entries()) {
    const primaryDomain = node.exports[0]?.inferredDomain || 'unknown';
    if (!domainMap.has(primaryDomain)) {
      domainMap.set(primaryDomain, []);
    }
    domainMap.get(primaryDomain)!.push(file);
  }
 
  const clusters: ModuleCluster[] = [];
 
  for (const [domain, files] of domainMap.entries()) {
    if (files.length < 2 || domain === 'unknown') continue;
 
    const totalTokens = files.reduce((sum, file) => {
      const node = graph.nodes.get(file);
      return sum + (node?.tokenCost || 0);
    }, 0);
 
    // Calculate shared import ratio for coupling discount
    let sharedImportRatio = 0;
    Eif (files.length >= 2) {
      const allImportSets = files.map(
        (f) => new Set(graph.nodes.get(f)?.imports || [])
      );
      let intersection = new Set(allImportSets[0]);
      let union = new Set(allImportSets[0]);
 
      for (let i = 1; i < allImportSets.length; i++) {
        const nextSet = allImportSets[i];
        intersection = new Set([...intersection].filter((x) => nextSet.has(x)));
        for (const x of nextSet) union.add(x);
      }
 
      sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
    }
 
    const fragmentation = calculateFragmentation(files, domain, {
      ...options,
      sharedImportRatio,
    });
 
    // Average cohesion for the cluster
    let totalCohesion = 0;
    files.forEach((f) => {
      const node = graph.nodes.get(f);
      Eif (node) totalCohesion += calculateEnhancedCohesion(node.exports);
    });
    const avgCohesion = totalCohesion / files.length;
 
    clusters.push({
      domain,
      files,
      totalTokens,
      fragmentationScore: fragmentation,
      avgCohesion,
      suggestedStructure: generateSuggestedStructure(
        files,
        totalTokens,
        fragmentation
      ),
    });
  }
 
  return clusters;
}
 
function generateSuggestedStructure(
  files: string[],
  tokens: number,
  fragmentation: number
) {
  const targetFiles = Math.max(1, Math.ceil(tokens / 10000));
  const plan: string[] = [];
 
  if (fragmentation > 0.5) {
    plan.push(
      `Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
    );
  }
 
  if (tokens > 20000) {
    plan.push(
      `Domain logic is very large (${Math.round(tokens / 1000)}k tokens). Ensure clear sub-domain boundaries.`
    );
  }
 
  return { targetFiles, consolidationPlan: plan };
}