Code health is repowise's deepest differentiator. Linters check patterns. The health score predicts which files are likely to harbor the next bug, ranked and validated forward in time against a real defect corpus, and benchmarked head-to-head against the leading commercial tool in the space.
Repowise scores every file from 1 to 10 from deterministic markers computed over tree-sitter ASTs and git history, surfaced as three orthogonal signals: defect risk, maintainability, and performance. No LLM calls, no cloud requirement, no new runtime dependencies, just pure Python that finishes in under 30 seconds on a 3,000-file repo.
The markers
Each file starts at 10.0; marker findings deduct from the score, bounded per category so no single category can dominate. Code categories bend toward their bound instead of stopping at it, so shrinking a critical function raises the score even while it stays critical, and several findings on one function count mostly once. Several history findings on one file count the largest in full and the rest at a quarter.
Each code category's raw total R deducts A * (1 - exp(-R / A)), with A
at 3.09 times the category's cap. Findings on one function count the largest
in full and the rest at 0.25, and the score breakdown reports each category's
ceiling.
| Category | Markers |
|---|---|
| Structural complexity | nested_complexity, repeated_nesting, complex_conditional, complex_method, large_method, primitive_obsession. brain_method is advisory: listed, not scored. A rule naming the old id bumpy_road still applies |
| Cohesion & size | low_cohesion (LCOM4), god_class |
| Duplication | dry_violation (native Rabin-Karp clone detection) |
| Error handling | error_handling (empty catches, swallowed errors, bare panics) |
| Test coverage | untested_hotspot, coverage_gap, coverage_gradient |
| Test quality | large_assertion_block, duplicated_assertion_block deduct; assertion_free_test, mock_saturated_test are advisory markers that surface but never deduct |
| Organizational / git | developer_congestion, knowledge_loss (shown as fading ownership), function_hotspot, code_age_volatility, ownership_risk, churn_risk, change_entropy, co_change_scatter, prior_defect. hidden_coupling is also computed here but is advisory: it surfaces with no score effect |
| Performance | io_in_loop (N+1), string_concat_in_loop, blocking_sync_in_async, resource_construction_in_loop, serial_await_in_loop, lazy_load_in_loop, unbounded_read_reduced_in_memory, and more (see the performance signal) |
Five markers carry plain labels in reports and the web UI: nested_complexity
reads Deep nesting, large_method Long function, complex_conditional
Compound condition, developer_congestion Many authors and knowledge_loss
Fading ownership. Size markers read the judged CCN and code lines, so
large_method reports "N lines of code (M with blanks and comments)". Coverage
markers skip a file that holds only imports and re-exports, such as a package
__init__.py or an index.ts barrel.
How the detectors split across tiers
The registry holds 53 detectors (counting the three governance findings written by a separate additive pass, 56 marker ids in total). Each is permitted to affect only its own dimension:
| Group | Count | Scores into |
|---|---|---|
| Defect-scoring | 25 | defect (11 of them also maintainability) |
| Performance | 21 | performance only |
| SQL | 4 | maintainability or performance only |
| Advisory | 3 | nothing: assertion_free_test, mock_saturated_test, hidden_coupling surface but never deduct |
| Governance | 3 | nothing: the finding surfaces, the score is untouched |
The three governance markers (ungoverned_hotspot, stale_governance,
contradictory_decision) carry a category and a weight in the scoring
tables, but the pass that writes them runs after scoring completes and
never touches the stored score, so in practice they never deduct.
The three repo-level KPIs (all for the defect-risk signal):
- Hotspot Health. NLOC-weighted average over the files the git layer classifies as hotspots (high churn percentile plus minimum-activity floors).
- Average Health. NLOC-weighted average over all files.
- Worst Performer. The single lowest-scoring file.
While tests are part of the reading, repowise health names Production
code beside the Code health headline, and get_health kpis carry
production_average. A file with fewer than 5 recorded commits reports
history: "short", since its history deduction rests on little evidence.
Non-code files are never scored. No biomarker walks markdown, JSON, YAML or TOML, so scoring them would only record that nothing looked at them, and a repository could lift its average by adding documentation. They are left out of the population instead.
A code file in a language with no health walker is not analysed: it stores
no score, is left out of averages, hotspot health, bands and trends, and is
drawn grey on the map. get_health reports unanalysed_file_count. A headline
band such as "Excellent" is printed only when at least half of the code files
were analysed, and the headline says "analysed N of M code files" whenever some
were left out. Per-language coverage is on
Supported languages.
The bands
One band scale is used everywhere a score appears, in the CLI, over MCP, and on every page:
| Band | Score | Meaning |
|---|---|---|
| Excellent | 8.5 and above | Low-risk, maintainable |
| Good | 7.0 to 8.5 | Sound; nothing demanding attention |
| Fair | 5.5 to 7.0 | Rising complexity or process risk |
| Needs work | 4.0 to 5.5 | Worth scheduling |
| At risk | below 4.0 | High-risk; concentrates defects |
The bands are absolute, not percentile, so a score means the same thing on a private repo behind a firewall as it does against a public corpus. The 4.0 cutoff is the empirical one: on the 2,770-file measured corpus, files below it carry 16.9× the per-file defect rate of files at 8.0 and above (95% CI 8.6 to 29.0), and 2.18× the defects per KLOC once file size is normalized out (95% CI 1.00 to 3.58). The boundaries above 4.0 are reading points on the same scale rather than separate defect-rate claims.
8.0 is not a band edge. It survives separately as TARGET_SCORE, the score a
refactoring's leverage is measured against.
What a score counts
Two controls decide which files a figure describes and which half of the deduction it counts. Both are echoed on the response, so a number always states what it is.
| Control | Values | What it does |
|---|---|---|
| Scope | all (default), production | Which files are in the population |
| Counts | everything (default), code_shape | Whether the git-derived half of the deduction counts |
repowise health --scope production # exclude tests
repowise health --counts code_shape # the code-shape readingget_health(scope="production", counts="code_shape")The largest deduction cap is the organizational one, and it is git-derived:
churn, ownership, co-change, congestion and prior fixes describe what a file has
been through rather than what its code is like, and they rise as a file is
worked on. So a week of real refactoring can lower a file's structural deduction
while its history deduction climbs, leaving the headline flat. Every file stores
its deduction as two numbers, structure_deduction and history_deduction,
which sum to the total. History is also capped relative to structure: a
file with no code-shape finding at all keeps at least 9.0, so git history
alone can cost it no more than one point.
code_shape answers "is my code getting better". It subtracts
history_deduction and rescores from what is left. That is arithmetic over the
two stored columns, not a second scoring pass: history findings are dropped
rather than re-weighted, and a file with no stored split is reported in
unscored_files rather than counted as a ten.
production usually lowers the headline, which surprises people. Test files
generally score higher than the code they cover, so removing them removes the
easy end of the distribution. Nothing was found; the population changed.
everything stays the calibrated number every accuracy claim on this page is
about, and the health badge always reports it, so a projected reading cannot be
mistaken for the published one. A decline also names which half moved: when the
composite fell, history is the driver and history is the only half that moved
down, the trend says so and takes the neutral treatment history findings already
have rather than error red.
In CI: repowise health check
repowise health check origin/main...HEAD analyses the changed files at the
base and the head, with no index, and exits 1 only on a code-shape finding
the change wrote at high severity or above. A finding it made worse warns, and
one it only moved or renamed never fails. Git-history markers, coverage and
file scores never decide the gate. It also warns on an added suppression,
weakened tests or a test-only branch in production code. Flags:
repowise health check; setup:
Gates in CI.
Fix first
repowise health opens with Fix first: one ranked list across defect,
maintainability and performance, where each item is one unit of work (a
refactoring plan, one performance fix, or the strongest code-shape finding in
a file with no plan) with what to change, where, why, the first edit, the
expected gain, the effort and the tests to run.
Fix first (3 of 41 eligible items, 12 due; 230 in the inventory, tests, tooling and history-only files left out)
1. Split parse_config into 3 helpers now, effort M, +1.4 health
src/config/loader.py:212
CCN 47 and nesting 6; function changed in 19 commits.
verify: pytest tests/unit/test_loader.py- Order. Items rank by value first (how far the code sits past size and complexity bars), then by the complexity a fix removes times how widely the file is imported. Among items of equal value, a function many commits have changed comes first, and its reason names that history ("function changed in 38 commits").
- Stored plans lead. When an item's first step comes from a stored Extract Method plan, the item is titled "Split X into N helpers", with "(more stages needed)" when that plan alone leaves the function above the break-up bar. The dashboard card opens the plan.
- Tiers.
now(worth doing, safe to start),next(needs judgment),later(real but can wait, with the reason shown). - Performance and severity. A performance item joins Fix first and Do next only once its cost is confirmed. A critical code-shape marker ranks as high.
- What is left out, and counted. Test files, tooling, generated and
vendored code, docs (a root
docs/ordocs_src/tree, or one beside a docs-site config) and examples; files whose only findings come from git history; changes that recover under half a point; code a dead-code finding covers; functions a constant flag switches off.
Every surface reports the same counts (inventory, in_scope, eligible,
due, shown, excluded), stored at index time, so the dashboard,
get_health and repowise health --format json agree. repowise next prints
the cross-layer Do next list, which leads with the same item.
History markers are context
Git-history markers (ownership, churn, co-change and similar) describe what a
file has been through, not a defect in its code. On production files they
stay out of the default defect lists and are counted under history, while
scores, KPIs, get_risk and the Fix first order still use them. Findings on
vendored and generated files, and history markers on test files, are held
back at serving. include=["unverified"] shows all of them.
Preview languages
Every language with a health walker is served by default. Where a layer's
hand-judged precision on a language is still below the bar, its rows carry a
Preview label with the latest measurement: a badge with the measurement in
its tooltip on the web, a preview field in REST, one compact _meta.preview
entry in MCP and one line in the CLI.
| Layer | Preview on |
|---|---|
| Health findings | C++ |
| Refactoring plans | Java, C#, C, C++ |
| Performance opportunities | Java, C#, C, C++ |
| Dead code | Java, C#, C, C++, Kotlin, Swift |
Health findings on Java, C# and C ship without a label: each measured 92.5% or
more correct or debatable on a blind sample. Queue counts report rows held back
by language in a gated field. See
Findings and triage.
Calibrated, not hand-tuned
The marker weights are learned offline from a real defect corpus. Each file is scored at the commit immediately before a 6-month defect window (T0, so the measurement can't leak future information), and an L2-regularized logistic regression, with file size (NLOC) as an explicit control, fits each marker's defect lift beyond size. Only the learned constants ship; the runtime stays fully deterministic.
The strongest calibrated predictors: co_change_scatter,
change_entropy, ownership_risk, and nested_complexity.
Three health signals: defect risk, maintainability, performance
The score above is the defect-risk signal: calibrated against a defect corpus, with the bands above tuned to it (At risk files carry roughly 17× the defect rate of files at 8.0 and above). It is the overall number surfaced everywhere. But not every code smell predicts bugs, so repowise computes two co-equal companion signals from the same marker stream, never blended into the defect headline.
- Maintainability. A handful of markers fire widely and matter a
lot for how hard code is to read and change, yet proved weak as defect
predictors under leakage-free scoring, so the defect calibration floors
them (
low_cohesion,primitive_obsession,dry_violation,error_handling). Floored inside a defect score they get no credit for the real problem they describe. The maintainability signal deducts them at full weight against its own expert-set caps, so the smell is scored where it actually lives. Structural smells that are both defect predictors and core maintainability concerns (god_class,large_method,nested_complexity) count toward both. - Performance. Static performance risk (see below): high-precision, low-recall, always advisory, never folded into the defect number.
The signals are computed by one shared scoring kernel against independent
weight/category/cap tables and never feed back into each other. The
overall surfaced score stays exactly the defect score (a golden test locks
this byte-for-byte). Every finding carries a dimension
(defect / maintainability / performance) naming the pillar it homes
under, so findings can be filtered per signal, in the dashboard, the CLI,
and the get_health MCP tool.
Performance: static performance risk
The third signal flags shapes that waste work, code whose structure
does redundant I/O rather than measured runtime. It is deliberately
high-precision, low-recall: a few real findings the rest of the
toolchain can trust beat a wall of maybes. The headline detector is
io_in_loop (the N+1): a database call, network request, filesystem read,
or subprocess spawn that runs once per loop iteration. Two things make it
more than a file-local lint:
- Dependency classification. The loop-nested call is resolved through
a shared I/O-boundary classifier (
db/network/filesystem/subprocess/lock) and only fires on a classified execution sink (an actual round-trip), not a query-builder chain or a same-named pure helper. - Call-graph reachability. The loop and the I/O call need not be in the
same function. A bounded-depth (≤3 hops) walk over the resolved call graph
catches the interprocedural case no file-local linter can see;
cross-function findings carry their resolved
caller -> ... -> sinkpath.
Alongside it: string_concat_in_loop, blocking_sync_in_async,
resource_construction_in_loop, lock_in_loop, serial_await_in_loop,
membership_test_against_list_in_loop, nested_loop_with_io,
lazy_load_in_loop (an ORM relationship declared lazy and never eager-loaded
before the loop), unbounded_read_reduced_in_memory (a query result reduced
in Python instead of in the query), plus language-specific markers
(Go defer_in_loop / goroutine_in_unbounded_loop, Python
pd_concat_in_loop, JS/TS array_spread_in_reduce, and more).
Each finding also carries an actionability state: plan_ready (a proven,
safe transformation), advisory (a real finding whose fix needs validation,
or whose evidence confidence is too low to trust a proven fix), investigate
(evidence with no supported strategy yet), or expected (the repetition is
real and there is nothing to change, e.g. the loop is already batched).
Performance analysis ships as a self-contained dialect plugin per language:
11 dialects covering 16 language tags, including Python,
TypeScript/JavaScript, Java, Go, C#, Rust, Kotlin, Scala, Ruby, C++, and
Dart. A language without a dialect emits no perf findings (never a wrong
one).
On a 12k-file benchmark, repowise surfaced 557 I/O-in-loop findings, about 87 of which span function boundaries. The standard single-file linters (clippy, ruff PERF, ESLint, golangci-lint) found 0 of those cross-function cases. Following the call graph across files is the whole point: a file-local lint cannot see a loop in one function and its I/O sink in another. The clippy line of that comparison is catalogue-level, since a full end-to-end clippy run on the corpus was blocked by a Windows build wall rather than executed.
Performance is a static signal, so it under-reports rather than over-reports: dynamic dispatch, ORM lazy-load N+1, and unmodelled libraries are out of scope by design. That is why we call it performance risk.
Does the score predict real bugs?
Yes, validated across 21 open-source repositories spanning nine Full-tier languages (Python, TypeScript, JavaScript, Java, Kotlin, Go, Rust, C++, C#):
| Result | Value |
|---|---|
| Cross-project mean ROC AUC (21 repos, 9 languages) | 0.74 (mean 0.737) [95% CI 0.68 to 0.79] (0.55 to 0.86 across individual repos) |
| Beats recent-churn baseline | +0.10 AUC (DeLong p < 1e-9) |
| Beats prior-defect baseline | +0.12 AUC |
| External, never-seen dataset (PROMISE/jEdit) | AUC 0.76 to 0.78 |
Head-to-head vs the leading commercial tool
This is a different corpus from the cross-project number above: 2,770 shared files across 9 languages, scored at the same leakage-free commit against the same defect labels, with paired significance tests. The 0.731 ROC AUC below is on these 2,770 files; the 0.74 above is the cross-project mean over 21 repos. The two are measured on different file sets and are not the same number read twice.
| Axis | repowise | Leading commercial tool |
|---|---|---|
| Recall @ 20%-of-lines budget | 0.173 | 0.074 |
| Effort-aware ranking (Popt) | 0.607 | 0.462 |
| Defect density, size-normalized (defects/KLOC) | 2.18× | 0.56× |
| Discrimination (ROC AUC) | 0.731 | 0.705 |
The decisive wins here are the effort-aware ones, all paired and significant: ranking by repowise health surfaces 2.3× the defects under a fixed review budget (Popt Δ +0.145, recall Δ +0.099, density Δ, all at p = 0.003). The ROC AUC edge specifically is marginal (Δ +0.026, p = 0.054) and not significant, and precision@20% between the two tools is a tie (p = 0.64). The commercial tool is also the more mature product, with 28+ languages, a published Code Red defect study, and behavioral-analysis features repowise does not match.
Does the score find the bugs, on your repo?
After every index, repowise checks its own claim against the repo's
history: of the 20 least-healthy files, how many had a fix: commit in the
trailing ~180 days, versus the repo-wide base rate. It prints a one-line
callout (e.g. "16/20 lowest-health files had a recent bug fix, 3.3× the
24% baseline") and surfaces the same precision@K / lift stat on the web
dashboards and over MCP (get_health(include=["accuracy"])). It stays
silent on repos with too little history to be honest (fewer than 25 scored
files or fewer than 5 recently-fixed files), and discloses that
prior_defect is itself one down-weighted input, so this is an association
on indexed history, not a leakage-free forward prediction.
Full methodology, confidence intervals, and reproduction steps live in repowise-bench: the health-defect report and the head-to-head comparison.
Using it
repowise health # Fix first, KPIs, lowest-scoring files (stored, instant)
repowise next # the cross-layer Do next list
repowise coverage add cov.lcov # ingest LCOV/Cobertura/Clover -> untested-hotspot
repowise health --refactoring-targets # the stored refactoring queue
repowise health --recompute # analyze the working tree instead of the index
repowise health --trend # snapshots + declining / predicted-decline alertsCoverage lives behind its own command: repowise coverage add <report> ingests
it once, and every later repowise health folds those test gaps into the scores
automatically. When the report carries per-test contexts (a coverage.py
.coverage from pytest --cov=<package> --cov-context=test, or a per-test
LCOV), coverage add also builds a per-test
test-to-code map.
- The Code Health page. The map draws every file circle-packed by folder inside its module, dots in the five score bands, with zoom, pan and a Fit control; "Draw more" goes up to 8,000 files, with vendored and generated files behind a toggle. A file's drawer opens on its score, one plain sentence and the top three issues with their vital signs, and the trend chart has a hover and keyboard readout and a movers list.
- Three signals surfaced together. Defect risk is the headline;
maintainability and performance averages sit alongside it on the
dashboard and in
repowise status. The generatedCLAUDE.mdcarries the score and worst file, or every signal with thecode_healthsection. - Coverage ingestion. LCOV, Cobertura, Clover, or normalized JSON
light up the test-coverage markers. Each
coverage addretains the last 50 ingests, so repo-wide coverage has its own trend line alongside health. - Trend tracking. A rolling 50-row snapshot history powers
Declining HealthandPredicted Declinealerts, plus a history-drag alert when only the git-history half moved, so there is nothing in the code to act on. A repository with no hotspot files reports hotspot health as null, and the trend says so. - Documentation drift. Stale docs are scored by the same pass and travel with the health snapshot history; see Documentation drift for what it checks.
- Refactoring targets. Deterministic, rule-based, ranked by impact / effort. A health score says a file is in trouble; refactoring intelligence names the specific fix: six detectors (Extract Class, Extract Helper, Move Method, Break Cycle, Split File, Extract Method) that emit one structured, graph-aware plan per opportunity. Code generation is off by default and runs only when you ask for it.
- Per-repo policy.
.repowise/health-rules.jsondisables markers per glob and remaps severities (including a namedsmall-teamprofile); the calibrated weights stay locked so the benchmark claims hold.
Your agent reaches the same data through the
get_health MCP tool, and a single-line summary
shows up in repowise status. Full CLI reference:
repowise health.
How it connects to the other layers
Code health isn't a silo; it reuses signals from every other layer:
- Git feeds the organizational markers (ownership, churn, co-change scatter, fading ownership).
- Graph feeds centrality (a
brain_methodmust be central, not just long),hidden_coupling, and the call-graph reachability behind the performance N+1 detector. - Decisions surface as
ungoverned_hotspotandstale_governancehealth findings.
That's why a repowise health score carries more signal than a complexity linter's. A linter sees that a file is complex. The health score sees that the file is complex, central, churned by many hands, and untested, and weighs all four together.