Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b059811
docs: design what a score means, from the field measurement
oekazuma Aug 5, 2026
6355e6d
docs: floor the denominator and report reach beside the score
oekazuma Aug 5, 2026
cd270fa
docs: plan the score floor and reach
oekazuma Aug 5, 2026
1143a30
fix(core): never score a key against less than 25 points of checks
oekazuma Aug 5, 2026
0456549
test(core): make the inventory-floor guards exercise computeScore, no…
oekazuma Aug 5, 2026
cbd9bd9
test(core): re-anchor the fourth absorbed mean-integrity fixture abov…
oekazuma Aug 5, 2026
ca81701
feat(core): report how many keys a category reached
oekazuma Aug 5, 2026
2f45f20
feat(core): expose the weight each pair is scored against
oekazuma Aug 5, 2026
6ff77dc
test(core): pin severity ordering to a pair, not to a category
oekazuma Aug 5, 2026
62f2324
docs: scope the severity-ordering guarantee to a pair, not a category
oekazuma Aug 5, 2026
b4aced9
test(core): re-anchor tests the inventory floor silently gutted
oekazuma Aug 6, 2026
cf58376
refactor(core): remove the inventory floor's now-dead zero guard
oekazuma Aug 6, 2026
903d1fc
docs: correct the score floor's recomputability and zero-score claims
oekazuma Aug 6, 2026
e585680
docs: fix three wrong numbers in the score-floor design doc
oekazuma Aug 6, 2026
2a547b5
docs: scope the changeset's severity guarantee to a (category, scope)…
oekazuma Aug 6, 2026
ef997d0
docs: fix the health score guide's zero-score claim (en/ja)
oekazuma Aug 6, 2026
83769ff
docs: add a language tag to the score-floor design doc's formula fence
oekazuma Aug 6, 2026
3820d68
docs(core): state why inventories recomputes routes[].categories
oekazuma Aug 6, 2026
e5732dc
test(core): pin recomputation across every category, including one th…
oekazuma Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .changeset/score-floor-and-reach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@svelte-vitals/core': minor
'svelte-vitals': minor
'@svelte-vitals/vite': minor
---

A less severe finding now costs less than a more severe one **within the same (category, scope) pair**, and
the report says how much of a project each category touched.

A key's category score is the share of that category's severity weight that survived, checks grouped by
category and scope — the keys of the new `inventories` map, like `seo::route`. **Within one pair** a
`warning` costs five times an `info` and a `critical` fifteen times, so a more severe finding always costs
more, there. **Across pairs it does not**: a pair that checks very little is scored against a floor of 25, so
a `warning` there can cost more than a `critical` in a large pair — a `warning` in a floored pair costs 20
while a `critical` in `seo::route` costs 13.64. A key is now never scored against less than 25 points of
checks: in a one-rule pair the three severities give **96** (`info`), **80** (`warning`) and **40**
(`critical`), where a lone `warning` used to score **0**.

Scores rise wherever a category checks few things. **A `--min-health` gate calibrated on the previous release
will pass more easily; recalibrate it.**

Because a score is a mean over every key, forty affected keys and one affected key can display alike. Each
category in the JSON report now carries `keys` and `affectedKeys`, which distinguish them exactly, and
an `inventories` map giving the divisor behind every key of a pair, so a route's per-category score can be
checked by hand (a route's own `score`, which can span more than one pair, cannot).
6 changes: 5 additions & 1 deletion docs/src/content/docs/guides/(reporting)/health-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ Health is computed in two stages:
For each active category (SEO, Performance), svelte-vitals computes an independent score:

- Each route scores the share of that category's checks it was measured against — weighted by severity —
that passed: no failures scores **100**, every applicable check failing scores **0**.
that passed: no failures scores **100**. A route is never scored against less than 25 points of severity
weight (the _inventory floor_ — see the [Reporters guide](/guides/reporters) for the full rule). The floor
only helps a thin inventory: it stops one or two findings from zeroing out a route that checks very little.
Once that inventory reaches 25 on its own, the floor changes nothing, and a route failing every applicable
check still scores **0**.
- Severity sets the weight a failing check carries: `critical` weighs 15, `warning` weighs 5, `info` weighs 1.
- A failing check counts once per (route, rule) pair — duplicates take the maximum severity, not a sum.
- Route scores are averaged to produce the category's headline score.
Expand Down
29 changes: 27 additions & 2 deletions docs/src/content/docs/guides/(reporting)/reporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ svelte-vitals --reporter json
"routeAverage": 94, // mean of the per-route scores, floored
"sitePenalty": 0, // deducted for site-wide findings (no route)
"criticalCap": null // the cap value when a critical finding lowered the score, else null
}
},
"keys": 42, // routes (or other scored units) this category measured
"affectedKeys": 6 // of those, how many carried at least one finding
}
},
"summary": { "critical": 0, "warning": 33, "info": 44, "passed": 610, "dynamic": 2 },
Expand Down Expand Up @@ -71,10 +73,33 @@ svelte-vitals --reporter json
]
}
],
"siteIssues": [] // findings with no route (robots.txt, sitemap.xml, …), same issue shape
"siteIssues": [], // findings with no route (robots.txt, sitemap.xml, …), same issue shape
"inventories": {
"seo::route": 110 // floored severity weight behind every "seo" key scored against "route"
}
}
```

A category's score on a key is the share of that category's severity weight that survived. Checks are
grouped by category and scope — the keys of `inventories`, like `seo::route` — and **within one group** a
`warning` costs five times an `info` and a `critical` fifteen times, so a more severe finding always costs
more. **Across groups it does not**: a group that checks very few things is scored against a floor of 25,
which makes each of its findings a larger share, so a `warning` in a small group can cost more than a
`critical` in a large one. Repeated findings from the same rule on the same key cost what one costs. Beside
the score, `affectedKeys` says how much of the project the category touched: the score is depth, that is
reach.

Two things follow that the paragraph above doesn't say directly:

- per-key scores are comparable **within** a category; across categories the number says which category has
a larger share of _its own_ checks failing, not which problem is worse.
- `inventories` gives the divisor behind every key of one pair, so a route's per-category score
(`routes[].categories`) recomputes by hand from it — this holds because a key is either a route id or a
source file path, and those two key spaces never overlap, so a category's results on one key always share
one scope. A route's own `score` does not recompute the same way, once the route spans more than one pair:
it sums the raw inventory of every pair touched and floors that sum once, while `inventories` publishes
each pair already floored on its own — the two can disagree.

Two field names are worth pointing out, because guessing them wrongly fails silently:

- the rule identifier is **`id`**, not `rule`;
Expand Down
5 changes: 4 additions & 1 deletion docs/src/content/docs/ja/guides/(reporting)/health-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ Health は 2 段階で計算されます:
svelte-vitals は、アクティブなカテゴリ(SEO、パフォーマンスなど)ごとに独立したスコアを計算します:

- 各ルートは、そのカテゴリで測定対象となったチェックのうち、重大度で重み付けした上で合格した割合をスコアとします。
失敗が一つも無ければ **100**、該当するチェックがすべて失敗すれば **0** になります。
失敗が一つも無ければ **100** です。ルートが測定される重大度ウェイトには 25 点の下限値があります
(詳しくは[レポーターガイド](/ja/guides/reporters)を参照)。この下限値が効くのは測定対象がもともと薄いルート
だけで、一つか二つの検出でスコアがゼロになるのを防ぎます。測定対象がそれ自体で 25 点以上あるルートでは下限値は
何も変えず、該当するチェックがすべて失敗すればスコアは **0** のままです。
- 失敗したチェックの重みは重大度が決めます:`critical` は 15、`warning` は 5、`info` は 1 です。
- 失敗したチェックは(ルート、ルール)ペアごとに一度だけ数えます。同じペアで重複した場合は、合計せず最大の重大度を適用します。
- ルートスコアを平均してカテゴリの見出しスコアを算出します。
Expand Down
16 changes: 14 additions & 2 deletions docs/src/content/docs/ja/guides/(reporting)/reporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ svelte-vitals --reporter json
"routeAverage": 94, // ルートごとのスコアの平均(切り捨て)
"sitePenalty": 0, // サイト全体の検出(route を持たないもの)による減点
"criticalCap": null // critical によってスコアが抑えられた場合はその上限値、なければ null
}
},
"keys": 42, // このカテゴリが測定した対象(ルートなど)の数
"affectedKeys": 6 // そのうち何らかの検出があった数
}
},
"summary": { "critical": 0, "warning": 33, "info": 44, "passed": 610, "dynamic": 2 },
Expand Down Expand Up @@ -71,10 +73,20 @@ svelte-vitals --reporter json
]
}
],
"siteIssues": [] // route を持たない検出(robots.txt、sitemap.xml など)。issue の構造は同じ
"siteIssues": [], // route を持たない検出(robots.txt、sitemap.xml など)。issue の構造は同じ
"inventories": {
"seo::route": 110 // "seo" と "route" の組み合わせで測定されるキーそれぞれの、下限値適用後の重大度ウェイト合計
}
}
```

あるキーに対するカテゴリのスコアは、そのカテゴリが持つ重大度ウェイトのうち生き残った割合です。チェックはカテゴリとスコープの組——`inventories` のキーである `seo::route` のような単位——でグループ化されており、同じ組の中でなら `warning` は `info` の5倍、`critical` は15倍のコストがかかるので、重大度の高い検出は必ずより大きなコストになります。ところが組をまたぐとこの順序は成り立ちません。チェック対象が極端に少ない組は下限値25を分母にスコアが計算されるため、そこでは1件あたりの負担が相対的に大きくなり、小さな組の `warning` が大きな組の `critical` より高くつくことがあります。同じルールが同じキーで何度検出されても、コストは1件分のままです。スコアの隣にある `affectedKeys` は、このカテゴリがプロジェクトのどれだけに触れたかを示します——スコアが深さなら、こちらは到達範囲です。

この段落が伝えていないことがもう二つあります。

- キーごとのスコアは**同一カテゴリ内でのみ**比較可能です。カテゴリをまたいだ数値が示すのは、どちらの問題がより深刻かではなく、どちらのカテゴリで自身のチェックがより多く失敗しているかです。
- `inventories` は一つの組に属するすべてのキーの分母を示すので、ルートごとのカテゴリスコア(`routes[].categories`)はそこから手計算で検算できます。これが成り立つのは、キーがルート ID かソースファイルパスのどちらか一方であり、この二つの空間が重ならないため、あるキーの1カテゴリ内の結果が必ず単一のスコープに属するからです。ルート自身の `score` はそうはいきません。複数の組にまたがるルートでは、触れたすべての組の生の重みを合計してから一度だけ下限値を適用するのに対し、`inventories` は各組を個別に下限値適用済みで公開しているため、両者は食い違うことがあります。

次の2つのフィールド名は、取り違えても**エラーにならず静かに空振りする**ため、特に注意してください。

- ルールの識別子は **`id`** です(`rule` ではありません)
Expand Down
Loading