How a 25-Marker Code Health Scorer Works

repowise teamUpdated 9 min read
Compared in this post: repowise and
  • CodeScene
  • SonarQube
On this page

repowise calculates code health by running 25 deterministic markers over every file, deducting each finding's severity times its marker weight from a starting 10, capping the deduction per category, and clamping the result to a 1 to 10 score. The weights are fitted against a defect corpus, so a marker earns its place only if it predicts bug fixes beyond file size. Nothing in the pipeline calls a model.

This post walks through the whole path: markers, weights and caps, bands, module rollups, trends, and the benchmark that checks the score against real bug fixes. For the concept first, read what code health is.

What goes into a code health score?

A file's score comes from findings, the observations the markers produce from the parse tree, the dependency graph and git history. repowise reads all three in one pass during repowise init and repowise update, and the analysis is budgeted at under 30 seconds for 3,000 files, enforced by a test.

The same findings feed three scores that are never blended, because they answer different questions:

SignalWhat it answersHow its weights are set
Code health (defect risk)Which files are most likely to carry the next bugFitted against a defect corpus
MaintainabilityHow hard the code is to read and changeSet by hand, never fitted to bugs
Performance riskWhere the structure wastes workPer-marker precision weights

Scroll the table sideways to see every column.

Of the 53 registered detectors, 25 move the defect score, 14 feed maintainability, and 22 feed performance. Three advisory markers are listed but carry zero impact, and three governance findings from the decisions layer never deduct.

The 25 markers, by family

The defect markers fall into families, and each family has a cap so no single kind of problem can dominate a file. Every marker is stateless, measurable and explainable on its own: a low score always arrives with the findings that caused it.

FamilyMarkersCategory capWhat it catches
Structural complexitybrain method, low cohesion, god class, nested complexity, bumpy road, complex conditional2.5Logic that is deep, tangled or concentrated
Size and complexitycomplex method, large method, primitive obsession1.5Long or branchy functions, raw parameters that want to be one object
DuplicationDRY violation1.0Cloned blocks, ranked by how often the copies change together
Error handlingswallowed catches, catch-alls, unwrap, discarded Go errors0.5Failures that vanish instead of surfacing
Change historychurn risk, change entropy, co-change scatter, function hotspot, code age volatility, ownership risk, developer congestion, knowledge loss, prior defectup to 3.5How the file is changed and by whom
Test coverageuntested hotspot, coverage gap, coverage gradient2.0 eachCentral code with no tests (coverage markers need a report)
Test qualitylarge assertion block, duplicated assertion block0.5Tests where a failure points at a line, not a cause

Scroll the table sideways to see every column.

Three design choices make this list different from a lint pass.

Class-level and function-level smells are measured on the AST. Cyclomatic complexity, nesting depth, cognitive complexity and parameter counts come from a tree-sitter walk. Low cohesion uses LCOM4, the number of connected groups of methods that share fields or call each other. A class that splits into two or more groups is doing two jobs.

Centrality gates the brain-method marker. A long, branchy function is a problem everywhere, but a worse one in a file many others import. The marker fires when the file sits in the top fifth of connected files in that repository, or clears an absolute hub bar, so it keeps working on sparse-graph languages such as TypeScript barrels and Rust.

Dispatch is not nesting. A function that is mostly one switch or match on one value is judged by the code around the dispatch and its heaviest arm, and a TypeScript view that chooses one of ten panels is one decision per arm. This keeps routers and renderers from reading as complexity smells.

How much weight does git history get?

History amplifies code risk; it does not stand alone. Git history costs a file at most 1.0 point by itself, plus one point for each point its code-shape findings deduct, up to 3.5. A simple file that changes often reads as busy, not broken, and never drops below 9.0 from history alone.

The change-history markers come from the research literature: relative churn (Nagappan and Ball), the change-entropy metric (Hassan), ownership dispersion (Bird and colleagues) and co-change breadth (D'Ambros). The people markers adjust to team size: on a small team, knowledge_loss and ownership_risk are held to low severity unless the file is a hotspot, and the small-team profile lowers them further.

Which markers are down-weighted, and which never deduct?

Smells that fire often but predict bugs weakly (low cohesion, brain methods, duplicated code, broad error handling) are down-weighted in the defect score and counted in full in maintainability. They still tell you what is hard to change, which is a different question from what is likely to break.

Hidden coupling, files that co-change without importing each other, is still detected and listed but no longer deducts: on its own it ranked defect-prone files near chance, and a pre-registered test on 12 repositories found the score without it non-inferior.

How is a file score calculated?

Each file starts at 10. Every finding deducts its severity times its marker weight, each category is capped, and the total is clamped to 1 to 10. Bands are absolute, so a 6.2 means the same in every repository.

text
file_health = clamp(10 - sum over categories of min(cap, sum of severity x weight), 1, 10)
BandScoreMeaning
Excellent8.5 and aboveLow risk, easy to change
Good7.0 to 8.5Sound; nothing demands attention
Fair5.5 to 7.0Rising complexity or process risk
Needs work4.0 to 5.5Worth scheduling
At riskunder 4.0Where defects concentrate

Scroll the table sideways to see every column.

Two details keep the number honest. Coverage markers stay silent when no coverage report was ingested: absent coverage is never imputed as uncovered. And a file in an unsupported language is counted as not analysed, never scored as a 10.

The coverage gradient shows the shape of a continuous marker. Binary gates fire only below roughly 40 to 60 percent coverage, so on a well-tested codebase they go blind. The gradient deducts in proportion to the uncovered fraction (4.0 * (1 - line_coverage_pct / 100) points, bounded by its cap), so a file at 85 percent coverage still scores a little below one at 99.

How does a file score roll up into a module and a repository?

Repository figures are code-line-weighted, so a 2,000-line file moves them more than a 40-line helper. repowise reports three KPIs: average health, hotspot health (hotspot files only) and the worst performer. Modules come from the graph's community detection, so a module is a real cluster of connected code rather than a folder.

Two controls decide what a figure describes, and every response echoes them:

  • scope: all (default) or production, which drops test files and lowers the headline because tests usually score higher.
  • counts: everything (default) or code_shape, which removes the git-history half of each deduction so you can check whether a refactor improved the code even though it raised churn.
bash
repowise health                 # KPIs, Fix first, lowest-scoring files
repowise health --trend         # the last 10 snapshots
repowise health --refactoring-targets

Every init, update and health --recompute stores a snapshot, the last 50 per repository, with the three KPIs and a score per file. Alerts are rule-based and explainable:

AlertFires when
DecliningThe score fell 0.5 or more against five snapshots back
Predicted declineThree falls in a row
History dragEither of the above, but only the git-history half moved, so there is nothing in the code to act on

Scroll the table sideways to see every column.

History drag is the useful one. It tells a reviewer that a score fell because a file got busier, not because anyone wrote worse code, so no one hunts for a regression that is not there.

Does the score predict bugs?

Yes, within limits we measure and publish. We scored each file at a historical commit, then counted bug fixes over the following six months. Nothing after the scoring commit feeds the score.

  • Discrimination: ROC AUC 0.74 across 21 repositories, 9 languages and 2,826 files, and 0.76 to 0.78 on PROMISE/jEdit, a dataset the weights never saw.
  • Against simple baselines: a margin of +0.10 AUC over ranking by recent churn.
  • Against CodeScene (paired, same 2,770 files, same labels): 2.3x the defects under a fixed 20%-of-lines review budget, with effort-aware ranking (Popt) of 0.607.

The score is built to order a fixed review budget and to say why, not to be a bug oracle. Lines of code alone discriminate about as well, so the gain is in ranking and in the explanation attached to each file. The full method, with confidence intervals, is on the benchmarks page.

How does this compare with SonarQube and CodeScene?

The three tools answer different questions, which is why teams often run more than one.

SonarQubeCodeScenerepowise
Central questionHow much remediation effort sits behind the issues?How healthy is this code, with hotspots for priority?Which files carry the next bug, and what to fix first?
OutputDebt ratio, ratings, quality gatesCode Health score and hotspots1 to 10 file score, three separate signals, Fix first queue
WeightsRule-basedProprietaryPublished, fitted against a defect corpus

Scroll the table sideways to see every column.

As of October 2026, SonarQube documents maintainability through technical debt and remediation effort (SonarQube docs), and CodeScene describes Code Health as an aggregated metric used with hotspot analysis (CodeScene). For a direct comparison, see repowise vs CodeScene.

What did we leave out on purpose?

Signals that are easy to measure but hard to defend stay out of the score.

  • Style-only lint rules. Formatting is automatable and says little about long-term cost.
  • Commit message quality. A process signal, not a code signal.
  • Subjective labels. "Messy" cannot be audited or tuned.
  • Raw lines changed in one PR. What matters is where the change lands and how often that area already moves; that is the job of get_change_risk.
  • Repository popularity. Stars and forks describe a project, not a file.

How to run it on your repository

bash
pip install repowise
repowise init        # full index; scores every file, no LLM key needed
repowise health      # Fix first, KPIs, lowest-scoring files
repowise next        # the short "Do next" list across every layer

Over MCP, get_health() returns the same KPIs, and get_health(targets=["src/api/server.py"]) returns one file's findings. It is one of repowise's ten flagship MCP tools, so an agent can check a file's health before editing it. To tune per-file rules, put a profile, disabled_biomarkers or severity_overrides in .repowise/health-rules.json; only severity labels are tunable, because the weights and caps are the calibrated constants the published accuracy rests on. See the live output on the FastAPI code health page.

FAQ

How is code health calculated?
Each file starts at 10. Every finding deducts its severity times its marker weight, deductions are capped per category, and the result is clamped to 1 to 10. repowise fits the weights against a defect corpus and reports defect risk, maintainability and performance risk as three separate scores.
What are the 25 markers in a code health scorer?
They span structural complexity (brain method, low cohesion, god class, nested complexity, bumpy road, complex conditional), size (complex method, large method, primitive obsession), duplication, error handling, change history (churn risk, change entropy, co-change scatter, function hotspot, code age volatility, ownership risk, developer congestion, knowledge loss, prior defect), test coverage and test quality. Each is measurable and explainable on its own.
Why not use only cyclomatic complexity?
Because one metric misses too much. A file with moderate complexity can still be high risk if it changes often, sits at the center of the graph and has weak tests. Complexity is one family of markers among several.
Does a high change count lower the score?
Only a little on its own. Git history costs a file at most 1.0 point, and a simple file that changes often never drops below 9.0 from history alone. History amplifies code-shape findings; it does not replace them.
How does a module score differ from a file score?
A file score covers one unit. Repository and module figures are weighted by code lines, and modules come from community detection on the dependency graph, so a cluster of connected code gets its own roll-up.
Does it need an LLM or an API key?
No. Every marker is parse-tree, git or coverage math, and the analysis runs locally inside repowise init and repowise update.
Is it open source?
Yes. repowise is AGPL-3.0, so the markers, weights and caps are inspectable. The AGPL keeps the code available when it runs as a network service.