# Change risk

Change risk ranks a commit or base..head range by diff size and spread against your repo's recent commits, next to the touched files' bug-fix record.

[Code health](https://docs.repowise.dev/intelligence/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:

```text
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

```bash
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-readable
```

It 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](https://docs.repowise.dev/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

```text
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.3
```

Prior fixes counts the [bug-fix commits](https://docs.repowise.dev/intelligence/bug-history#what-counts-as-a-bug-fix)
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`

> **Note**
>
> `repowise risk` and [`get_change_risk`](https://docs.repowise.dev/mcp/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`](https://docs.repowise.dev/mcp/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 carry `claim: structural_reach` and
  `runtime_breakage_claim: false`, and the sibling
  `will_break_consumers_semantics` reads `structural_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`](https://docs.repowise.dev/mcp/get-change-risk): the MCP tool, its
  parameters, and the response shape.
- [`get_risk`](https://docs.repowise.dev/mcp/get-risk): file-level risk, PR-mode blast radius,
  and the cross-repo directive fields above.
- [Code health](https://docs.repowise.dev/intelligence/code-health): the complementary file-level
  score this layer does not replace.
- [Bug-fix history](https://docs.repowise.dev/intelligence/bug-history): the `fix_history.overlap`
  block `get_change_risk` attaches to a diff.
- [Change Records](https://docs.repowise.dev/intelligence/change-records): the size rank is one
  item of the evidence checklist every merged change carries.
