Reporters
Choose how svelte-vitals formats and outputs its findings.
svelte-vitals supports seven output reporters. Select one with --reporter <fmt>, or let auto-selection pick the right one for your environment.
Available reporters
console (default)
Human-readable text output, suitable for terminal use. Groups findings by severity and includes route paths and file locations.
svelte-vitals --reporter console
json
Machine-readable JSON output. Useful for scripts, dashboards, or feeding results into other tools.
svelte-vitals --reporter json
Shape
{
"version": "0.35.0", // the svelte-vitals version that produced this report
"score": 97, // combined Health score, 0-100 (floored: 100 means zero deduction)
"weights": { "seo": 1 }, // per-category Health weights actually applied
"categories": {
"seo": {
"score": 94,
"scoreModel": {
"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 },
"rules": {
// Every rule that ran. An entry with `findings: 0` ran and reported nothing;
// a rule missing from this map was disabled at the top level — `--ignore`, `--rules`,
// `--category`, or `rules: { id: 'off' }` in config. A rule disabled through an
// `overrides` entry instead still ran and still appears here (see below).
"architecture/unit-entry-file": { "findings": 0, "passed": 12 }
},
"routes": [
{
"route": "/about", // a route id, or a source file path for file-scoped rules
"score": 95, // share of this route's rule inventory (by category/scope, weighted by severity) left intact
"categories": { "seo": 94 }, // per category present on this route, scored against that category's own inventory
"issues": [
{
"id": "seo/single-h1", // the rule id
"category": "seo",
"severity": "warning", // after any severity override you configured
"title": "Two <h1> elements", // the human-readable finding
"detection": { "presence": "none", "value": "absent" },
"location": "src/routes/about/+page.svelte",
"line": 12,
"recommendation": "Keep exactly one <h1> per page.",
"docsUrl": "https://svelte-vitals.dev/rules/seo/single-h1",
"fix": { "description": "…", "snippet": "…", "lang": "svelte" }
}
]
}
],
"siteIssues": [], // findings with no route (robots.txt, sitemap.xml, …), same issue shape
"inventories": {
"seo::route": 100 // floored severity weight behind every "seo" key scored against "route"
},
"examined": {
"architecture/reserved-name-placement": {
"capitalisedUnitPlacements.parts → src/**": 28 // places this declaration judged, permitted or rejected
}
},
"skipped": {
"a11y/no-missing-id-ref": [
{
"route": "/checkout",
"refs": 2,
"causes": [{ "kind": "component", "detail": "Textbox", "file": "src/routes/checkout/+page.svelte", "line": 12 }]
}
]
}
}
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, which is what the keys of inventories like seo::route name. 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, whatever inventories reports for it, which makes each of its findings a larger share; 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.
Some things follow that the paragraph above doesn’t say directly:
-
A floored group is clamped, not measured. A group holding fewer points of checks than the floor reports the floor in
inventories, so a group with one rule and a group with eight can look identical there. The number is the divisor a score used, not a count of what ran;rulesis where you see which checks actually reported. -
A
::projectgroup’s number divides nothing. Findings with no route, robots.txt and sitemap.xml and their kind, are absolute deductions: awarningthere costs its category a flat 5 points whatever the project’s size. Those entries appear ininventoriesfor uniformity only. -
keyscounts per category, not per project. A category’skeysis the number of keys that category touched, so the denominators differ between categories on one run. A project can showseoat 13 keys andarchitectureat 334. -
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.
-
inventoriesgives 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 ownscoredoes 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, whileinventoriespublishes 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, notrule; - the finding text is
title, notmessage.
line, docsUrl and fix are present only when the rule supplies them, and location only for a finding tied to a file. issues lists failing findings only; summary.passed counts the passing checks but does not list them. A route with no failures still appears in routes, with an empty issues array and its own score.
categories holds only the categories that produced a result on that route. An absent category means “not measured here,” not “perfect here.” Its values are not guaranteed to average to the route’s own score, in either direction: score is one ratio over everything the route was measured against, while each category score uses that category’s own inventory. They agree whenever every category on the route scores the same ratio (including every route with no findings) and can differ by several points otherwise.
rules answers a question the rest of the report cannot: whether a rule ran at all. issues lists
only failing findings, so a rule that found nothing leaves no trace there, and a rule disabled at the top
level (--ignore, --rules, --category, or rules: { id: 'off' } in config) leaves the same absence.
Look it up in rules instead: present means it ran, missing means it was excluded at the top level. One
exception follows.
The counts in rules describe the report, not the tree. svelte-vitals applies baseline, suppression and
--diff filtering before it builds the report, so a rule whose findings were all suppressed shows
findings: 0 while remaining present. The same is true of a rule disabled through an overrides entry rather than at the top
level: overrides drops its results (passing ones included) after the rule has already run, so it shows
{ "findings": 0, "passed": 0 }, indistinguishable from a selected rule that simply found nothing.
Presence in rules proves a rule wasn’t excluded by --ignore, --rules, --category, or config’s
top-level rules; it does not prove overrides left anything for it to find.
examined is a separate top-level map: rule id, then declaration label, then a count of how many places
that declaration judged, whether it permitted them or rejected them. --diff, --baseline and suppressions
do not narrow it. Those apply to rules, and examined describes the analysis rather than the report,
which is why it sits beside rules instead of inside it. The map itself is optional: a run in which
no rule reported counts omits examined entirely. When it is present, there are three states. A rule that
reports no counts has no entry. A rule that counts but whose configuration declares nothing has an empty
entry; enabling architecture/reserved-name-placement in top-level rules without declaring a placement
reaches exactly that. A declaration that judged nothing is present with 0. The entries that do appear use
the exact declaration label the rule’s own diagnostic names.
architecture/reserved-name-placement’s aggregated finding and its examined entry share the string
capitalisedUnitPlacements.parts → src/**, so you can read the two together. A non-zero count is what tells
you the declaration was checked at all, the question a rule with no findings otherwise leaves open.
skipped is a second analysis-side map, keyed by rule id. Today only
a11y/no-missing-id-ref uses it: the rule needs a fully resolved route composition, and a
route that fails that bar is skipped without a result. Each entry lists the skipped route,
its literal id-reference count in refs (a route with 0 would produce nothing even if it
ran), and the causes that broke the closed world, each with kind (component, spread, html,
or dynamic-id), the first offending file and line, and for components the name as
written in detail. Like examined, it describes the analysis rather than the report, so
--diff, --baseline and suppressions do not narrow it, and it is omitted when no
analyzed route was skipped.
agent
A Markdown remediation document designed for AI coding agents. Each failing finding includes:
- The route and source file location
- A concrete code fix with a snippet
- An acceptance check
svelte-vitals auto-selects the agent reporter when it detects a recognized AI-agent harness (Claude Code, Cursor, Codex, and others), or when SVELTE_VITALS_AGENT=1 is set. That variable is the universal opt-in for a harness gunshi doesn’t recognize yet. Detection is delegated to gunshi’s agent profile, so the recognized list evolves with it rather than being hard-coded here. On an auto-selection you did not explicitly request, svelte-vitals prints a one-line hint to stderr explaining how to override.
svelte-vitals --reporter agent
Override auto-selection via the environment variable:
SVELTE_VITALS_REPORTER=agent svelte-vitals
sarif
SARIF v2.1 format, compatible with GitHub Code Scanning, Azure DevOps, and other SAST tooling that consumes SARIF.
svelte-vitals --reporter sarif
github
GitHub Actions workflow command format. Outputs ::error and ::warning annotations that appear inline in pull requests.
The github reporter is auto-selected when GITHUB_ACTIONS=true is set (which GitHub Actions sets automatically).
svelte-vitals --reporter github
md
A compact Markdown summary: Health score, per-category score table, severity counts, and a
findings table with links to each rule’s docs page. Designed for a GitHub Actions job summary or
a PR comment; capped at 50 finding rows to stay within GitHub’s comment size limits. See the
CI integration guide for svelte-vitals ci install, which wires
this reporter into a generated workflow automatically.
svelte-vitals --reporter md
HTML report
--reporter html writes a self-contained HTML report. It is the same UI as the live dashboard, one shared renderer, so the two can’t drift: searchable sortable route list, severity/category filters, dark mode, and a copy-to-clipboard AI Prompt on every finding.
A static file has no dev server behind it, so the live-update machinery (SSE, measured refinement) is absent. All CSS and JS are inlined, so it works offline and uploads as a CI artifact unchanged.
svelte-vitals --reporter html # writes svelte-vitals-report.html
svelte-vitals --reporter html --out-file report.html
svelte-vitals --reporter html --out-file - # write to stdout instead of a file
By default it writes svelte-vitals-report.html in the current directory and prints the path to stderr. Use --out-file <path> to change the location, or --out-file - to stream it to stdout (for piping or CI artifacts).
Auto-selection priority
- Explicit
--reporter <fmt>. Always wins. SVELTE_VITALS_REPORTERenvironment variable. Overrides auto-detection.- A recognized AI-agent harness (Claude Code, Cursor, Codex, and others) or
SVELTE_VITALS_AGENT=1→agent. - GitHub Actions (
GITHUB_ACTIONS=true) →github. - Default →
console.
Example: CI pipeline
- name: Check SEO
run: npx svelte-vitals@latest --fail-on warning
# GITHUB_ACTIONS is already set; github reporter is auto-selected