Reports and scoring
The report object
audit() always resolves to the same structure, whatever options you passed.
A category entry carries its own score and its finding counts:
A page entry carries two channels:
Findings and measurements
A finding is a verdict: the check ran, and the page passed or it did not. A measurement is a number: the title is 82 characters, the page is 340KB, these 14 URLs are linked more than once.
They are separate because they answer different questions, and because forcing a number through a finding means inventing a rule to hang it on. A measurement carries its own verdict instead:
ValueStatus never touches the score. Nothing in metrics can move the number, so a report can carry as much detail as it likes without diluting what a failing rule means.
Measurements come in two shapes.
Tables are capped at 25 rows so a report stays readable. total is the count before the cap, and every format prints "showing 25 of 340" rather than quietly truncating.
Reading them back:
Rendering
render is synchronous and takes a report you already have, so you can audit once and emit all three.
HTML is one self-contained file. Inline styles, no external requests, no webfonts, nothing to serve alongside it. It is the format to hand to someone who will not read JSON. Findings are grouped by category and rule; measurements sit under their own section, one block per page.
It carries the same brand layer as this site: the near-black canvas, the one yellow accent on scores and section labels, flat card surfaces with no shadows, and the light band it closes on. Type follows the same two roles, the text face for anything read as language and a tabular mono for numbers only. There is no light mode, because the system does not have one.
Markdown is per-category headings with a findings table and the score summary, then a measurements section per page. Good for pasting into an issue or a pull request comment.
JSON is the report object, stringified, findings and measurements both. For pipelines, dashboards, and diffing one run against the next.
The 25-row cap is applied when the measurement is collected, not when it is rendered, so it holds in JSON too. It keeps a twenty-page crawl from turning into a report nobody can open.
How the score is calculated
Two different formulas run, and the difference is deliberate.
Page and category scores
Findings are weighted by severity:
A pass earns full weight. A warn earns a quarter of it. A fail earns nothing. Findings with status info are excluded from scoring entirely, since they report a value rather than a verdict.
A page with no scoreable findings scores 100.
The overall score
The headline number does not average the categories, because an average buries a handful of real errors under a pile of passing checks. Instead it starts at 100 and deducts.
For each rule, the worst status it reached on each page is counted, then:
with these weights:
So one error rule failing on every page costs 22 points on its own. The same rule failing on a quarter of the site costs 5.5. This is why the overall score sits lower than the category scores, and why it moves when you fix something real.
Reading findings
A rule that fails on every page is a template problem and one fix clears it. A rule that fails on three pages is content work. Sorting by count tells you which is which.