Baseline / allowlist
Un control estricto en CI ("fallar la compilación ante cualquier resultado fail") es un obstáculo para la adopción por parte de un equipo que analiza por primera vez un sitio existente e imperfecto: no pueden publicar nada hasta corregir cada violación preexistente. Un baseline permite que un equipo diga "estas N ya se conocen, no bloquees por ellas, pero falla en cuanto aparezca una genuinamente nueva".
# 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
El archivo de baseline está pensado para incorporarse a git, de modo que aceptar más deuda de accesibilidad se convierta en una decisión explícita y revisable: una violación nueva aparece como una fila nueva en el diff del archivo en un PR, no como un cambio silencioso en un recuento.
Cuándo es útil
- Adoptar el análisis en un sitio existente. El caso motivador anterior: un primer análisis en un sitio maduro puede revelar cientos de violaciones preexistentes. Registrarlas en el baseline una sola vez elimina el bloqueo de inmediato en lugar de exigir "corregir todo primero".
- Remediación incremental con un avance visible. Corrige violaciones por lotes, y luego regenera periódicamente el baseline con
--write-baseline. El diff de git de cada regeneración se reduce a medida que desaparecen entradas: el propio archivo se convierte en un rastreador de progreso, y un PR que corrige un lote de problemas muestra exactamente qué se limpió. - Contenido de terceros o de proveedores que no controlas. Una página incrusta un widget (burbuja de chat, unidad de anuncios, iframe de pago) con violaciones conocidas que no puedes corregir. Registrar en el baseline solo esas ocurrencias específicas es más preciso que excluir todo el subárbol mediante
excludeSelectors(ver la guía de opciones del motor); los problemas nuevos introducidos en cualquier otro lugar cercano, incluido tu propio código, siguen bloqueando con normalidad. - Red de seguridad contra regresiones durante una refactorización grande. A mitad de una migración, algunas reglas pueden fallar legítimamente de forma temporal, en formas ya rastreadas y en proceso de corrección a lo largo de muchos PR. Registrar en el baseline el conjunto conocido y en curso sigue detectando regresiones nuevas no relacionadas provenientes de otros PR durante la misma ventana, en lugar de desactivar el control por completo o bloquear cada PR con él.
- Despliegue escalonado de una regla nueva o más estricta. Activar una regla (o un nivel completo de WCAG) contra el que la base de código aún no está limpia: registra en el baseline los huecos existentes, activa el control de inmediato, y evita cualquier violación nueva de esa regla mientras el trabajo pendiente se limpia por separado, en lugar de esperar a activar la regla hasta que la base de código ya cumpla.
Cómo funciona la coincidencia
Nada en la estructura de una ocurrencia es una identidad perfecta e independiente de la posición en la página (ver el esquema de salida): selector y structuralPath se derivan ambos de la posición del elemento en el DOM, así que cualquiera de los dos puede cambiar cuando se modifica marcado no relacionado en otra parte de la página, aunque el elemento marcado en sí no haya cambiado.
En su lugar, la identidad de una entrada de baseline es:
ruleId + reasonCode + html
donde reasonCode es el código específico de la regla proveniente de occurrence.data.details.reasonCode (con "DEFAULT" por defecto cuando una regla no establece uno, o establece algo que no sea una cadena no vacía), y html es el fragmento de outer-HTML de la ocurrencia. Esto se basa en el contenido y no en la posición: sobrevive a que el elemento marcado se mueva por la página (un reordenamiento, un elemento hermano no relacionado añadido o eliminado) siempre que el propio marcado del elemento marcado no cambie. Para un hallazgo sobre la propia página, informado en <html> o <body> (html-lang-attr-present, bypass-blocks-present, landmark-one-main), el fragmento es solo la etiqueta de apertura, así que editar el contenido de la página no lo convierte en nuevo. El fragmento se compara con los atributos de cada etiqueta ordenados por nombre y sus nombres de clase ordenados, así que una compilación que escribe class="b a" src donde la anterior escribió src class="a b" coincide; los archivos de baseline escritos antes de esto siguen coincidiendo, porque la comparación se hace en el momento de buscar coincidencias. El selector sigue registrándose en el archivo de baseline, pero únicamente como contexto para una persona que lea un diff; nunca se usa para la coincidencia.
Limitación conocida: un elemento cuyo propio marcado incluye contenido dinámico (una marca de tiempo, un contador en vivo, un id generado aleatoriamente) nunca coincidirá consigo mismo entre dos análisis, ya que su fragmento html difiere cada vez. Si tus páginas presentan este caso, el mecanismo de baseline no ayudará para esas reglas o elementos específicos (ver "Alternativa" más abajo).
La coincidencia cuenta ocurrencias, no solo presencia. Si una página tiene 3 elementos que producen una combinación idéntica byte a byte de ruleId+reasonCode+html (por ejemplo, el mismo componente defectuoso repetido 3 veces) y el baseline solo registró 1 de ellos, un análisis nuevo devuelve 1 conocido y 2 nuevos, no los 3 como conocidos.
Un hallazgo dentro de un frame de un resultado entre frames (runa11yCoreAcrossFrames) se distingue también por su frame: su entrada registra la ruta del frame, los selectores de los elementos frame que llevan hasta él ("frame": ["#checkout"]), y solo coincide con un hallazgo en el frame de esa ruta. El mismo componente defectuoso en la página y en un frame son dos hallazgos. Las entradas sin frame son las de la propia página, así que un baseline escrito a partir de un análisis de un solo documento sigue coincidiendo con la página.
Un resultado compacto (engineOptions.output.detail: 'findings', ver Esquema de salida) se lee como el resultado completo que representa, así que se informa igual.
Las entradas de baseline que no coinciden con nada en un análisis nuevo se indican como obsoletas: puede que la violación se haya corregido, que el marcado del elemento haya cambiado o que el análisis no llegara a él (otro viewport, otro ámbito u otro conjunto de reglas). Esto es solo informativo y nunca bloquea la compilación; revísalas y luego regenera el baseline con --write-baseline periódicamente para limpiarlas.
Hallazgos que probablemente fallan
Un baseline registra fallos. Un hallazgo sin decidir no lo es, pero desde la 1.14 algunos apuntan a un fallo: la evidencia de la página indica un fallo sin demostrarlo (su lean, ver el esquema de salida). Una compilación que se bloquea por ellos (leanFailAs en los generadores de informes JUnit y SARIF) necesita una forma de aceptar los que una página ya tiene, igual que acepta sus fallos. Pídelos al crear el baseline:
const { buildBaselineEntries } = require('@surea11y/core/baseline');const entries = buildBaselineEntries(result, { leanFail: true });
Cada hallazgo que apunta a un fallo y bloquea con leanFailAs (leaningFailures(result) de @surea11y/core) recibe una entrada marcada con "lean": "fail", después de los fallos:
{ "ruleId": "heading-order", "reasonCode": "HEADING_ORDER_SKIPPED_LEVEL", "selector": "html > body > main > h3", "html": "<h3>Details</h3>", "lean": "fail" }
Una entrada así solo coincide con un hallazgo que apunta a un fallo, con la misma identidad, y una entrada normal solo con un fallo. Con leanFailAs, JUnit no bloquea por un hallazgo con el que coincide (su mensaje <skipped> lo cuenta entre los probables fallos conocidos) y SARIF lo deja como warning. Sin leanFailAs las entradas no cambian nada, y matchBaseline las deja fuera de sus cuentas, obsoletas incluidas, salvo que reciba { leanFail: true }, en cuyo caso las compara en leanFail ({ total, knownCount, newCount, newOccurrences, staleCount }). --write-baseline solo escribe fallos.
Los lean llegan en versiones menores, así que una actualización puede traer probables fallos que el baseline no tiene: vuelve a crearlo con leanFail: true tras actualizar y revisa el diff. Cómo se eligen y se miden está en LEANS.md, en GitHub.
Actualizar de versión
La identidad de un hallazgo incluye su código de motivo, y una versión menor puede darle uno nuevo: una regla que no daba ninguno (DEFAULT) ahora da uno, o un código se divide en dos según la causa (ver la identidad de un hallazgo en API_STABILITY.md, en GitHub). El hallazgo es el mismo, y un baseline escrito antes de actualizar sigue coincidiendo con él: cada cambio figura en src/baseline-moves.json (regla, código antiguo, códigos nuevos), y una entrada con el código antiguo coincide con el hallazgo con uno nuevo cuando el resto de su identidad (la regla, el marcado y el frame) es el mismo. matchBaseline y la opción baselineEntries de los generadores de informes SARIF y JUnit lo leen. Una entrada se busca primero con el propio código del hallazgo, así que nunca va a un hallazgo movido cuando otro hallazgo tiene exactamente esa identidad.
La tabla cubre una versión menor: un cambio figura desde la versión en que llegó hasta la siguiente versión menor, que lo quita. Ni la 1.15 ni la 1.16 movieron ningún hallazgo, así que la tabla está vacía.
La 1.14 movió estos, desde la 1.13, y la 1.15 los quitó:
| Regla | Código antiguo | Código nuevo |
|---|---|---|
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 |
Así, un baseline escrito por la 1.13 sigue coincidiendo con estos en la 1.14.x, no en la 1.15: allí una entrada con un código antiguo está obsoleta y su hallazgo es nuevo. Pon al día ese baseline en la 1.14.x, como se explica abajo, antes de pasar a la 1.15; migrateBaselineEntries de la 1.15 ya no conoce estos cambios. Un baseline escrito por la 1.14 sigue coincidiendo en la 1.15 y la 1.16.
Regenerar el baseline con --write-baseline (o buildBaselineEntries) tras actualizar escribe cada hallazgo con su código actual y hace innecesaria la tabla. También acepta cualquier hallazgo nuevo que traiga la actualización, así que revisa su diff. Para poner al día los códigos sin aceptar nada nuevo, reescribe el archivo con 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');
Devuelve una copia de las entradas con cada una movida a su código nuevo, y todas las demás como estaban. Sin un resultado, solo reescribe los cambios que puede deducir sin el hallazgo: un código que la regla ya no da y que pasó a uno solo nuevo (el DEFAULT de img-alt-present), no una división.
Otra cosa es un hallazgo cuyo resultado cambió con la actualización: una regla que ahora hace fallar lo que antes preguntaba, o pregunta por lo que antes hacía fallar, da un hallazgo que el baseline no tenía, o deja de dar uno que tenía, y el registro de cambios explica por qué.
Un análisis de varios perfiles
Un resultado de varios perfiles (engineOptions.profile como lista, core 1.15.0 y posteriores) recoge cada hallazgo una vez, con lo que cada perfil hace de él en verdicts. Una entrada del baseline es el hallazgo, por regla, código de motivo y marcado, así que una entrada lo cubre en todos los perfiles, y un baseline escrito a partir de un análisis de un perfil sigue coincidiendo con los mismos hallazgos en un análisis de varios.
Analizar con varios anchos de viewport
Una entrada del baseline no registra el viewport, y la comparación no lo usa: el mismo elemento con el mismo defecto coincide con cualquier ancho. Eso encaja con una página cuyos hallazgos no dependen de su diseño. Una regla que depende del diseño, como text-spacing-content-loss o target-size-minimum, puede fallar con un ancho y no con otro, así que para una página analizada con varios anchos, mantén un baseline por ancho: a11y-baseline-390.json, a11y-baseline-1280.json.
Un único baseline escrito a partir de todos los anchos sigue bloqueando correctamente, ya que un hallazgo solo es nuevo cuando ninguna entrada coincide con él. Pero entonces cada ejecución informa de los hallazgos de los demás anchos como obsoletos, así que el recuento de obsoletos deja de decirte qué se corrigió.
Formato del archivo
{"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> rechaza un archivo que no sea { version: 1, entries: [...] } con un error claro de salida 2 en lugar de adivinar un formato más antiguo o distinto, y desde @surea11y/cli 1.6.0 también uno con una entrada que no lo es, antes del análisis, nombrando el archivo y la entrada. matchBaseline, y la opción baselineEntries de los generadores de informes SARIF y JUnit, aceptan el archivo tal como se guardó (JSON.parse del mismo) o su lista entries. null o la ausencia de baseline no tiene entradas, así que todo fallo es nuevo. Desde core 1.16.0, cualquier otra cosa lanza un TypeError con code: 'INVALID_BASELINE' y un mensaje que dice qué falla: una cadena o un objeto sin lista entries, una entrada que no es un objeto o no tiene ruleId, un reasonCode, selector o html que no es ni una cadena ni null, o un frame que no es una lista de cadenas. migrateBaselineEntries lo comprueba de la misma manera. Antes, ese valor se leía como un baseline vacío, lo que hacía nuevo cada fallo sin avisar.
Combinación con --json
Cuando --baseline o --write-baseline se usa junto con --json, el objeto impreso gana una clave baseline junto al resultado normal del motor (esto es exclusivo de la salida de la CLI, no forma parte del contrato de resultado propio del motor descrito en el esquema de salida):
--write-baseline:{ mode: "write", path, entries: <number written> }--baseline:{ mode: "check", totalFail, knownCount, newCount, staleCount, newOccurrences: [...] }
Alternativa: comparar dos análisis tú mismo
Si tus páginas no encajan en este modelo (contenido dinámico intenso dentro de los propios elementos marcados), la alternativa siempre disponible es comparar tú mismo dos salidas completas de --json. Consulta INTEGRATION.md. Eso te da control total sobre la lógica de identidad y coincidencia, a costa de tener que escribirla tú mismo.