Baseline / Allowlist
Ein striktes CI-Gate („Build bei jedem fail-Ergebnis abbrechen“) blockiert die Einführung, wenn ein Team eine bestehende, unvollkommene Website zum ersten Mal scannt — es kann nichts ausliefern, bis jeder bereits vorhandene Verstoß behoben ist. Mit einer Baseline kann ein Team sagen: „Diese N sind bereits bekannt — daran soll das Gate nicht scheitern, wohl aber sofort, sobald ein tatsächlich neuer auftaucht.“
# Once: record every current fail occurrence, does not fail the build.$ surea11y scan ./dist/index.html --write-baseline baseline.json# Commit baseline.json to git.# From then on, in CI: only a NEW (not-yet-baselined) fail occurrence gates the build.$ surea11y scan ./dist/index.html --baseline baseline.json
Die Baseline-Datei ist dafür gedacht, in git committet zu werden. Weitere bekannte Barrieren hinzunehmen, statt sie zu beheben, wird so zu einer expliziten, überprüfbaren Entscheidung — ein neuer Verstoß erscheint als neue Zeile im Diff der Datei in einem PR, nicht als stille Änderung einer Zahl.
Wann das nützlich ist
- Scans auf einer bestehenden Website einführen. Der oben beschriebene Ausgangsfall: Ein erster Scan einer gewachsenen Website kann Hunderte bereits vorhandener Verstöße zutage fördern. Werden sie einmal in die Baseline aufgenommen, ist Gating sofort möglich, statt „erst alles beheben“ zu verlangen.
- Schrittweise Behebung mit sichtbarem Burn-down. Beheben Sie Verstöße in Etappen und erzeugen Sie die Baseline regelmäßig mit
--write-baselineneu. Der git-Diff jeder Neuerzeugung schrumpft, weil Einträge verschwinden — die Datei selbst wird zum Fortschrittsprotokoll, und ein PR, der eine Reihe von Problemen behebt, zeigt genau, was bereinigt wurde. - Inhalte von Drittanbietern, die Sie nicht kontrollieren. Eine Seite bettet ein Widget ein (Chat-Bubble, Werbefläche, Zahlungs-iframe) mit bekannten Verstößen, die Sie nicht beheben können. Genau diese Vorkommen in die Baseline aufzunehmen ist präziser, als den ganzen Teilbaum per
excludeSelectorsauszuschließen (siehe den Leitfaden zu den Engine-Optionen) — neue Probleme, die irgendwo sonst in der Nähe entstehen, auch in Ihrem eigenen Code, lösen das Gate weiterhin ganz normal aus. - Sicherheitsnetz gegen Regressionen während eines großen Refactorings. Mitten in einer Migration können einige Regeln vorübergehend und berechtigterweise fehlschlagen, auf eine Weise, die bereits erfasst ist und über viele PRs hinweg behoben wird. Wenn diese bekannte, in Arbeit befindliche Menge in der Baseline steht, werden davon unabhängige neue Regressionen aus anderen PRs im selben Zeitraum trotzdem erkannt, statt das Gate ganz abzuschalten oder jeden PR daran scheitern zu lassen.
- Schrittweise Einführung einer neuen oder strengeren Regel. Sie aktivieren eine Regel (oder eine ganze WCAG-Stufe), die die Codebasis noch nicht erfüllt: Nehmen Sie die bestehenden Lücken in die Baseline auf, aktivieren Sie das Gating sofort und verhindern Sie jeden neuen Verstoß gegen diese Regel, während der Rückstand separat abgearbeitet wird — statt mit dem Aktivieren der Regel zu warten, bis die Codebasis bereits konform ist.
Wie das Matching funktioniert
Nichts an der Form eines Vorkommens ist eine perfekte, von der Position auf der Seite unabhängige Identität (siehe das Ausgabeschema): selector und structuralPath werden beide aus der Position des Elements im DOM abgeleitet und können sich deshalb verschieben, wenn sich unabhängiges Markup an anderer Stelle der Seite ändert, obwohl sich das gemeldete Element selbst nie geändert hat.
Stattdessen ist die Identität eines Baseline-Eintrags:
ruleId + reasonCode + html
Dabei ist reasonCode der regelspezifische Code aus occurrence.data.details.reasonCode (standardmäßig "DEFAULT", wenn eine Regel keinen setzt oder etwas anderes als einen nicht leeren String setzt), und html ist das Outer-HTML-Snippet des Vorkommens. Das ist inhaltsbasiert statt positionsbasiert: Es bleibt stabil, wenn sich das gemeldete Element auf der Seite verschiebt (eine Umsortierung, ein unabhängiges Geschwisterelement kommt hinzu oder fällt weg), solange sich das Markup des gemeldeten Elements selbst nicht ändert. Bei einem Befund zur Seite selbst, der an <html> oder <body> gemeldet wird (html-lang-attr-present, bypass-blocks-present, landmark-one-main), ist der Ausschnitt nur das Start-Tag, sodass eine Änderung am Inhalt der Seite ihn nicht neu macht. Der Ausschnitt wird mit den Attributen jedes Tags nach Namen sortiert und mit sortierten Klassennamen verglichen, sodass ein Build, der class="b a" src schreibt, wo der vorige src class="a b" geschrieben hat, übereinstimmt; Baseline-Dateien, die vorher geschrieben wurden, stimmen weiterhin überein, da der Vergleich beim Abgleich erfolgt. selector wird zwar weiterhin in der Baseline-Datei gespeichert, aber nur als Kontext für Menschen beim Lesen eines Diffs — für das Matching wird er nie verwendet.
Bekannte Einschränkung: Ein Element, dessen eigenes Markup dynamischen Inhalt enthält (einen Zeitstempel, einen Live-Zähler, eine zufällig erzeugte id), stimmt zwischen zwei Scans nie mit sich selbst überein, da sich sein html-Snippet jedes Mal unterscheidet. Wenn Ihre Seiten diesen Fall betreffen, hilft der Baseline-Mechanismus für diese Regeln und Elemente nicht — siehe „Alternative“ unten.
Das Matching zählt Vorkommen, nicht nur deren Vorhandensein. Wenn eine Seite 3 Elemente enthält, die byte-identische ruleId+reasonCode+html erzeugen (z. B. dieselbe fehlerhafte Komponente dreimal wiederholt), und die Baseline nur 1 davon erfasst hat, meldet ein neuer Scan 1 bekanntes und 2 neue Vorkommen — nicht alle 3 als bekannt.
Ein Befund in einem Frame eines Frame-übergreifenden Ergebnisses (runa11yCoreAcrossFrames) wird zusätzlich an seinem Frame unterschieden: Sein Eintrag speichert den Pfad des Frames, die Selektoren der Frame-Elemente, die zu ihm führen ("frame": ["#checkout"]), und passt nur auf einen Befund im Frame unter diesem Pfad. Dieselbe fehlerhafte Komponente in der Seite und in einem Frame ergibt zwei Befunde. Einträge ohne frame gehören zur Seite selbst, sodass eine Baseline aus einem Scan eines einzelnen Dokuments weiterhin auf die Seite passt.
Ein kompaktes Ergebnis (engineOptions.output.detail: 'findings', siehe Ausgabeschema) wird als das vollständige Ergebnis gelesen, für das es steht, und daher genauso gemeldet.
Baseline-Einträge, die in einem neuen Scan auf nichts mehr passen, werden als veraltet (stale) gemeldet: Der Verstoß wurde vielleicht behoben, oder das Markup des Elements hat sich geändert, oder der Scan hat es nicht erreicht (ein anderer Viewport, Bereich oder Regelsatz). Das ist rein informativ und lässt den Build nie fehlschlagen; sehen Sie sich diese Einträge an und erzeugen Sie die Baseline dann regelmäßig mit --write-baseline neu, um sie zu bereinigen.
Befunde mit Tendenz zu fail
Eine Baseline hält Fehlschläge fest. Ein unentschiedener Befund ist keiner, aber seit 1.14 tendieren manche zu fail: Die Hinweise der Seite deuten auf einen Fehlschlag, ohne ihn zu beweisen (ihr lean, siehe Ausgabeschema). Ein Build, der an ihnen scheitert (leanFailAs im JUnit- und im SARIF-Bericht), braucht einen Weg, die bereits vorhandenen einer Seite zu akzeptieren, so wie er ihre Fehlschläge akzeptiert. Fordern Sie sie beim Erstellen der Baseline an:
const { buildBaselineEntries } = require('@surea11y/core/baseline');const entries = buildBaselineEntries(result, { leanFail: true });
Jeder Befund, der zu fail tendiert und unter leanFailAs den Build scheitern lässt (leaningFailures(result) aus @surea11y/core), bekommt nach den Fehlschlägen einen Eintrag mit "lean": "fail":
{ "ruleId": "heading-order", "reasonCode": "HEADING_ORDER_SKIPPED_LEVEL", "selector": "html > body > main > h3", "html": "<h3>Details</h3>", "lean": "fail" }
Ein solcher Eintrag passt nur auf einen Befund mit Tendenz zu fail und derselben Identität, ein gewöhnlicher Eintrag nur auf einen Fehlschlag. Unter leanFailAs scheitert JUnit nicht an einem Befund, auf den er passt (seine <skipped>-Meldung zählt ihn zu den bekannten wahrscheinlichen Fehlschlägen), und SARIF belässt ihn bei warning. Ohne leanFailAs ändern die Einträge nichts, und matchBaseline lässt sie aus seinen Zählungen heraus, veraltete eingeschlossen, außer mit { leanFail: true }: Dann vergleicht es sie in leanFail ({ total, knownCount, newCount, newOccurrences, staleCount }). --write-baseline schreibt nur Fehlschläge.
Leans kommen in Minor-Releases hinzu, ein Upgrade kann also wahrscheinliche Fehlschläge bringen, die die Baseline nicht enthält: Erstellen Sie sie nach einem Upgrade mit leanFail: true neu und prüfen Sie den Diff. Wie Leans ausgewählt und gemessen werden, steht in LEANS.md auf GitHub.
Upgrade
Die Identität eines Befunds enthält seinen Reason-Code, und ein Minor-Release kann einem Befund einen neuen geben: Eine Regel, die keinen gab (DEFAULT), gibt jetzt einen, oder ein Code wird nach Ursache in zwei geteilt (siehe Identität eines Befunds in API_STABILITY.md auf GitHub). Der Befund ist derselbe, und eine vor dem Upgrade geschriebene Baseline passt weiterhin auf ihn: Jede Verschiebung steht in src/baseline-moves.json (Regel, alter Code, neue Codes), und ein Eintrag unter dem alten Code passt auf den Befund unter einem neuen, wenn der Rest seiner Identität, die Regel, das Markup und der Frame, gleich ist. matchBaseline und die Option baselineEntries des SARIF- und des JUnit-Berichts lesen sie alle. Ein Eintrag wird zuerst unter dem eigenen Code des Befunds gesucht, geht also nie an einen verschobenen Befund, wenn ein anderer Befund genau diese Identität hat.
1.14 verschiebt diese, gegenüber 1.13:
| Regel | Alter Code | Neuer Code |
|---|---|---|
img-alt-present, embed-text-alternative-present, object-text-alternative-present, svg-image-text-alternative-present, svg-text-alternative-present, video-poster-text-alternative-present | DEFAULT | TEXT_ALTERNATIVE_MISSING |
area-alt-present | DEFAULT | TEXT_ALTERNATIVE_MISSING, TEXT_ALTERNATIVE_EMPTY |
canvas-text-alternative-present | DEFAULT | TEXT_ALTERNATIVE_MISSING, ROLE_IMG_NAME_MISSING |
area-alt-quality, canvas-text-alternative-quality, embed-text-alternative-quality, input-image-alt-quality, object-text-alternative-quality | DEFAULT | TEXT_ALTERNATIVE_REVIEW |
img-alt-quality, svg-text-alternative-quality | DEFAULT | TEXT_ALTERNATIVE_REVIEW, IMAGE_ONLY_CONTROL_CONTENT |
img-alt-decorative | EXCLUDED_ELEMENT_REVIEW | IMAGE_ONLY_CONTROL_CONTENT |
input-image-alt-decorative | DEFAULT | NAME_FROM_ELSEWHERE_REVIEW |
manual-review | DEFAULT | KEYBOARD_REVIEW |
list-children-valid | LIST_INVALID_CHILD | LIST_ITEM_ROLE_OVERRIDDEN |
listitem-parent-valid | LISTITEM_INVALID_PARENT | LIST_ROLE_OVERRIDDEN |
aria-prohibited-children | ARIA_PROHIBITED_CHILD_ROLELESS | ARIA_PROHIBITED_CHILD |
duplicate-id | DUPLICATE_ID | DUPLICATE_ID_IN_SHADOW_TREE |
valid-lang | ELEMENT_LANG_INVALID | DEFAULT_LANG_INVALID |
p-as-heading | BOLD_LARGE_PARAGRAPH, BOLD_LARGE_DIV | HEADING_LIKE_NUMBER, HEADING_LIKE_LINK |
Die Tabelle gilt für ein Minor-Release: Eine Verschiebung steht darin ab dem Release, mit dem sie kam, bis zum nächsten Minor-Release, das sie entfernt. Eine von 1.13 geschriebene Baseline passt also unter 1.14.x weiter und muss vor 1.15 neu erzeugt werden.
Die Baseline nach einem Upgrade mit --write-baseline (oder buildBaselineEntries) neu zu erzeugen, schreibt jeden Befund unter seinem aktuellen Code und macht die Tabelle überflüssig. Es akzeptiert aber auch jeden Befund, den das Upgrade neu meldet; prüfen Sie also den Diff. Um die Codes zu aktualisieren, ohne etwas Neues zu akzeptieren, schreiben Sie die Datei mit migrateBaselineEntries aus @surea11y/core/baseline um:
const { migrateBaselineEntries } = require('@surea11y/core/baseline');const file = JSON.parse(fs.readFileSync('baseline.json', 'utf8'));// With the scan's result, an entry for a split code takes the code its finding has now.file.entries = migrateBaselineEntries(file, result);fs.writeFileSync('baseline.json', JSON.stringify(file, null, 2) + '\n');
Die Funktion gibt eine Kopie der Einträge zurück, jeden verschobenen unter seinem neuen Code und alle anderen unverändert. Ohne Ergebnis schreibt sie nur die Verschiebungen um, die sie ohne den Befund erkennen kann: einen Code, den die Regel nicht mehr gibt und der zu genau einem neuen wurde (das DEFAULT von img-alt-present), keine Teilung.
Ein Befund, dessen Ergebnis sich mit dem Upgrade geändert hat, ist etwas anderes: Eine Regel, die jetzt mit fail bewertet, wonach sie fragte, oder nach dem fragt, was sie mit fail bewertete, gibt einen Befund, den die Baseline nicht enthielt, oder keinen mehr, den sie enthielt, und das Changelog sagt, warum.
Scannen bei mehreren Viewport-Breiten
Ein Baseline-Eintrag hält den Viewport nicht fest, und das Matching verwendet ihn nicht: Dasselbe Element mit demselben Mangel passt bei jeder Breite. Das passt zu einer Seite, deren Befunde nicht von ihrem Layout abhängen. Eine layoutabhängige Regel wie text-spacing-content-loss oder target-size-minimum kann bei einer Breite fehlschlagen und bei einer anderen nicht. Führen Sie daher für eine Seite, die bei mehreren Breiten gescannt wird, eine Baseline pro Breite: a11y-baseline-390.json, a11y-baseline-1280.json.
Eine einzige Baseline, die aus allen Breiten geschrieben wurde, steuert den Build trotzdem korrekt, denn ein Befund ist nur dann neu, wenn kein Eintrag auf ihn passt. Dann meldet aber jeder Lauf die Befunde der anderen Breiten als veraltet, und die Zahl der veralteten Einträge sagt Ihnen nicht mehr, was behoben wurde.
Dateiformat
{"version": 1,"generatedAt": "2026-07-30T12:00:00.000Z","entries": [{ "ruleId": "img-alt-present", "reasonCode": "TEXT_ALTERNATIVE_MISSING", "selector": "html > body > img", "html": "<img src=\"logo.png\">" }]}
--baseline <path> lehnt eine Datei, die nicht { version: 1, entries: [...] } ist, mit einem klaren Fehler (Exit-Code 2) ab, statt ein älteres oder anderes Format zu erraten. matchBaseline und die Option baselineEntries der SARIF- und JUnit-Reporter nehmen die Datei so, wie sie gespeichert ist (JSON.parse davon), oder ihre entries-Liste.
Kombination mit --json
Wenn --baseline oder --write-baseline zusammen mit --json verwendet wird, erhält das ausgegebene Objekt neben dem normalen Engine-Ergebnis einen Schlüssel baseline (das betrifft nur die CLI-Ausgabe — es ist nicht Teil des im Ausgabeschema beschriebenen Ergebnisvertrags der Engine):
--write-baseline:{ mode: "write", path, entries: <number written> }--baseline:{ mode: "check", totalFail, knownCount, newCount, staleCount, newOccurrences: [...] }
Alternative: zwei Scans selbst vergleichen
Wenn Ihre Seiten nicht in dieses Modell passen (viel dynamischer Inhalt innerhalb der gemeldeten Elemente selbst), bleibt als Ausweg immer, zwei vollständige --json-Ausgaben selbst zu vergleichen — siehe INTEGRATION.md. Damit haben Sie die volle Kontrolle über die Identitäts- und Matching-Logik, müssen sie aber selbst schreiben.