Skip to content

Adopt a baseline on an existing codebase

If your project already has thousands of accessibility findings, you do not have to fix them before you start gating. Record what exists today as a baseline, commit it, and b8e ci fails the build only on findings that are not in it. Existing debt stays visible and stops blocking; anything new is caught from the first pull request.

From the project root:

Terminal window
b8e baseline .
binclusive baseline — snapshotted 1284 finding(s) → binclusive-baseline.json

The snapshot is unscoped. It runs the same multi-stack scan the gate runs, over the whole project, with no --base and no severity filter — so it records warn-level findings as well as block-level ones. That is deliberate: the day you raise enforcement with b8e ci --enforce warn, or move a success criterion into your block list, the debt that was only advisory yesterday is already accounted for and cannot surprise the build.

Source findings only. b8e baseline runs the static source collectors and nothing else — the same set b8e ci runs. It never opens a browser, so findings from a live-page scan (b8e scan --url) are not in the snapshot. That is not a gap you need to close: b8e ci does not scan URLs either, so there is nothing on that side for a baseline to grandfather. Baselining is for the source backlog, which is the backlog a build gates on.

The file lands at <dir>/binclusive-baseline.json, beside binclusive.json. Write it somewhere else with --output:

Terminal window
b8e baseline . --output config/a11y-baseline.json

b8e baseline needs a credential to run, the same as every other b8e command: a b8e auth login session, or a BINCLUSIVE_API_KEY. With neither, the command exits 7 and tells you to sign in.

Holding a credential does not mean anything is sent. b8e baseline reads your source and writes one file; it uploads nothing, with or without a key.

Terminal window
git add binclusive-baseline.json
git commit -m "chore: baseline current accessibility findings"

Treat it as a source file, not a build artifact: it belongs in review and it belongs in diffs. Entries are sorted by fingerprint, so re-running b8e baseline . with no source change rewrites the same bytes and produces an empty git diff.

b8e ci auto-detects binclusive-baseline.json next to the directory it scans. If the file is at that default path, your existing workflow step needs no change at all:

Terminal window
b8e ci --base origin/main

Pass --baseline only when the file lives somewhere else:

Terminal window
b8e ci --base origin/main --baseline config/a11y-baseline.json

The run says how much the baseline absorbed:

binclusive ci — 12 file(s) under /repo, 37 finding(s), 34 baselined, 3 gating (enforce: block)
  • A finding in the baseline is subtracted before the gate. It does not fail the build and it does not count toward --max-violations. It is not hidden, though: it is still scanned, it is counted in the run’s finding total and in the N baselined tally, and it is still uploaded to your dashboard. What it loses is its line in the gating list and its vote on the exit code.
  • Where it shows up in machine-readable output depends on whether that surface remembers:
    • --format json carries it plain — the export is the whole set.
    • --format sarif carries it marked suppressed, so a code-scanning alert stays honest rather than reading as fixed.
    • --format github omits it — an inline annotation asks you to act now, and accepted debt re-annotated on every pull request is the noise a baseline exists to remove.
  • A finding not in the baseline gates exactly as it would with no baseline at all.
  • A finding the scanner cannot locate — and therefore cannot fingerprint — is never subtracted. Baseline mode only ever removes proven matches; it never waves through an unmatched finding.

Two scopes are in play and they are not the same one. The baseline is whole-project, because a partial snapshot would record only part of your debt. b8e ci --base <ref> is changed-file scoped. A gating run therefore sees the findings in the files that pull request touched, minus everything the baseline already knew about.

A baseline entry is { ruleId, location } and nothing else. Every entry is a source finding, so the location is the repo-relative path, a hash of the offending line’s content, and an index that separates identical lines within one file. It is deliberately not file:line.

Entries do also carry a line number, and it is easy to misread when you open the file: it is recorded for display, so a tool has something to link to. Nothing matches on it, and it is allowed to go stale. So:

  • Lines shift — you add an import above it, or reformat the file. Still matched, still grandfathered. This is the case the design is for.
  • The offending line’s content changes. No longer matched. If the finding still fires it gates as new, which is right: you edited that line and did not fix it.
  • The file is renamed, or the code moves to another file. No longer matched, so it gates. If a large move is not the moment to fix those findings, regenerate the baseline in the same commit.

A broken baseline is a crash, never a quiet pass

Section titled “A broken baseline is a crash, never a quiet pass”

A baseline that cannot be trusted must not be allowed to disable the gate, so b8e ci refuses to run rather than pass green:

  • Unreadable file, invalid JSON, or a shape violation — the run fails with exit 2 (crash), distinct from the 1 a findings failure exits with. Do not let a continue-on-error step swallow it.
  • --baseline <path> pointing at a file that is not there — an error. You asked for a baseline that does not exist.
  • No --baseline and no binclusive-baseline.json — the one tolerated empty state. The gate runs unchanged, as if no baseline existed, because none does.

See the exit-codes reference for the full contract.

A baseline nobody ever reduces is technical debt with a green tick on top. The file is a static snapshot — it never learns that you fixed something, and an entry that outlives the finding it grandfathered will happily grandfather that finding’s reintroduction.

b8e ci tells you when that is happening. On a whole-project run — no --base — it counts the entries that matched nothing:

BASELINE — 41 of 1284 recorded issues are already fixed
run `b8e baseline .` to update the file — until you do, those issues won't fail the build if they come back

That is a report, not a gate: it never changes the exit code, and b8e ci will not rewrite a committed file on your behalf.

It appears only on an unscoped run, and that is worth planning around. Under --base, an entry elsewhere in the repo matched nothing because the run never looked at it — calling that “already fixed” would be a lie, printed on every pull request. So run b8e ci with no --base on a schedule (nightly, or on pushes to your default branch) and read the BASELINE line as your burn-down signal.

When it fires, regenerate and commit:

Terminal window
b8e baseline .
git add binclusive-baseline.json
git commit -m "chore: prune fixed findings from the a11y baseline"

The diff is the debt you paid.

binclusive.json has hand-authored escape hatches — learned rules, and the components declarations that pin a wrapper to its host element. You write those by intent, one at a time, because you decided a specific thing should be treated differently, and you expect them to stay.

A baseline is the opposite: bulk, machine-generated, and meant to be burned down. The two never mix. b8e baseline never writes into binclusive.json, and no baseline entry is ever folded into learned — which is what keeps deliberate suppression legible next to grandfathered debt. The committed file says so in its own note field.

If what you actually want is to teach the scanner about a component it cannot resolve, that is Declare a component the scanner can’t resolve, not a baseline.

For every flag these commands accept, see the baseline reference and the ci reference.