Code health scores files. Change risk reports
on a change: a commit, a base..head range, or your uncommitted work. It
leads with one line:
Size and spread: 91st percentile of recent commits (diff size, not a bug predictor)That percentile says how large and spread out the change is compared with this repository's own recent commits. It does not say the change is likely to break. The second block, the touched files' bug-fix record, answers the question size cannot: have these files broken before?
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 constants learned offline. No LLM, no
network, no index required. With no revspec the subject is your uncommitted
work when the tree is dirty, otherwise HEAD. A merge commit is scored for
the diff it brought onto its first parent.
To gate a pull request on it, repowise risk --fail-above-percentile 95
flags a change larger than 95% of this repository's recent commits, never on
the raw score. It is a size gate: 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. Exit codes are 0 at or below the percentile, 1 above, 2 when there
is nothing to rank against (a shallow clone with fewer than 8 recent commits,
or --baseline 0). See Gates in CI.
Reading the result
| Field, in reading order | What it is | Act on it |
|---|---|---|
risk_percentile | Where the change's diff size and spread rank among recent commits (0-100) | Yes: size, not danger |
fix_history | Prior bug fixes on the touched files | Yes: where to look |
review_priority | Tercile of the percentile: low, moderate, high (MCP returns it with include=["scales"]) | Same fact as the percentile |
score | 0-10 diff size and spread on a single-commit scale | Supporting only |
independent_changes | Groups of changed files nothing links | Consider splitting |
The bug-fix record
Bug-fix record for HEAD: touches files with prior bug fixes
File Lines Prior fixes
core/pipeline/persist.py 40 21.6
cli/commands/update_cmd/command.py 6 19.3Prior fixes counts the bug-fix commits
that touched each file before the change, weighted by age (a fix one year
earlier counts a half). History is read from before the change, so a commit
never ranks against fixes it caused. density is the churn-weighted mean of
those numbers, so it does not grow with diff size: one line in a file fixed
twenty times outranks a thousand lines in files never fixed. Manifests and
lockfiles are left out of the listed files.
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.
The 0-10 score
The headline is repo-relative. The 0-10 score is a logistic over the
features above, anchored to the offline calibration corpus, so on a repo
whose typical commit is large the absolute value skews high, and ranges and
squash merges skew high because the scale is one commit. Every payload states
this in score_unit. Read the percentile; keep the score as a supporting
measure.
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
low 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 the percentile; in the web UI it is precomputed from
the indexed commit history.
Accuracy and limits
- The score is a diff-size statistic. Lines added carries a weight 7.6 times the next feature, and lines added alone reproduces the full score within 0.12 to 0.16 points on every repository tried.
- Against churn alone it adds little. Leave-one-repo-out AUC on 4,102 commits across 7 repositories (AG-SZZ labels) is 0.772 for the model and 0.766 for churn alone; the 95% CI of the gap includes zero.
- It does not rank danger. On 47 within-repo pairs of a small dangerous change and a large boring one, built so that size alone scores 0, the score picks the dangerous one in 0 of 47 and fix density in 46 of 47. This is a falsification test, not an accuracy estimate.
- The fix record is keyword based. A project whose fix commits do not say "fix", "bug", "patch" or "resolves" reads low.
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".
The health delta follows code a change moves between files: moved symbols are
mapped, findings pair across the move, and a pure split reads as moved rather
than new. Its files list gives up to 10 analysed files with base and head
score, largest move first, and both sides read the index's dependents.
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. - Change Records: the size rank is one item of the evidence checklist every merged change carries.