PullPulse
The rating model

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

kindwhat countsexamples
sourcecode that runssrc/…, api/…, .go, .py, .ts, .sql
testscode that checks codetests/, __tests__/, *.test.*, *_test.go, test_*.py, spec/
configbuild, CI and dependency manifests.github/, Dockerfile, package.json, Cargo.toml, go.mod, pyproject.toml
docsprose*.md, *.mdx, *.rst, docs/, LICENSE
generatedoutput nobody reviews line by linelockfiles, 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

tierreviewable lineswhat it means for the reviewer
▮▯▯▯ trivialnone, or up to 20approve at a glance
▮▮▯▯ lowup to 150read one file, in one sitting
▮▮▮▯ mediumup to 600hold several files in your head
▮▮▮▮ highmore than 600block out time; an owner of the area should look

Step 3 — two adjustments, and only two

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.

flagrule
migrationa file under migrations/ or migrate/, a .sql file, or a schema file
dependenciesa dependency manifest changed together with source code — a manifest plus its lockfile alone is a trivial bump, not a flag
cia workflow file, a Dockerfile, a compose file, or a deploy manifest
infraTerraform, Helm or Kubernetes definitions
public api.proto, OpenAPI or GraphQL schema files
tests removeda test file was deleted
no testssource changed with no test change, in a repository where tests usually accompany source
secretsan added line matches a credential pattern. Only checked when the diff is available, and never guessed from a file name
breakingthe title uses the conventional ! marker, or a label containing "breaking"
revertthe 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.