Skip to content

Every formula

Nothing about how dowsing scores your code is hidden. This page is the complete list. For where each number comes from, see Why these metrics.

Every threshold is a hypothesis

They are grounded in the literature, but none of them is guaranteed to fit your repository. All are configurable.

Hotspot

change_frequency(f) = commits in the window that changed f (after removing merges/bots/format-only)
complexity(f)       = the chosen complexity proxy (default: effective LoC)
hotspot_score(f)    = rank(change_frequency) × rank(complexity)

rank is percentile normalisation to 0–1. Only files high on both rise to the top. The min-max normalisation is kept alongside it in the output.

A file that was never changed scores exactly 0, however complex it is — code nobody touches returns nothing on investment.

Aggregation to packages is reported both ways.

hotspot_sum(pkg)          = Σ hotspot_score(f)
hotspot_loc_weighted(pkg) = Σ(hotspot_score(f) × loc(f)) / Σ loc(f)

Complexity

MetricDefinition
Effective LoCLines excluding comments and blanks. The default proxy.
CyclomaticOne per decision point (as in ESLint complexity): if, loops, case, catch, ternary.
CognitivePenalises nesting (Campbell / SonarSource). A chain of logical operators counts once, not per operator.

Switch with --complexity=loc|cyclomatic|cognitive.

Change coupling

The symmetric Tornhill / Code Maat formulation is the default.

degree(A,B)      = 2·|A∩B| / (|A|+|B|)     symmetric coupling degree
confidence(A⇒B)  = |A∩B| / |A|             directional (optional)

Default filters:

ItemDefaultSource
Minimum revisions5Code Maat
Minimum shared commits5Code Maat
Minimum degree30%Code Maat
Large changeset exclusion> 50 filesCodeScene

Large changesets are excluded because a tangled commit — several concerns in one commit — fabricates coupling. They are still counted in churn, just not in coupling.

The denominator of degree

|A| is the number of all revisions that touched A, including commits where A changed alone. Dropping those shrinks the denominator and overestimates the degree. This was a real bug during implementation.

Hidden coupling

Coupling is classified against the package dependency graph.

kindMeaning
hiddenNo edge in the dependency graph — coupling not visible in the structure
type-onlyConnected only through types, no value dependency
declared-onlyDeclared in package.json but never imported
obviousThere is a dependency, so co-change is expected

The dependency graph carries both the declared dependencies from package.json and the actual imports extracted by oxc.

Churn verification

The AST is hashed into a normalised fingerprint and compared across revisions.

fingerprint(src) = hash(AST structure with only identifier name/value removed)
                   operators, literal values, and modifiers carry meaning, so they stay in
VerdictCondition
identicalContent is byte-identical (file move, mode change)
format-onlyAST matches including names (whitespace, comments, semicolons)
rename-onlyStructural hash and node count match (identifiers were renamed)
changedAnything else
unknownParse failure, or fewer than 5 nodes
effective_commits(f) = |changed| + |unknown| + 1

A deliberate asymmetry

unknown counts as a real change. Wrongly discarding an unclassifiable change as "false churn" (a false negative) is more dangerous than slightly overstating churn (a false positive).

No type information is required — comparing syntax ASTs is enough.

Code Health

health(f)   = 10 − Σ_smell (weight × severity)      floor 1.0
health(pkg) = LoC-weighted mean

severity(value, threshold) = min(1, (value − threshold) / (threshold × 2))

Severity is 0 exactly at the threshold and saturates at 1 at three times the threshold. That keeps a single outlier from pinning the score.

Implemented smells and default weights:

SmellThresholdWeightRationale
High Cognitive Complexitycognitive > 153.0The most direct proxy for cost of understanding
Complex Methodcyclomatic > 102.5Paths a test suite must cover (McCabe)
Large FileLoC > 4001.5An indirect signal, so weighted lightly
Many Argumentsargs > 41.0Coupling to every caller

The weights sum to 8.0, so with the defaults the worst possible health is 2.0. Adding smells lowers that floor.

The LoC baseline is always printed next to the score. If the gap is small, that health score is file length in disguise.

Ownership and bus factor

The main developer is determined by added lines over the full history. git blame would credit whoever touched a line last, which is a much weaker signal.

minor contributor = a contributor with ownership < 5%   (Bird et al.)

Bus factor follows Avelino et al. (ICPC 2016): Degree-of-Authorship plus a greedy removal. The coefficients come from Fritz et al. (ICSE 2010).

DOA(d,f) = 3.293 + 1.098·FA + 0.164·DL − 0.321·ln(1 + AC)
  FA = first authorship (1 if d created f)
  DL = number of changes d made to f
  AC = number of changes to f by everyone else

DOA_N(d,f) = DOA(d,f) / max_d' DOA(d',f)
author(d,f) ⇔ DOA_N > 0.75 and DOA ≥ 3.293

Greedily remove the author who owns the most files, repeatedly. The bus factor is the number removed at the point where fewer than 50% of files still have an author.

dowsing computes this per package. Existing tools report it for the whole repository or per file, neither of which tells you which part of the codebase becomes unmaintainable.

Prerequisites

Identity merging via .mailmap and bot removal must happen first. If one person commits under several addresses, the bus factor is overestimated — the package looks safer than it is. dowsing does both while collecting the git history.

What is excluded

Covered in full in Trusting the data. In short:

TargetWhy
Merge commitsThey carry no change of their own
Bot commitsThey distort ownership
Format-only commitsWhitespace-only edits inflate churn
.git-blame-ignore-revs entriesA standard GitHub also honours
Generated code and lockfilesThe canonical source of false churn
Large changesetsTangled commits — excluded from coupling only

The exclusion counts are always printed. Excluding silently would let you read the output as "everything, analysed".

Released under the MIT License