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
| Metric | Definition |
|---|---|
| Effective LoC | Lines excluding comments and blanks. The default proxy. |
| Cyclomatic | One per decision point (as in ESLint complexity): if, loops, case, catch, ternary. |
| Cognitive | Penalises 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:
| Item | Default | Source |
|---|---|---|
| Minimum revisions | 5 | Code Maat |
| Minimum shared commits | 5 | Code Maat |
| Minimum degree | 30% | Code Maat |
| Large changeset exclusion | > 50 files | CodeScene |
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.
| kind | Meaning |
|---|---|
hidden | No edge in the dependency graph — coupling not visible in the structure |
type-only | Connected only through types, no value dependency |
declared-only | Declared in package.json but never imported |
obvious | There 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| Verdict | Condition |
|---|---|
identical | Content is byte-identical (file move, mode change) |
format-only | AST matches including names (whitespace, comments, semicolons) |
rename-only | Structural hash and node count match (identifiers were renamed) |
changed | Anything else |
unknown | Parse failure, or fewer than 5 nodes |
effective_commits(f) = |changed| + |unknown| + 1A 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:
| Smell | Threshold | Weight | Rationale |
|---|---|---|---|
| High Cognitive Complexity | cognitive > 15 | 3.0 | The most direct proxy for cost of understanding |
| Complex Method | cyclomatic > 10 | 2.5 | Paths a test suite must cover (McCabe) |
| Large File | LoC > 400 | 1.5 | An indirect signal, so weighted lightly |
| Many Arguments | args > 4 | 1.0 | Coupling 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.293Greedily 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:
| Target | Why |
|---|---|
| Merge commits | They carry no change of their own |
| Bot commits | They distort ownership |
| Format-only commits | Whitespace-only edits inflate churn |
.git-blame-ignore-revs entries | A standard GitHub also honours |
| Generated code and lockfiles | The canonical source of false churn |
| Large changesets | Tangled commits — excluded from coupling only |
The exclusion counts are always printed. Excluding silently would let you read the output as "everything, analysed".