How PullPulse rates a pull request
Every card carries one small meter — ▮▮▮▯ medium · ~45 min · migration, no tests — answering the two questions a reviewer has before opening a diff: how much of my attention does this need, and is there anything I must know before I approve. Every part of it comes from the rules below, and the Explain button on any card shows which one fired.
Complexity: four tiers
Complexity is review effort, not diff size. A 900-line lockfile bump is trivial; a six-line change spread across three services is not. PullPulse rates it from the shape of the change and nothing else: no model, no history, no guesses about what your directories mean.
Step 1 — sort the changed files into kinds
| kind | what counts | examples |
|---|---|---|
| source | code that runs | src/…, api/…, .go, .py, .ts, .sql |
| tests | code that checks code | tests/, __tests__/, *.test.*, *_test.go, test_*.py, spec/ |
| config | build, CI and dependency manifests | .github/, Dockerfile, package.json, Cargo.toml, go.mod, pyproject.toml |
| docs | prose | *.md, *.mdx, *.rst, docs/, LICENSE |
| generated | output nobody reviews line by line | lockfiles, dist/, vendor/, __snapshots__/, *.snap, *.min.*, CHANGELOG.md |
When a path fits more than one kind the first match in this order wins: generated, docs, tests, config, source. So a CHANGELOG.md is generated rather than prose, and a package.json fixture under __tests__/ is a test rather than a dependency change.
Reviewable lines are the added plus deleted lines in source, tests and config. Docs and generated files never count, however large.
Step 2 — pick the tier from reviewable lines
| tier | reviewable lines | what it means for the reviewer |
|---|---|---|
| ▮▯▯▯ trivial | none, or up to 20 | approve at a glance |
| ▮▮▯▯ low | up to 150 | read one file, in one sitting |
| ▮▮▮▯ medium | up to 600 | hold several files in your head |
| ▮▮▮▮ high | more than 600 | block out time; an owner of the area should look |
Step 3 — two adjustments, and only two
- Spread raises it one tier. Three or more top-level areas of the repository, or 25 or more files.
- Tests-only lowers it one tier. A change that only adds or edits tests is easier to review than its line count says.
Three kinds of change are always trivial whatever their size: docs only, generated only, and a dependency bump — at least one manifest and at least one lockfile and nothing else. A manifest edited on its own is rated by its lines, because it changes what the code depends on.
Before the file list is available — the first, fast notification — every changed line counts as reviewable and no kind-based rule can fire. The tier is recomputed as soon as the files arrive, and Explain says which of the two answers you are looking at.
Attention: things to know before you approve
Separate from complexity on purpose. Complexity answers “how much of my attention does this need”; these answer “is there anything I must know”, and none of them changes the tier — a migration is not harder to read, it is more expensive to get wrong.
| flag | rule |
|---|---|
| migration | a file under migrations/ or migrate/, a .sql file, or a schema file |
| dependencies | a dependency manifest changed together with source code — a manifest plus its lockfile alone is a trivial bump, not a flag |
| ci | a workflow file, a Dockerfile, a compose file, or a deploy manifest |
| infra | Terraform, Helm or Kubernetes definitions |
| public api | .proto, OpenAPI or GraphQL schema files |
| tests removed | a test file was deleted |
| no tests | source changed with no test change, in a repository where tests usually accompany source |
| secrets | an added line matches a credential pattern. Only checked when the diff is available, and never guessed from a file name |
| breaking | the title uses the conventional ! marker, or a label containing "breaking" |
| revert | the title starts with "Revert" |
Your own areas. PullPulse ships no built-in guesses about which directories are sensitive — a payments company’s whole codebase is payments — so name yours once in .pullpulse.yml under attention.paths and they show up as a flag with your own word.
When the AI brief is on, the model may propose up to three extra flags with a one-sentence reason. Those are shown as suggested, and they never set the tier.
Routing on it
Rules compare the tier and the flags directly: complexityAtLeast: high and attention: [migration]. The old 0–100 risk score and its colour bands are gone; a .pullpulse.yml that still writes riskAtLeast keeps routing — the bound is rewritten as the equivalent tier at load, and Explain says so.