Skip to content

[Bug]: calculateCouplingScore divides by zero for empty modules — repository health renders as NaN #2500

Description

@DhruvalBhinsara1

Description

calculateCouplingScore in src/utils/complexityCalculator.ts divides by modules.length without guarding against an empty modules array. When a snapshot has zero modules, this produces 0 / 0 = NaN, which propagates through calculateRepositoryHealth and makes the repository health score NaN.

Every sibling scoring function in the same file guards the empty case — calculateModularityScore (if (modules.length === 0) return 0) and calculateCohesionScore (if (modules.length === 0) return 100) — but calculateCouplingScore does not.

Location

src/utils/complexityCalculator.ts, calculateCouplingScore (lines ~46–54):

export const calculateCouplingScore = (snapshot: ArchitectureSnapshot): number => {
  const { metrics, modules } = snapshot;

  const baseCoupling = Math.min((metrics.averageCoupling / 3) * 100, 100);
  const circularPenalty = metrics.circularDependencyCount * 5;
  const highDepModules = modules.filter((m) => m.dependencies.length > 5).length;
  const highDepPenalty = (highDepModules / modules.length) * 20; // ← 0/0 = NaN when modules is empty

  return Math.round(Math.min(100, baseCoupling + circularPenalty + highDepPenalty));
};

Why it's a bug

For an empty-modules snapshot:

  • highDepModules = 0, modules.length = 0highDepPenalty = (0 / 0) * 20 = NaN
  • coupling = Math.round(Math.min(100, finite + finite + NaN)) = NaN

Then in calculateRepositoryHealth:

const healthScore = Math.round(modularity*0.3 + (100 - coupling)*0.3 + cohesion*0.2 + (100 - complexity)*0.2);
health: Math.max(0, Math.min(100, healthScore)) // Math.max(0, NaN) === NaN

Math.max(0, NaN) returns NaN, so health is NaN.

Reproduction

const snapshot = {
  modules: [],
  metrics: { averageCoupling: 0, circularDependencyCount: 0, /* …other fields 0 */ },
  // …
};
calculateCouplingScore(snapshot);   // → NaN
calculateRepositoryHealth(snapshot).health; // → NaN

Impact

calculateRepositoryHealth is consumed by src/services/architectureDriftService.ts and rendered by src/components/repository/ArchitecturalDriftDetector.tsx (health score + status/color). A repository whose analysis yields zero modules (empty/tiny repo, or a parse that produced no modules) shows a broken NaN health value in the UI instead of a sensible number. The fact that the sibling functions all guard modules.length === 0 shows the empty case is an expected input that this function simply forgot to handle.

Proposed fix

Guard the division so an empty modules array contributes 0 high-dependency penalty:

const highDepPenalty =
  modules.length > 0 ? (highDepModules / modules.length) * 20 : 0;

I'll add a unit test for complexityCalculator (currently untested) covering the empty-modules case and a non-empty sanity check. I'd like to work on this.

/assign gssoc

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggssoc-2026Issue appears to be GSSoC-eligible.uiThis is a ui change

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions