Skip to content

crap-metric

crap-score-review

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
Category: implementation
  • crap
  • complexity
  • cyclomatic
  • coverage
  • change-risk
  • metrics
  • refactoring
  • code-review
  • technical-debt
  • testing

Install this skill

npx github:yingkitw/skillsheet --remote --tool claude-code --only crap-metric

Fetches the latest published version from skillsheet.dev — the first run installs the package from GitHub, after that npx caches it.


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).

  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).
  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 — The broader audit; CRAP is its Coverage & Complexity axes with one number.
  • test-to-completion — The coverage discipline that collapses the cube term.
  • minimal-fix — Shaping each per-finding fix the score points at.