Every accessibility tool says it is accurate. Here is our receipt.
Marlo checks your pages against the official ACT rule corpus. Then it turns the same corpus on itself and commits the score, including the part where it loses.
$ marlo scan apps/demo/checkout.html apps/demo/settings.html apps/demo/checkout.html static renderer dom, script NOT EXAMINED 2 rules need layout and paint, which this renderer does not provide. 09o5cg afw4f7 This is not a pass. Use --renderer browser to evaluate them. ▲▲▲ critical e086e5 Form field has non-empty accessible name WCAG 1.3.1, 2.5.3, 4.1.2 axe-core precision 1.00 over 17 official test cases input[name="postcode"] (not located in source) Form elements must have labels ▲▲ serious afw4f7 Text has minimum contrast WCAG 1.4.3, 1.4.6 alfa precision 0.83 over 28 official test cases INVARIANT alfa reported a failure the routed engine did not, so Marlo may not report clean. disagreement: alfa says failed disagreement: htmlcs says passed xpath /html[1]/body[1]/main[1]/p[1]/text()[1] (not located in source) The highest possible contrast of the text is 3.45:1 which is below the required contrast of 4.5:1 ------------------------------------------------------------------------ 38 findings 0 fixed 0 flagged 2 not evaluated 0 crashed coverage: 41 of 94 published ACT rules one-directional invariant enforced on 2 rule(s): a peer reported a failure the routed engine did not.
In the output above, two rules need CSS layout. That renderer has none, so it says so first, before anything it did find. "No contrast problems found" and "contrast not examined" are different sentences and Marlo will not blur them.
The router sends that ARIA rule to axe-core. Marlo's own rule said it could not tell. axe-core said failed, so the failure goes through and the dissent gets printed underneath it.
That is real stdout from marlo scan over the deliberately broken
pages in apps/demo. Trimmed for length, not edited: the test suite
checks every line of it against the committed golden file.
Detection is not the hard part. Nobody publishing an error rate is.
Lighthouse hands out 100s to pages a screen reader cannot get through. A large AI site builder shipped an unusable product while its own bundled checker reported perfection. The best funded vendor in this category advertises "19x more critical issues" and has never said more than what, or how it counted.
None of that is a detection failure. It is a market where a claim nobody can check is free to make.
So Marlo does the tedious version instead. Four engines, every official W3C test case, precision and recall per rule per engine, table committed to the repository, regenerated on every push. When a number gets worse the build goes red and the changelog says which one.
An improvement fails the build too, if nobody committed the new table. A figure that quietly got better is a figure the README is now wrong about.
pnpm corpus:verify fails on a changed byte. If a test case could
be edited, an inconvenient result could be fixed by changing the question
instead of the answer.
pnpm calibrate in CI. Both a regression and an
uncommitted improvement fail the build.
Six stages, and stage five can override the other five
Marlo routes. It does not pile four engines' output into one list and call the pile thorough. Each rule is reported by whichever engine measured best at that rule, and one stage is allowed to overrule the routing.
-
Render
A Node DOM by default: no browser, no network, milliseconds. Playwright if you want layout, and it is opt-in rather than assumed.
happy-dom, or chromium -
Run four engines
axe-core, Alfa, HTML CodeSniffer, and Marlo's own 41 rules. All in-process. None of them can see the others, and that isolation is a dependency rule rather than a habit.
nobody gets a longer timeout -
Normalise to ACT identifiers
Four vocabularies collapse into one. The axe-core mapping was derived by measurement over the corpus. The other two are documentation matches, and every entry in both says so.
partial mappings are labelled partial -
Route from the table
Whichever engine measured best on a rule reports that rule. 18 go to axe-core, 20 to Marlo, 12 to Alfa, and 0 to nobody at all.
one finding, provenance attached -
Apply the one-directional invariant
Routing picks who speaks. It cannot silence anyone. If any engine reports a failure, Marlo may not report clean. It is allowed to dissent, named, on the record.
tested over all 256 outcome combinations -
Report
Terminal, JSON, SARIF 2.1.0, or a pull request body. Every surface puts what was not examined above what was found.
severity is a text mark first
Why routing and not a union: engines find largely disjoint issue sets and agree poorly, so unioning them lifts recall and buries the signal. One project integrated ten engines and about a thousand rules, got published at SIGACCESS, got adopted inside a Fortune 50 company, and reached almost nobody. Hand somebody ten engines' output and you have turned their detection problem into a triage problem, which is worse.
The list a sales page would leave out
41 of 94 published ACT rules, and automation reaches a minority of WCAG no matter who writes it. The denominator is on every page here for that reason.
Contrast gets detected and located. Never changed. Picking a colour is a design decision and it stays yours.
Decorative images get an empty alt, confidently. A description gets written only where the page already contains the meaning. The rest comes back to you, because a confident wrong description is worse than a missing one. You can spot a gap.
Neither can anybody else. What you get is verified repair against named success criteria, plus an error rate you can look up. That is the whole offer.
Detection, routing, calibration and reporting are.
marlo fix exits with an error saying that the repair layer is not merged,
instead of being a flag that quietly does nothing.
Only the axe-core mapping was derived by measurement. Alfa's and HTML CodeSniffer's are documentation matches, marked partial, and a test fails if either one ever claims otherwise.
Marlo will never sell remediation to anyone it rates.
Every incumbent that both scores you and fixes you has a reason to find work it can bill for. That is not a conspiracy, it is an incentive, and incentives win.
Independence is the only asset a measuring tool owns. Spend it and you do not get it back.
- Merge its own pull requests
- Push to your default branch
- Force push, or rewrite history
- Deploy anything, anywhere
Enforced in the token scopes it asks for and asserted by tests, not promised in prose.
Running it takes about five minutes.
No API key, no browser download, no account, and no network after install. If any of that turns out to be false, it is a bug and we want the issue.