Baseline / allowlist

Un contrôle de CI strict (« faire échouer le build au moindre résultat fail ») bloque l’adoption pour une équipe qui analyse pour la première fois un site existant et imparfait : elle ne peut plus rien livrer tant que chaque violation préexistante n’est pas corrigée. Une baseline permet à l’équipe de dire « ces N violations sont déjà connues, ne bloquez pas le build pour elles, mais faites-le échouer dès qu’une violation réellement nouvelle apparaît ».

# 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

Le fichier de baseline est fait pour être versionné dans git, afin qu’accepter davantage de dette d’accessibilité devienne une décision explicite et relisible : une nouvelle violation apparaît comme une nouvelle ligne dans le diff du fichier au sein d’une PR, et non comme un changement silencieux dans un compteur.

Quand c’est utile

  • Adopter l’analyse sur un site existant. Le cas qui motive tout ce qui précède : une première analyse d’un site mature peut faire remonter des centaines de violations préexistantes. Les enregistrer une fois dans une baseline débloque le contrôle immédiatement, au lieu d’exiger de « tout corriger d’abord ».
  • Corriger progressivement avec un burn-down visible. Corrigez les violations par lots, puis régénérez périodiquement la baseline avec --write-baseline. Le diff git de chaque régénération rétrécit à mesure que des entrées disparaissent : le fichier lui-même devient un suivi de progression, et une PR qui corrige un lot de problèmes montre exactement ce qui a été nettoyé.
  • Contenu tiers ou fournisseur que vous ne maîtrisez pas. Une page intègre un widget (bulle de chat, bloc publicitaire, iframe de paiement) avec des violations connues que vous ne pouvez pas corriger. Placer seulement ces occurrences précises dans la baseline est plus précis que d’exclure tout le sous-arbre via excludeSelectors (voir le guide des options du moteur) : les nouveaux problèmes introduits n’importe où à proximité, y compris dans votre propre code, bloquent toujours normalement.
  • Filet de sécurité contre les régressions pendant une grosse refonte. En pleine migration, certaines règles peuvent légitimement échouer temporairement, d’une manière déjà suivie et en cours de correction sur de nombreuses PR. Placer cet ensemble connu dans la baseline permet quand même de détecter les nouvelles régressions sans rapport venant d’autres PR pendant la même période, au lieu de désactiver complètement le contrôle ou d’y bloquer chaque PR.
  • Déploiement progressif d’une règle nouvelle ou plus stricte. Vous activez une règle (ou tout un niveau WCAG) que le code ne respecte pas encore : placez les écarts existants dans la baseline, activez le contrôle immédiatement, et empêchez toute nouvelle violation de cette règle pendant que l’arriéré est traité à part, plutôt que d’attendre que le code soit déjà conforme pour activer la règle.

Fonctionnement de la correspondance

Rien dans la structure d’une occurrence ne constitue une identité parfaite et indépendante de la position dans la page (voir le schéma de sortie) : selector et structuralPath dérivent tous deux de la position de l’élément dans le DOM, donc l’un comme l’autre peut changer quand un balisage sans rapport est modifié ailleurs dans la page, alors que l’élément signalé lui-même n’a pas bougé.

L’identité d’une entrée de baseline est donc :

ruleId + reasonCode + html

où reasonCode est le code propre à la règle, issu de occurrence.data.details.reasonCode ("DEFAULT" par défaut si la règle n’en définit pas, ou définit autre chose qu’une chaîne non vide), et html est l’extrait de HTML externe de l’occurrence. Cette identité repose sur le contenu et non sur la position : elle résiste au déplacement de l’élément signalé dans la page (un réordonnancement, un élément frère sans rapport ajouté ou supprimé) tant que le balisage propre de l’élément signalé ne change pas. Pour un constat qui porte sur la page elle-même, signalé sur <html> ou <body> (html-lang-attr-present, bypass-blocks-present, landmark-one-main), l’extrait se limite à la balise ouvrante : modifier le contenu de la page n’en fait donc pas un nouveau constat. L’extrait est comparé avec les attributs de chaque balise triés par nom et ses noms de classes triés, si bien qu’un build qui écrit class="b a" src là où le précédent écrivait src class="a b" correspond ; les fichiers de baseline écrits avant ce changement correspondent toujours, puisque la comparaison est faite au moment de la correspondance. selector est toujours enregistré dans le fichier de baseline, mais uniquement pour donner du contexte à la lecture d’un diff : il ne sert jamais à la correspondance.

Limite connue : un élément dont le balisage propre contient du contenu dynamique (un horodatage, un compteur en direct, un id généré aléatoirement) ne correspondra jamais à lui-même d’une analyse à l’autre, puisque son extrait html diffère à chaque fois. Si vos pages sont dans ce cas, le mécanisme de baseline n’aidera pas pour ces règles ou éléments précis : voir « Alternative » ci-dessous.

La correspondance compte les occurrences, pas seulement leur présence. Si une page contient 3 éléments qui produisent des ruleId+reasonCode+html identiques à l’octet près (par exemple le même composant défectueux répété 3 fois) et que la baseline n’en a enregistré qu’un, une nouvelle analyse en signale 1 connu et 2 nouveaux, et non les 3 comme connus.

Un constat situé dans un frame d’un résultat multi-frames (runa11yCoreAcrossFrames) se distingue aussi par son frame : son entrée enregistre le chemin du frame, les sélecteurs des éléments frame qui y mènent ("frame": ["#checkout"]), et ne correspond qu’à un constat situé dans le frame à ce chemin. Le même composant défectueux dans la page et dans un frame donne deux constats. Les entrées sans frame appartiennent à la page elle-même, si bien qu’une baseline écrite à partir d’une analyse d’un seul document continue de correspondre à la page.

Un résultat compact (engineOptions.output.detail: 'findings', voir Schéma de sortie) est lu comme le résultat complet qu’il représente ; il est donc signalé de la même façon.

Les entrées de baseline qui ne correspondent à rien dans une nouvelle analyse sont signalées comme obsolètes : la violation a peut-être été corrigée, ou le balisage de l’élément a changé, ou l’analyse ne l’a pas atteint (un autre viewport, un autre périmètre ou un autre ensemble de règles). C’est purement informatif et cela ne bloque jamais le build ; examinez-les, puis régénérez périodiquement la baseline avec --write-baseline pour les nettoyer.

Constats qui penchent vers l’échec

Une baseline enregistre des échecs. Un constat non tranché n’en est pas un, mais depuis la 1.14 certains penchent vers l’échec : les indices de la page pointent vers un échec sans le prouver (leur lean, voir le schéma de sortie). Un build qui bloque sur eux (leanFailAs dans les rapports JUnit et SARIF) doit pouvoir accepter ceux qu’une page a déjà, comme il accepte ses échecs. Demandez-les en construisant la baseline :

const { buildBaselineEntries } = require('@surea11y/core/baseline');
 
const entries = buildBaselineEntries(result, { leanFail: true });

Chaque constat qui penche vers l’échec et bloque avec leanFailAs (leaningFailures(result) de @surea11y/core) reçoit une entrée marquée "lean": "fail", après les échecs :

{ "ruleId": "heading-order", "reasonCode": "HEADING_ORDER_SKIPPED_LEVEL", "selector": "html > body > main > h3", "html": "<h3>Details</h3>", "lean": "fail" }

Une telle entrée ne correspond qu’à un constat qui penche vers l’échec, de même identité, et une entrée ordinaire qu’à un échec. Avec leanFailAs, JUnit ne bloque pas sur un constat auquel elle correspond (son message <skipped> le compte parmi les échecs probables connus) et SARIF le laisse en warning. Sans leanFailAs, ces entrées ne changent rien, et matchBaseline les laisse hors de ses comptes, obsolètes comprises, sauf avec { leanFail: true } : il les compare alors dans leanFail ({ total, knownCount, newCount, newOccurrences, staleCount }). --write-baseline n’écrit que les échecs.

Les lean arrivent dans des versions mineures : une mise à jour peut donc apporter des échecs probables que la baseline ne contient pas. Reconstruisez-la avec leanFail: true après une mise à jour, et examinez le diff. La façon dont ils sont choisis et mesurés est décrite dans LEANS.md, sur GitHub.

Mettre à jour

L’identité d’un constat comprend son code de raison, et une version mineure peut lui en donner un nouveau : une règle qui n’en donnait pas (DEFAULT) en donne un, ou un code est scindé en deux selon la cause (voir l’identité d’un constat dans API_STABILITY.md, sur GitHub). Le constat est le même, et une baseline écrite avant la mise à jour y correspond toujours : chaque déplacement figure dans src/baseline-moves.json (règle, ancien code, nouveaux codes), et une entrée sous l’ancien code correspond au constat sous un nouveau quand le reste de son identité, la règle, le balisage et le frame, est le même. matchBaseline et l’option baselineEntries des rapports SARIF et JUnit le lisent tous. Une entrée est d’abord cherchée sous le code propre du constat : elle ne va donc jamais à un constat déplacé quand un autre constat a exactement cette identité.

La 1.14 déplace ceux-ci, depuis la 1.13 :

RègleAncien codeNouveau 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-presentDEFAULTTEXT_ALTERNATIVE_MISSING
area-alt-presentDEFAULTTEXT_ALTERNATIVE_MISSING, TEXT_ALTERNATIVE_EMPTY
canvas-text-alternative-presentDEFAULTTEXT_ALTERNATIVE_MISSING, ROLE_IMG_NAME_MISSING
area-alt-quality, canvas-text-alternative-quality, embed-text-alternative-quality, input-image-alt-quality, object-text-alternative-qualityDEFAULTTEXT_ALTERNATIVE_REVIEW
img-alt-quality, svg-text-alternative-qualityDEFAULTTEXT_ALTERNATIVE_REVIEW, IMAGE_ONLY_CONTROL_CONTENT
img-alt-decorativeEXCLUDED_ELEMENT_REVIEWIMAGE_ONLY_CONTROL_CONTENT
input-image-alt-decorativeDEFAULTNAME_FROM_ELSEWHERE_REVIEW
manual-reviewDEFAULTKEYBOARD_REVIEW
list-children-validLIST_INVALID_CHILDLIST_ITEM_ROLE_OVERRIDDEN
listitem-parent-validLISTITEM_INVALID_PARENTLIST_ROLE_OVERRIDDEN
aria-prohibited-childrenARIA_PROHIBITED_CHILD_ROLELESSARIA_PROHIBITED_CHILD
duplicate-idDUPLICATE_IDDUPLICATE_ID_IN_SHADOW_TREE
valid-langELEMENT_LANG_INVALIDDEFAULT_LANG_INVALID
p-as-headingBOLD_LARGE_PARAGRAPH, BOLD_LARGE_DIVHEADING_LIKE_NUMBER, HEADING_LIKE_LINK

Le tableau couvre une version mineure : un déplacement y figure depuis la version qui l’a apporté jusqu’à la version mineure suivante, qui le retire. Une baseline écrite par la 1.13 continue donc de correspondre avec la 1.14.x, et doit être régénérée avant la 1.15.

Régénérer la baseline avec --write-baseline (ou buildBaselineEntries) après une mise à jour écrit chaque constat sous son code actuel, et rend le tableau inutile. Cela accepte aussi tout constat que la mise à jour signale pour la première fois : examinez donc son diff. Pour mettre les codes à jour sans rien accepter de nouveau, réécrivez le fichier avec migrateBaselineEntries de @surea11y/core/baseline :

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');

Elle renvoie une copie des entrées où chaque entrée déplacée porte son nouveau code, et toutes les autres telles quelles. Sans résultat, elle ne réécrit que les déplacements qu’elle peut établir sans le constat : un code que la règle ne donne plus et qui est passé à un seul nouveau (le DEFAULT de img-alt-present), pas une scission.

Un constat dont le résultat a changé avec la mise à jour est une autre affaire : une règle qui fait désormais échouer ce qu’elle demandait, ou demande ce qu’elle faisait échouer, donne un constat que la baseline ne contenait pas, ou n’en donne plus un qu’elle contenait, et le journal des modifications explique pourquoi.

Analyser à plusieurs largeurs de viewport

Une entrée de baseline n’enregistre pas le viewport, et la correspondance ne s’en sert pas : le même élément avec le même défaut correspond quelle que soit la largeur. Cela convient à une page dont les constats ne dépendent pas de sa mise en page. Une règle qui dépend de la mise en page, comme text-spacing-content-loss ou target-size-minimum, peut échouer à une largeur et pas à une autre ; pour une page analysée à plusieurs largeurs, gardez donc une baseline par largeur : a11y-baseline-390.json, a11y-baseline-1280.json.

Une baseline unique écrite à partir de toutes les largeurs bloque toujours correctement, puisqu’un constat n’est nouveau que si aucune entrée ne lui correspond. Mais chaque exécution signale alors les constats des autres largeurs comme obsolètes, si bien que le nombre d’entrées obsolètes ne vous dit plus ce qui a été corrigé.

Format du fichier

{
"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> rejette tout fichier qui n’est pas de la forme { version: 1, entries: [...] } avec une erreur claire (code de sortie 2), plutôt que d’essayer de deviner un format plus ancien ou différent. matchBaseline, ainsi que l’option baselineEntries des reporters SARIF et JUnit, acceptent le fichier tel qu’il est enregistré (son JSON.parse) ou sa liste entries.

Combinaison avec --json

Quand --baseline ou --write-baseline est utilisé avec --json, l’objet affiché comporte une clé baseline en plus du résultat normal du moteur (cela ne concerne que la sortie de la CLI et ne fait pas partie du contrat de résultat du moteur décrit dans le schéma de sortie) :

  • --write-baseline : { mode: "write", path, entries: <number written> }
  • --baseline : { mode: "check", totalFail, knownCount, newCount, staleCount, newOccurrences: [...] }

Alternative : comparer vous-même deux analyses

Si vos pages ne correspondent pas à ce modèle (beaucoup de contenu dynamique à l’intérieur même des éléments signalés), la solution de repli toujours disponible consiste à comparer vous-même deux sorties --json complètes : voir INTEGRATION.md. Vous gardez ainsi un contrôle total sur la logique d’identité et de correspondance, au prix de l’écrire vous-même.