---
name: crap-score-review
description: Reviews code with the CRAP score — Change Risk Anti-Patterns — where cyclomatic complexity and test coverage combine into one risk number per method (crap = comp²·(1−cov)³ + comp). Hunts complex-and-untested code, prescribes the cheapest fix first (cover it, then simplify it), and gates regressions in CI. Use when auditing a codebase for risky methods, when review says "too complex" or "untested", or when prioritizing where tests and refactors pay off most.
category: implementation
keywords: crap, complexity, cyclomatic, coverage, change-risk, metrics, refactoring, code-review, technical-debt, testing
---

# CRAP Score Review

CRAP — Change Risk Anti-Patterns — is the metric that refuses to let complexity and coverage be discussed separately. Per method:

```
crap(m) = comp(m)² · (1 − cov(m))³ + comp(m)

comp = cyclomatic complexity        cov = coverage, 0…1
```

The shape of the formula is the message: risk grows with the **square of complexity** but explodes with the **cube of missing coverage**. A complexity-10 method fully covered scores 10 — annoying but known. The same method at 0% coverage scores ~901 — every future change is a bet. CRAP finds the code where a bug is both *likely to live* (many branches) and *free to multiply* (nothing fails when you break it).

## When to apply

Use when auditing or reviewing a codebase for risk, when deciding where the next tests go, when a reviewer says "this is too complex" or "this is untested" and you want a number instead of a shrug, or when setting a CI quality gate that won't cry wolf.

## Reading the number

- **crap ≈ comp** — method is fully covered; risk is just the branching itself. Higher comp still hurts (comp² would be the risk *if the tests vanished*), but changes here fail loudly.
- **crap ≤ 30** — the conventional "acceptable" ceiling (Feathers' original guidance). Most mature codebases gate here.
- **crap > 30** — the method is both complex and under-tested: refactor it, cover it, or both. Every point past 30 is a change waiting to break something silently.
- **crap > 100** — the method is a hazard: nobody understands it and nothing observes it. Do not "clean up later" it — schedule it.

Two traps when reading scores: a covered-but-worthless test suite (asserts nothing) makes crap look healthy — coverage counts execution, not verification. And gaming the metric by splitting a 20-branch function into five 4-branch functions just moves the complexity into the call graph; the branches still exist, now hidden behind indirection.

## Workflow

1. **Measure** — generate per-method complexity + coverage in one pass. Tools that produce CRAP directly: `lizard -C`, SonarQube, PHP_CodeSniffer/PHPMD; or compose it: cyclomatic (ESLint `complexity`, gocyclo, radon) × coverage (Istanbul/go cover/coverage.py). Script the join — per-method rows of `file:line, method, comp, cov, crap` — so the score is reproducible, not vibes.
2. **Rank, don't drown** — sort by crap descending; report the top offenders, not the firehose. A list as long as the codebase is a filing cabinet (see [engineering-roast](/skills/engineering-roast)).
3. **Fix cheapest-first, per finding**:
   - **High comp, low cov** → write characterization tests *first*. Coverage moves the cube term before you touch the code; the refactor that follows runs behind a net. This ordering is the whole point of the metric.
   - **High comp, high cov** → the branches are known but too many: extract guard clauses, replace conditionals with lookup tables, split on domain seams — reduce comp, and the score falls quadratically.
   - **Dead or unreachable branches** → delete them; the cheapest complexity is the kind that stops existing ([minimal-fix](/skills/minimal-fix)).
4. **Re-measure** — rerun the pass after each fix; the score must drop and coverage must not. A refactor that lowers complexity while quietly untesting error paths has made the code worse.
5. **Gate it** — set a CI threshold (start at crap > 30 per method, or a per-module average) that blocks new violations but grandfathers existing ones. Ratchet down as debt is paid; a gate applied retroactively to everything gets disabled by Friday.
6. **Report as findings** — same receipt discipline as any review:

   ```
   ## CRAP 412 — Orders.applyDiscount() (src/orders.ts:88)
   comp 14 · cov 0.12 · no test touches the discount paths
   Fix: characterize the 4 discount tiers, then split tier logic from total math.
   ```

## Rules

- Complexity and coverage are one conversation — a finding that cites only one of them is half a finding.
- Tests before refactors on uncovered code — the cube term exists to enforce this order.
- Score is a pointer, not a verdict — read the method before prescribing; the metric says *where*, not *what*.
- No metric gaming — splitting a monster into five helpers that still branch 20 times is hide, not fix.
- Gates block new debt, not history — grandfather the backlog, ratchet the ceiling down.

## Related skills

- [engineering-roast](/skills/engineering-roast) — The broader audit; CRAP is its Coverage & Complexity axes with one number.
- [test-to-completion](/skills/test-to-completion) — The coverage discipline that collapses the cube term.
- [minimal-fix](/skills/minimal-fix) — Shaping each per-finding fix the score points at.
