Scoring
Source: src/engine/scorer/index.ts, src/engine/scorer/weights.ts, src/engine/scorer/labels.ts
Converts filtered diagnostics into a 0-100 health score with a quality label.
One number lets you track progress over time, set a CI threshold with
--min-score, and compare projects. The weights are chosen so a security error
costs more than an info-level performance hint.
Input is the filtered diagnostics and the file count:
diagnostics: Diagnostic[] // filtered diagnostics
fileCount: number // total files scannedThe result is the score and its label:
interface Score {
value: number // 0-100, clamped
label: string // "Excellent" | "Good" | "Fair" | "Poor" | "Critical"
}What counts
A rule declares which surfaces it appears on. Only diagnostics carrying the
score surface reach the formula below. A rule marked surfaces: ["cli"]
still reports every finding in the console and the HTML report, and weighs
nothing, which keeps a matter of taste from moving a number meant to track
defects. See Surfaces.
Formula
The score starts at 100 and loses points for every diagnostic:
penalty per diagnostic = SEVERITY_WEIGHT × CATEGORY_MULTIPLIER
total penalty = sum of all diagnostic penalties
score = 100 − (totalPenalty / fileCount) × PENALTY_SCALEThe score is clamped to the 0-100 range.
Severity weights
| Severity | Weight |
|---|---|
| error | 3.0 |
| warning | 1.5 |
| info | 0.5 |
Category multipliers
| Category | Multiplier | Rationale |
|---|---|---|
| security | 1.5x | The most impactful category |
| correctness | 1.3x | Bugs that cause runtime errors |
| schema | 1.1x | Database structural integrity |
| architecture | 1.0x | Slows the project down as it grows |
| performance | 0.8x | Important, but less critical than correctness |
Penalty scale
PENALTY_SCALE = 10 is a calibration constant. Combined with the severity weights and category multipliers, it produces scores on this scale:
| Scenario | Score |
|---|---|
| 1 security error per 10 files | ~95 (Excellent) |
| 1 error per 3 files | ~85 (Good) |
| 1 error per file | ~55 (Fair) |
| 2 errors per file | ~10 (Critical) |
Normalization
The score is divided by fileCount so projects of different sizes are comparable. A 500-file project with 10 errors scores the same as a 50-file project with 1 error, because the density is the same.
Labels
| Score | Label |
|---|---|
| 90-100 | Excellent |
| 75-89 | Good |
| 50-74 | Fair |
| 25-49 | Poor |
| 0-24 | Critical |
Monorepo scoring
In a monorepo, each sub-project gets its own score. The combined score comes from the merged diagnostics of every sub-project, divided by the total file count across all of them.
Merging drops a diagnostic an earlier sub-project already reported, matched on rule, file, position and message. Two sub-projects sharing a workspace-root package.json or ORM schema therefore pay for it once.
A sub-project carries the diagnostics the merge kept, so the breakdown table stays consistent with the combined counts. Its score is not recalculated from them: a score always describes the whole project it was measured on, never the subset a report shows.
Debugging tips
- If the score seems too low, look for high-severity rules producing many diagnostics. A single security error per file adds
3.0 × 1.5 = 4.5penalty points. - The score is normalized by file count. Adding files without adding diagnostics raises the score.
- Use
--jsonto see the full diagnostic list and what is contributing to the penalty.