pr-complexity
v1.2.0
Published
Score code changes by information content — K(diff | codebase)
Maintainers
Readme
pr-complexity
A GitHub Action that scores pull requests by information content instead of lines of code. Language-agnostic: JS/TS, Python, Go, Rust, Ruby, Java, C/C++, C#, PHP, Swift, Kotlin, SQL, shell, Dockerfiles — anything text-based.
It compresses the diff against the rest of your codebase (brotli). Code that repeats existing patterns — boilerplate, renames, house style — compresses to almost nothing. Genuinely new logic doesn't. Deletions earn symmetric credit. The raw byte count is then mapped onto a log-scale points score approximating the Kolmogorov complexity of the change given the codebase: K(diff | repo).
Lockfiles, generated code, and build output are ignored.
Usage
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
complexity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: sebMellen/pr-complexity@v1The action posts one comment per PR and updates it on every push.
CLI (for humans and agents)
Zero dependencies, runs anywhere with Node 20+ and git:
npx pr-complexity # score uncommitted changes vs HEAD (incl. untracked files)
npx pr-complexity --base main # score HEAD vs merge-base of main (PR mode)
npx pr-complexity --json # machine-readable, per-file attribution
npx pr-complexity --budget 40 # exit 1 if over budgetBecause the score is cheap, deterministic, and signed, an agent can optimize against it — implement, score, simplify, repeat. The cheapest way to lower the score is to reuse existing patterns and delete dead code, so gaming it mostly means doing the right thing:
while ! pr-complexity --budget 40 --json > score.json; do
agent "Reduce complexity. Biggest contributors: $(jq -c '.files[:5]' score.json)"
doneAlways constrain on behavior (tests must pass) — the score is a simplicity prior, not proof of correctness.
Reading the score
Points are a Fahrenheit-style 0–100 scale: 0 means the PR adds no new information, and 100 is the limit — roughly 16 KB of novel compressed logic, the largest change that should ever land in one PR. Scores are clamped to ±100.
Points = min(100, 5 · log2(1 + bytes/100)^1.5), signed — deletions push negative. The 1.5 exponent compresses the everyday range (most PRs land under 50) and stretches the top end, so genuinely huge changes still shoot toward the ceiling. On top of that, a file-scatter penalty charges for review context-switching: 0 for ≤10 files, 6·log2(files/10) beyond, capped at +30. Net-negative PRs are exempt — simplification is never taxed.
| Points | Band | Meaning |
|---|---|---|
| ≤ 0 | net simplifier | Removes more information than it adds. Reward these. |
| < 10 | trivial | Typos, config tweaks, one-line fixes. |
| < 30 | small | A focused fix or small feature slice. |
| < 50 | moderate | Real new logic; review carefully. |
| < 70 | large | Heavy new complexity — should earn its keep. |
| 70–99 | split-worthy | Strongly consider splitting. |
| 100 | the limit | Too big to review. Split it. |
Calibration data from a large production TypeScript API: focused bug fixes score 5–30, feature slices 42–58, major upgrades (Prisma v7, CI reworks) 70–82, and a whole new platform API prototype (~3.3K lines, dozens of files) scores 88. Because the scale is anchored to information content rather than repo size, a point means roughly the same thing across codebases.
Outputs
The action sets points and net_bytes as step outputs, so you can gate on them (e.g. require extra review above a threshold).
How it works
- Diff the PR against its merge base (
git diff base...head). - Drop lockfiles (
*.lock,*.sum), generated code (*.gen.*,*.pb.*), minified files, and build output (dist/,node_modules/,target/,__pycache__/, …). - Strip comments (
//,#,--,/* */— heuristic) and whitespace from the added/removed lines. - Concatenate the repo's source files (capped at 256 KB, excluding the files being changed so new code can't compress against itself) as a compression context.
- Score =
compress(context + added) − compress(context)minus the same for removed lines, pooled across all files — a pattern repeated in 20 files is one idea, not twenty. The file-scatter penalty separately captures the per-file review burden, and per-file byte estimates keep the score attributable.
Because compression is a novelty detector, unusual identifiers, magic numbers, and new abstractions cost full entropy, while the 50th return statement costs ~zero.
Zero dependencies — just Node's built-in zlib, git, and the GitHub REST API.
