The PR quality score

An integer from 0 to 100, or nothing at all. It measures how many known low-effort patterns a pull request trips, and nothing else.

How the number is produced

Every heuristic is a detector for something bad. Passing is the normal case, so the score starts at 100 and comes down.

  1. Start at a ceiling of 100.
  2. For each blocker that fired, lower the ceiling. One blocker caps the score at 50, two at 35, three or more at 20.
  3. A few heuristics carry their own cap. A trivial patch caps the score at 50, or at 25 if the title is also vague or the body is empty. The lowest cap wins.
  4. For every other heuristic that fired, subtract ten points per weight. Weight 1 costs 10, weight 2 costs 20, weight 3 costs 30.
  5. Some heuristics compute their own penalty instead, so it scales with what they measured: one point per inline code reference over the limit, five points per commit that is not in Conventional Commits form.
  6. Floor at 0.
score.ts
const W4_SCORE_CAPS = [50, 35, 20];
const PENALTY_PER_WEIGHT = 10;

ceiling = min(perSignalCaps..., w4Cap(blockersFired))
score   = max(0, ceiling - deductions)

A heuristic that is turned off is not counted. A heuristic that has never run against this pull request is not counted either. If nothing ran, there is no score rather than a score of zero.

Four examples

What firedArithmeticScore
Source added with no tests (w2), one overlong commit message (w1)100 − 20 − 1070
One AI watermark phrase (blocker), nothing elseceiling 50, no deductions50
AI watermark and honeypot hit (two blockers), plus emoji count (w1)ceiling 35 − 1025
All three blockers, plus emoji count (w1)ceiling 20 − 1010

With the default comment threshold of 20, the third example is silent and the fourth gets a public warning comment.

The four weights

Weights are fixed in the code. Maintainers can turn a heuristic off or change its threshold. They cannot change what it costs.

WeightMeaningEffect
1Mild−10
2Major−20
3Critical−30
4Blockercaps the score at 50, 35 or 20

Blocker is reserved for 3 things that are close to proof rather than suspicion: a phrase a language model left in the body, honeypot text copied out of the pull request template, and ignoring the template altogether. Passing everything else should not rescue a pull request that did one of those.

Every heuristic

36 heuristics in six groups, 29 of them on by default. The identifiers are the exact keys stored on each scored pull request, so a maintainer looking at raw signals can match them one to one.

Size

size · 4 heuristics · 4 on by default
HeuristicWeightDefaultWhat it looks for
Mega PR (huge diff in a single commit)size.mega_pr3 criticalon50Trips when both file and line counts exceed thresholds AND commit count is 1: a typical AI-bulk-generation pattern.
Trivial patch (too small to score high)size.trivial_patch3 criticalon3PR changes ≤ N lines in ≤ 1 file with ≤ 1 commit: likely a typo, default-web-UI edit, or probe PR. Caps the score at 50% (or 25% when paired with a vague title or empty body).
Excessive file countsize.file_count2 majoron50Penalize unfocused PRs touching too many files.
Excessive line countsize.line_count2 majoron10000Penalize PRs with very large diffs.

PR text

pr · 11 heuristics · 9 on by default
HeuristicWeightDefaultWhat it looks for
Honeypot keyword hitpr.honeypot_hit4 blockeronPR body contains hidden honeypot text from the project's PR template, typically copy-pasted by AI bots.
PR doesn't use the repo's PR templatepr.uses_template4 blockeron0Repo ships a PR template (e.g. .github/PULL_REQUEST_TEMPLATE.md). When the template has checklist items, the body must include them. Threshold sets how many checkboxes may be missing (default 0). Match strictness is set project-wide (Quality core settings, default 80%). When the template has no checkboxes, at least one heading must appear. Skipped when the repo has no template.
AI watermark phrasepr.ai_watermark4 blockeronBody contains a phrase commonly emitted by language models (e.g. "as an AI", "Here is the updated").
Body adds too many extra headers beyond the templatepr.template_extra_headers3 criticalon0Counts headers in the body whose text doesn't appear in the repo's PR template. Threshold is the maximum number of extra headers admins allow (default 0). Skipped when the repo has no template.
Empty PR bodypr.body_empty2 majoronBody is missing or whitespace-only.
Vague PR titlepr.title_vague2 majoronTitle is a single vague word (update / fix / wip / patch / changes / misc / stuff / chore), GitHub's web-UI default ("Update README.md"), under 8 chars, or emoji-only.
AI description assessmentpr.ai_assessment2 majoroff40Fires when a model judges the PR's description to be a poor guide to what the change actually does. Only applies to PRs where the AI assessment task has been run; PRs without one are unaffected.
Wall-of-text PR bodypr.body_too_long1 mildon2500Body exceeds the configured character limit.
Excessive emojis in bodypr.body_emoji_count1 mildon2Many AI-generated PRs are decorated with emojis.
Excessive inline code referencespr.body_inline_code_refs1 mildon5Walls of inline ` ` references often indicate AI summaries.
No linked issuepr.no_linked_issue1 mildoffBody has no #N reference or fixes/closes/resolves keyword.

Commits

commit · 5 heuristics · 4 on by default
HeuristicWeightDefaultWhat it looks for
Author mismatch across commitscommit.author_mismatch2 majoronCommits authored by multiple identities, usually cherry-picked work. Threshold is an allowlist of author emails or logins (one per line) that don't count toward the distinct-author tally (e.g. bot accounts like dependabot[bot] or noreply@github.com).
Single giant commitcommit.single_giant2 majoron2000One commit with very many changes: typical AI bulk diff.
Commit message too longcommit.message_too_long1 mildon500Any single commit message exceeds the configured length.
Conventional Commits formatcommit.conv_commits1 mildoffEach commit message must match `type(scope): subject`. Off by default.
Whitespace-only commitscommit.whitespace_only1 mildonAt least one commit changes only whitespace (heuristic: tiny diffs only modify ws characters in the patch).

Code

code · 8 heuristics · 8 on by default
HeuristicWeightDefaultWhat it looks for
Lockfile-only PRcode.lockfile_only3 criticalonPR changes only lockfiles: almost always low-effort.
Excessive added commentscode.excessive_added_comments2 majoron10Ratio of added comment lines to added code lines is high: typical AI over-commenting.
Touches blocked pathscode.blocked_paths2 majoron**/package-lock.json, **/pnpm-lock.yaml, **/yarn.lock, **/Cargo.lock, **/Gemfile.lock, **/go.sum, **/poetry.lock, **/composer.lock, dist/**, build/**, out/**, **/*.min.js, **/*.min.cssPR modifies paths listed in the project's blocked-paths config (default: lockfiles, dist, build, minified files).
Code added without testscode.test_to_code_ratio2 majoronAdds source files but no test files are added or modified.
Formatter-only diffcode.formatter_only2 majoronDiff appears to be exclusively reformatting (every hunk has equal token counts ignoring whitespace).
Binary or generated files addedcode.binary_or_generated2 majoronAdds binary files or files matching common generated patterns (*.min.js, *.lock, dist/**).
Missing final newlinecode.missing_final_newline1 mildonAny added file missing a trailing newline character.
Docs-only changescode.docs_only_in_code_repo1 mildonOnly Markdown files changed.

Diff cohesion

diff · 2 heuristics · 1 on by default
HeuristicWeightDefaultWhat it looks for
Cross-module sprawldiff.cross_module1 mildoff4PR spans more than the configured number of top-level directories.
Suspicious renamesdiff.suspicious_renames1 mildon10Many file renames in a single PR: often AI bulk-rename slop.

Account

account · 6 heuristics · 3 on by default
HeuristicWeightDefaultWhat it looks for
Mass forkingaccount.mass_forking3 criticaloff6Author has created an unusually large number of forks in the last 24 hours.
Spam-like usernameaccount.spam_username2 majoronUsername matches common spam/AI patterns (digit-heavy, ai/gpt/bot suffix, etc.).
Account too newaccount.too_new2 majoron30GitHub account created less than the configured number of days ago.
Low global merge ratioaccount.low_merge_ratio2 majoroff30Author's global merged-PR / total-PR ratio is below the configured percent.
Thin profileaccount.profile_thin2 majoronEmpty bio AND no avatar AND <2 followers AND <1 public repo. Often a throwaway account.
No public emailaccount.no_email1 mildoffProfile exposes no public email address.

What maintainers can change

Per project

  • Whether scoring runs at all. Off by default.
  • Which heuristics are enabled.
  • Each heuristic's threshold.
  • The honeypot phrases.
  • Template match strictness, default 80 percent.
  • The score below which a public comment is posted, default 20.

Fixed in the code

  • The weights.
  • The blocker caps.
  • The ten points per weight.
  • What each heuristic actually looks for.

Changing the configuration takes effect immediately, including on pull requests that were scored last month. Raw signals are stored per pull request and the score is recomputed from them on every read, so there is no recompute job and no stale number.

Who sees the score

Maintainers, in the dashboard. That is the default and usually the whole story.

The one public surface is the warning comment, posted at most once per pull request when the score is under the project's threshold. It names the score and lists which heuristics flagged, with their measured values.

What it does not do

It does not read your code for correctness. It does not decide whether a change is a good idea. It does not block a merge, and it is not wired into branch protection.

It flags patterns that correlate with low-effort and machine-generated pull requests. That is all it is: correlation. A careful one-line fix to a typo trips the trivial patch heuristic and scores badly. A sloppy thousand-line pull request with a filled-in template can score well. The number is triage, not judgement.

How the gating decision works