Browse documentation
Docs
Scoring methodology
Every merged PR gets three numbers. Here is exactly how they are computed.
Architecture overview
Every merged PR produces three numbers:
| Metric | Range | Produced by |
|---|---|---|
| quality_score | 0-100 | LLM (banded rubric) |
| effort_multiplier | 0.0-1.0 | LLM (fraction of real work vs. churn) |
| velocity_score | calculated | Deterministic: base_velocity x effort_multiplier |
The core design decision: the LLM never produces an effort number directly. LLMs are unreliable at absolute magnitude but good at classification. The size/effort base is computed deterministically from lines changed, and the LLM only answers two questions: "how much of this diff is real work vs. churn?" and "is it good?"
Step 1 - Exclusions
Before any counting, these files are stripped and do not contribute to any score:
- -Test files: paths containing
/tests/,/test/,/__tests__/,/__mocks__/; files ending inTest.php,.spec.ts,.test.ts, etc. - -Vendored/generated: paths containing
/vendor/,/node_modules/,/site-packages/,/boto3/, etc. - -Dependency lockfiles:
package-lock.json,composer.lock,yarn.lock,poetry.lock,symfony.lock.
These same exclusion rules apply to both velocity scoring and AI attribution share percentages, so "non-excluded lines" means the same thing in both contexts.
Step 2 - Non-code weighting (1/4)
After exclusions, remaining files are classified as non-code if they match any of these rules. Non-code lines count at 1/4 weight when computing effective line counts.
| Rule type | Matches |
|---|---|
| Extensions | .md, .txt, .env, .gitignore, .editorconfig, .yml, .yaml, .sh, .bash, .zsh |
| Filenames | Makefile, Dockerfile, docker-compose.yml, .dockerignore |
| Path segments | .github/workflows/, .github/actions/, .claude/ |
Formula: effective_lines = code_lines + round(non_code_lines / 4)
Step 3 - Per-module grouping
Files are grouped by their top-level directory. Files at repository root go into a _root group. Each group is scored independently, and the group scores are summed.
Rationale: two developers sharing one branch score the same total as two separate PRs. The logarithmic compression applies per module, not globally, preventing one massive directory from drowning everything else.
Step 4 - Base velocity step table
Each group's effective line count maps to a base score through this step table. Above 3000 lines, scoring grows logarithmically - it does not hit a flat cap.
| Effective lines (per module) | Base score |
|---|---|
| 0 lines | 0 |
| 1-10 lines | 3 |
| 11-30 lines | 5 |
| 31-100 lines | 10 |
| 101-300 lines | 20 |
| 301-700 lines | 30 |
| 701-1200 lines | 40 |
| 1201-2000 lines | 50 |
| 2001-3000 lines | 65 |
| 3000+ lines | 95 + round(75 * log2(lines / 3000)) |
95 + round(75 * log2(lines / 3000)), so 6000 lines yields 170. FeatureFactory ports the code behavior, not the doc. Step 5 - Per-group modifiers
| Condition | Modifier |
|---|---|
| 3-5 files in a group | +3 |
| 6-10 files in a group | +5 |
| >10 files in a group | +8 |
| Files span 2+ top-level directories | +5 (cross-module, applied once) |
| Group touches a critical area | +5 |
Critical area patterns
A group is flagged as touching a critical area if any filename (case-insensitive) contains one of these substrings:
/auth//permissions//billing//payment//invoice//pricing//migrations/schema/guards//middleware/All patterns except "schema" are directory segments (i.e. /invoice/). A file named app/Models/Invoice.php does NOT match /invoice/, but app/Billing/Invoice.php matches /billing/. API-contract changes are not currently included in critical-area detection.
Effort multiplier (LLM)
An LLM classifies each file in the diff into a churn bucket, then estimates what fraction of total lines represent genuine engineering effort. The multiplier is a number from 0.0 to 1.0.
| Value | Meaning |
|---|---|
| 1.0 | All changes are meaningful code - new features, logic changes, bug fixes, real refactoring. |
| 0.75 | Mostly real changes with some boilerplate, config, or trivial adjustments. |
| 0.5 | Mixed - significant portions are generated code, copy-paste, or mechanical refactoring. |
| 0.25 | Mostly formatting, whitespace, renaming, or auto-generated code with some real changes. |
| 0.0 | Entirely formatting, whitespace, or no-op changes. |
Large feature PRs that introduce substantial new functionality score high effort even when they include some boilerplate or config files. The multiplier only drops significantly when changes are predominantly auto-generated, copy-pasted, or mechanical.
For very large PRs (diff exceeding ~250K chars), the diff is chunked and each chunk analyzed independently. A second LLM pass reads all chunk summaries to emit a single unified effort_multiplier for the PR.
Final velocity: velocity_score = base_velocity x effort_multiplier
Quality score (LLM, 0-100)
Produced by the same LLM call as the effort multiplier. Uses banded anchors to keep scores comparable across PRs and reviewers.
Excellent
Correct, clear, maintainable. Tests present where appropriate. Safe error handling, minimal unintended coupling.
Solid
Good work with minor nits - small missing tests or docs, low-risk edge cases.
Mixed
Unclear intent, weak tests, likely edge cases, or maintainability concerns.
Risky
Incorrect or confusing changes, missing critical tests, or high regression risk.
Auditability
Every scored PR stores:
- -Typed score columns (
quality_score,velocity_score) for fast queries and dashboards. - -A full JSON blob of the LLM response - summaries, strengths, risks, review focus, confidence, and the raw file classifications.
- -A breakdown of the base velocity computation: which files were excluded and why, which group each file landed in, the effective line count, the step-table base, and each modifier applied.
This means you can always answer "why did this PR score X?" with a specific, auditable trail - not a black box number.
Was this article helpful?
Your response enters the same customer insight queue used by product feedback.