Code health scores files. Change risk scores
a change: a commit or a base..head range, from the shape of its diff.
It is a just-in-time / pre-merge signal, complementary to health, and useful
as a PR gate because it fires on risky small changes a file-level score
misses entirely.
Scoring a change
repowise risk # score uncommitted work, or HEAD when the tree is clean
repowise risk abc123 # score a single commit
repowise risk main..HEAD # score a branch / PR range as one change
repowise risk main...HEAD # from the merge-base: what the pull request changed
repowise risk main..HEAD --ext .py # count only .py files
repowise risk main..HEAD -x 'tests/' -x '*.spec.ts' # omit matching paths
repowise risk --format json # machine-readableIt runs in-process: pure git plus learned constants. No LLM, no network,
and no blame at runtime. SZZ labelling lives entirely in the offline
calibration that produced the constants, not in anything that runs when you
call it.
To gate a pull request on it, repowise risk --fail-above-percentile 95
fails a change that ranks above the 95th percentile of this repository's
recent commits, never on the raw score. At that setting about 5% of changes
fail by construction, so a failure asks for a split or a second reviewer,
not a code fix. See Gates in CI.
Excluding paths
Use repeatable --exclude / -x flags with gitignore-style patterns to omit
files from a score. The same filters apply to the requested change and to the
recent commits sampled for its percentile, so the comparison stays like for
like. Put project-wide, risk-only rules in a repository-root .riskignore;
those patterns apply automatically and combine with any command-line flags.
For example, tests/ excludes that directory recursively, while test_*.py
excludes matching test filenames anywhere in the repository.
What it measures
The model uses Kamei-style change metrics (Kamei et al., "A large-scale empirical study of just-in-time quality assurance"):
| Feature | Meaning |
|---|---|
la, ld | lines added / deleted |
nf | files touched |
nd, ns | distinct directories / top-level subsystems touched |
entropy | Shannon entropy of the per-file churn distribution (diffusion) |
exp | author's prior commit count (experience); unknown is scored neutrally |
These are properties of the diff, so the score is a change-level signal
rather than a file-size proxy. The risk is a plain L2-logistic over
standardized, log-compressed features (logit = intercept + Σ coefᵢ·zᵢ), so
every feature's push on the risk is exact and reported as an attributable
driver, the same linear / per-finding-attributable contract the file health
score holds.
How to read the result
The headline signal is repo-relative. The raw 0-10 logistic probability is anchored to the offline calibration corpus, so on a repo whose typical commit is large the diff-size term dominates and the absolute band skews high: two-thirds of commits can read "high" while ranking perfectly normally for that repo. The ranking is sound; the absolute band is not portable.
So the surfaces lead with where the change sits in its own repo's distribution:
- Review priority:
Below typical/Typical/Elevated(terciles of the repo's own commit-risk distribution). This is the signal to triage on. - Percentile: "riskier than N% of this repo's commits".
- Raw model score (0-10): kept for transparency but shown as a secondary, clearly corpus-anchored number, not the thing to act on.
Each driver is reported relative to the model's baseline commit (the
calibration-corpus mean), not this repo, so a +19 / -1 change can
legitimately read "more lines added than baseline" while still ranking
Below typical for a repo of large commits. The signed contribution and
color (red raised the raw score, green lowered it) carry the direction; the
label only states the feature's standing, never an absolute verdict.
The repowise risk CLI samples the repo's recent commits live (--baseline,
default 200) to compute this percentile; in the web UI it is precomputed from
the indexed commit history.
Calibration and accuracy
Constants are learned offline against the defect corpus (AG-SZZ bug-inducing commits as labels, time-ordered evaluation with a right-censoring gap, and a leave-one-repo-out comparison to the churn-only baseline). On a 7-repo, 5-language slice the pooled leave-one-repo-out AUC is 0.772 vs 0.766 for churn-only (Δ +0.0068, 95% CI [-0.0003, +0.0131]): competitive with churn across the corpus and stronger on some repos (clap +0.053 on a time-ordered split). Diff size dominates the fit, with change entropy risky and author experience protective, both literature-consistent. Only the learned constants ship; the runtime stays deterministic and zero-LLM.
Distinct from get_risk
repowise risk and get_change_risk score a
change (diff shape, no index needed; get_change_risk adds the health
delta, tests and coverage from the index when it has one). get_risk
scores indexed files by path, and in PR mode (changed_files) adds
blast-radius and cross-repo fields the diff-shape score does not compute.
Use change risk to triage "is this commit unusual for this repo"; use
get_risk to ask "what does history say about touching these files".
get_risk's local blast radius is directive.may_break (production files in
structural reverse-import reach of the diff) and directive.may_break_tests
(test files reached the same way, kept separate so a burst of tests cannot
crowd production impact out of the capped list). The walk is given a file list
rather than a diff, so it cannot know whether the symbol an importer actually
uses changed. These are candidates for review, not proven breakage.
Cross-repo change risk (workspace mode)
In a workspace, a change rarely stops at the repo boundary. When get_risk
is called in PR mode (changed_files), its directive block gains two
cross-repo fields derived from the system graph:
will_break_consumers: services in other repos that structurally depend on the changed repo (a contract or package import). Despite the name, this is structural reach worth reviewing rather than a runtime-breakage claim: rows carryclaim: structural_reachandruntime_breakage_claim: false, and the siblingwill_break_consumers_semanticsreadsstructural_reach_only.missing_cross_repo_cochanges: services in other repos that historically co-change with the changed repo but aren't in the diff. Correlation, not a call, so they read as "may drift," not "will break."
The directive also carries breaking_changes when a provider contract in
the changed repo changed incompatibly (a removed route or field, a type or
field-number change, a newly-required field). Where will_break_consumers
is topology ("who depends on this repo"), breaking_changes is
schema-level truth ("this specific contract changed in a way that is
incompatible"). Those links still do not prove exact field use, runtime
failure, or deployment safety.
get_change_risk does not walk the system graph. In a workspace it adds a
cross_repo block listing the consumers of contracts the changed files
provide, read from the last repowise update --workspace.
See also
get_change_risk: the MCP tool, its parameters, and the response shape.get_risk: file-level risk, PR-mode blast radius, and the cross-repo directive fields above.- Code health: the complementary file-level score this layer does not replace.
- Bug-fix history: the
fix_history.overlapblockget_change_riskattaches to a diff.