Baselines and the gate
A project rarely starts with no accessibility violations. A baseline records the ones it accepts for now, so cora scan and cora report merge fail only when a change adds a new one.
The baseline is cora.baseline.json in the project root, beside the config, or outside a project in the folder you run cora in (Scanning without a project). Commit it, and review changes to it like code.
Accepting what a project has today
Section titled “Accepting what a project has today”Without a baseline, every violation is new, so the first scan of a project with violations fails:
Gate failed: 9 new issues at or above minorTargets: 11Issues: 10 (9 new, 1 needs review)Duration: 14.2 sResults: .cora/runs/20261006T170501123Z-k3j9x2ab.json
To accept them, run: npx cora baseline update .cora/runs/20261006T170501123Z-k3j9x2ab.jsonThe summary counts issues: 9 new, and 1 that needs review, which is never new.
- Run the command it prints, which writes
cora.baseline.json. It runscoraasnpx cora, oryarn corain a Yarn project, where the project has installedcora, and asnpx @vispero/corawhere it has not, such as outside a project. - Commit the file.
The next scan compares with the baseline, and passes until something new appears.
cora baseline update takes results files, or directories of them, such as every shard of a CI run.
How a violation is matched
Section titled “How a violation is matched”Each violation carries a fingerprint, built from its rule, its page and the element’s selector, so two findings are one issue when they share all three. A page on the origin of baseUrl, from the config, --base-url or CORA_BASE_URL, is its path alone, so a preview deployment and localhost share fingerprints when each run’s baseUrl names the origin it scans. Any other page, and every page of a run with no baseUrl, keeps its host too, so two sites’ issues stay apart (One issue on two sites). A violation is:
- new when the baseline has no entry for it;
- accepted when the baseline accepts it, which the results file records as
unchanged, SARIF’s word for it.
A fingerprint names neither the browser nor the viewport, so an issue counts once however many of them show it. The baseline records each page an accepted issue was seen on: its path, with its host where its fingerprint keeps one, browser, viewport and element. Each such page is then:
- fixed when the run scanned that page completely, in that browser at that viewport, with the issue’s rule, and no longer found the issue. Elements the config’s
page.excludeleaves out do not count against a complete scan. - not verified when the run did not scan that page completely. A page that redirected, a scan of other pages, a scan in another browser or at another viewport, a scan of the same path on another site, and one shard of a sharded run all leave it not verified.
Only a fixed page is removed from the baseline, so a run that checked part of a site never removes what it did not check.
cora baseline update also removes an accepted issue when the config changes:
- when the config starts to leave its element out with
page.exclude, the issue is no longer found, so its page reads as fixed; - when the config stops running its rule on its page,
cora baseline updatedrops it.
With source stamps
Section titled “With source stamps”On a page built with source stamps, a violation also carries a source fingerprint: its rule, its file and an opening tag. Whose tag that is decides what the baseline’s entry accepts.
An element with a stamp of its own is identified by its own opening tag, and the entry cora baseline update writes for it records that place:
- The entry accepts the violation on every page the element appears on.
- It accepts as many places in the file with that same tag as it held when it was accepted. One more such place makes them all new, since the baseline cannot tell which one was added.
An element without one, such as a button that a component from node_modules renders, is identified by its call site: the opening tag of the closest element in your source that encloses it. Every such element under that tag shares the fingerprint, so together they are one issue, such as one unlabeled button in every row of a table.
- The baseline holds one entry for the issue, which lists each element it was accepted on, by its selector.
- The entry accepts those elements only, since a call site cannot tell its elements apart. One more element there is a new violation. So is a listed element whose selector changes, as the last row left in a list can. Accepting either adds it to the same entry.
- The issue is fixed once a run finds it on no element at that call site.
An entry written from a build without stamps keeps matching by the selector.
When you edit an element’s opening tag, the entry still accepts the element by its selector, and cora baseline update records it under its new tag, as one changed issue.
The gate
Section titled “The gate”The gate fails when a run has a new violation at or above policy.failOn, the lowest severity that fails:
import { defineConfig } from '@vispero/cora/config';
export default defineConfig({ policy: { failOn: 'serious' }, // fail only on new critical or serious violations});- Best-practice rules, which no WCAG success criterion requires, fail like any other rule.
- A rule set to
reportin the config’sruleslists its findings in a section of their own. They never fail the gate, and are never baselined. The violations the baseline already accepts for the rule stay accepted, and never count as fixed, while it only reports. - Findings that need review, where axe-core could not decide, such as text over a background image, never fail the gate and are never baselined.
- A violation in a page state an agent reached on its own is reported, but not gated or baselined, since nobody can reach that state again to check it.
Coding agents see the same baseline: cora-mcp’s scan_url marks each issue it accepts, as the gate counts it, and CORA’s fixer leaves those issues out of its changes unless you ask for them (The fixer).
The gate’s verdict sets the exit code of cora scan and cora report merge, which the CLI reference lists.
Targets that were not scanned
Section titled “Targets that were not scanned”The gate has no verdict when a target was not scanned and nothing new failed it.
- A target that redirects to a page the run scanned, such as
/to/en/, counts as scanned there. - A redirect to a page the run did not scan counts as not scanned, as does a target that fails to load. In a crawl, fix such a link, or
excludeit. - The report directory’s
summary.mdlists the redirects, each with the page it landed on. - The baseline’s pages for the requested address stay not verified, never fixed.
Updating the baseline
Section titled “Updating the baseline”Run cora baseline update with the latest results to:
- add every new violation;
- add each page where an accepted issue now also appears;
- remove the pages the run proved fixed, then the entries left with none;
- keep everything the run did not verify.
It prints the file it wrote, then how the baseline’s issues changed:
Wrote cora.baseline.jsonIssues: 9 added, 0 removed, 0 changed, 0 keptIt counts issues, as the scan’s summary does, each issue once:
added: the issues the run found new that the baseline did not hold. From an empty baseline, that is the scan’s count of new issues.removed: the issues the baseline no longer holds, because the run proved them fixed or the config stopped running their rule.changed: the issues it still holds, with a different record, such as a page added or removed, or an element recorded under the opening tag it was edited to.kept: the issues it holds as before.
When the run could not verify some of the issues the baseline still holds, a second line counts them, as in Not verified: 2 issues. Those are the issues the run found on no page and did not scan every page of, counted as the report directory’s summary.md counts its Not verified.
The file is sorted and formatted the same way every time, so its diff in a pull request shows exactly what was accepted or fixed. The cora-baseline reference lists each of its fields.
A baseline written by a newer release of cora can hold fields this release does not know. cora baseline update refuses to rewrite such a file, since it would drop them, and asks you to update cora.