# All files / src cluster-detector.ts

97.95% Statements 48/49
72.72% Branches 16/22
100% Functions 6/6
97.77% Lines 44/45

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
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122

    11x

    11x
    19x
    19x
    11x

    19x

    11x

    11x

    5x
    5x

    5x

    5x
    1x

    5x

    11x
    11x

    5x
    12x
    12x

    5x
    5x
    5x
    12x

    5x
    5x

    5x
    7x
    7x
    7x

    7x

    5x

    5x
    5x

    5x
    12x
    12x
    12x
    12x

    12x
    12x

    5x
    5x

    5x

    11x

     import type { DependencyGraph, ModuleCluster } from './types';
    import { calculateFragmentation, calculateEnhancedCohesion } from './metrics';
    import {
      classifyFile,
      adjustFragmentationForClassification,
    } from './classifier';

    /**
     * Group files by domain to detect module clusters
     * @param graph - The dependency graph to analyze
     * @param options - Optional configuration options
     * @param options.useLogScale - Whether to use logarithmic scaling for calculations
     * @returns Array of 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[] = [];

      const generateSuggestedStructure = (
        files: string[],
        tokens: number,
        fragmentation: number
      ) => {
        const targetFiles = Math.max(1, Math.ceil(tokens / 10000));
        const plan: string[] = [];

        Iif (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 };
      };

      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]);
          const 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 rawFragmentation = calculateFragmentation(files, domain, {
          ...options,
          sharedImportRatio,
        });

        // Average cohesion and adjusted fragmentation for the cluster
        let totalCohesion = 0;
        let totalAdjustedFragmentation = 0;

        files.forEach((f) => {
          const node = graph.nodes.get(f);
          Eif (node) {
            const cohesion = calculateEnhancedCohesion(node.exports);
            totalCohesion += cohesion;

            const classification = classifyFile(node, cohesion);
            totalAdjustedFragmentation += adjustFragmentationForClassification(
              rawFragmentation,
              classification
            );
          }
        });

        const avgCohesion = totalCohesion / files.length;
        const fragmentationScore = totalAdjustedFragmentation / files.length;

        clusters.push({
          domain,
          files,
          totalTokens,
          fragmentationScore,
          avgCohesion,
          suggestedStructure: generateSuggestedStructure(
            files,
            totalTokens,
            fragmentationScore
          ),
        });
      }

      return clusters;
    }