Reading the results
How to read the numbers — and, just as important, what to ignore.
The hotspot score
score = rank(change frequency) × rank(complexity)A value from 0 to 1. Only files that are high on both rise to the top.
| Situation | Score | What it means |
|---|---|---|
| Changed often × complex | High | Highest return on effort. Start here. |
| Complex but nobody touches it | 0 | Fixing it returns nothing. Leave it alone. |
| Changed often but simple | Low | Not causing you problems. |
A file with no changes is always exactly 0
That is deliberate: it stops the tool from recommending code that is complex but stable. Cleaning up something that works and that nobody touches buys you nothing.
It is a rank, not an absolute
The score is a relative position within this repository. "Score 0.9" does not mean "dangerous"; it means "in the top 10% of this repo". It is not comparable across repositories.
Test files in the top ranks are normal
On real data, test files show up high. That is not a bug. Maintaining tests is real cost, and a test that breaks and gets fixed constantly is a genuine problem.
If you only want production code, filter the results.
Coupling degree
degree(A,B) = 2·|A∩B| / (|A|+|B|)A low degree with a high co-change count still matters. For two very active packages the denominator — their total number of changes — is large, which pushes the degree down.
| Pair | Degree | Co-changes | Reading |
|---|---|---|---|
| A ↔ B | 26% | 90 | Below the 30% default, but 90 shared commits is not something to ignore |
If nothing appears at the default threshold, lower it.
dowse coupling --hidden --min-degree 15Hidden coupling has two faces
kind: "hidden" only means "these change together and there is no edge between them in the dependency graph". Whether that is a problem depends entirely on what is behind it. Two real examples.
A real problem: duplicated code
@acme/admin-web ↔ @acme/doctor-web 33%, 91 times
src/lib/server-api.ts ↔ src/lib/server-api.ts 13
src/app/(authed)/page.tsx ↔ same path 15Identically named files on both sides, with the same edit copied by hand. A candidate for extraction into a shared package.
Not a problem: a deliberate protocol
@acme/api-contract ↔ @acme/audit-event-names 25%, 49 times
openapi.json ↔ audit-event-names/src/index.ts 34This was the convention "when you add an API, add its audit event name" — reasonable by design. Nothing to fix.
Always look at what is behind it
Do not conclude "they are coupled, so fix it" without running dowse why <a> <b>. Numerically, those two examples are the same "hidden coupling".
Code Health and the LoC baseline
Every health score is printed next to the score you would get from lines of code alone.
| File | Health | LoC baseline | Reading |
|---|---|---|---|
environment-guard.ts | 4.9 | 10.0 | Short but dense. Counting lines would miss it. |
big-but-simple.ts | 8.0 | 8.0 | No gap — the health score is just saying "this file is big" |
When the gap is small, that health score is file length in disguise. Do not read more into it than "this file is large".
Every threshold is a hypothesis
| Threshold | Default | Source |
|---|---|---|
| Cyclomatic warning | 10 | McCabe / ESLint |
| Cognitive warning | 15 | SonarSource |
| Minimum coupling degree | 30% | Code Maat |
| Minor contributor | < 5% | Bird et al. |
Each has a basis in the literature, but none of them is guaranteed to fit your repository. All are configurable. Measure where you are, then calibrate. See Why these metrics for where each number comes from.
Bus factor is about risk, not people
Bus factor 1 does not mean "this person is a bottleneck". It means the knowledge of this package sits with one person, and if they leave, it becomes unmaintainable.
Do not use it to evaluate individuals
Ranking developers by these numbers is explicitly discouraged. This is an indicator for a team to understand structural risk. Tornhill gives the same warning.
The calculation also depends on .mailmap merging identities and on bot commits being excluded. If one person commits under several addresses, dowse will overestimate the bus factor — the package will look safer than it is.
What has been excluded
Every run reports what it removed.
excluded: 6 merge, 0 bot, 1 format-only, 0 ignore-revs (51 large changesets flagged)| Kind | Why |
|---|---|
| Merge commits | They carry no change of their own |
| Bot commits | dependabot and friends distort ownership |
| Format-only | Whitespace-only edits inflate churn |
| ignore-revs | Honours .git-blame-ignore-revs |
| Large changesets (> 50 files) | Tangled commits fabricate coupling — excluded from coupling only, kept in churn |
Generated code (generated/, *.gen.ts) and lockfiles are out of scope by default.
Next
- Every formula — the definitions in full
- Trusting the data — why these exclusions exist