ultra11y — audit WCAG 2.2 AA and write accessible markup
On accessibility, an automated tool only sees part of the problem. ultra11y owns
that with a division of labour: the deterministic, install-free engine
(node scripts/ultra11y.mjs <command> — no npm install, no key; the JSX/TSX parser
is embedded in the bundle) does the mechanical work — detect the machine-checkable
non-conformities and tie each to the right WCAG success criterion — and the active coding
agent adjudicates the judgment criteria itself — alt relevance, link
purpose in context, reading order — statically, from the evidence the engine harvests
(verify --manual), each verdict gated by verify/check. Only the truly rendered-DOM
criteria (computed contrast, visible focus, zoom/reflow, content-on-hover) fall to the scan
tier (axe-core in a real browser); a human is at most optional oversight. Gates stop any
hallucinated non-conformity from surviving, and nothing is ever silently "conforming".
Do not confuse rules with criteria. The bundle ships 93 static checks, WCAG AA has 55
success criteria, and RGAA has 106 criteria. Several rules may evidence one criterion, one
rule may contribute to several pack criteria, and most criteria remain open until a scan or
an adjudication. The only honest run-level coverage is the grid's live provenance tally
(moteur + scan + agent, plus any named undecidable residue), never a fixed promise such as
“48 agent criteria”. GitHub summaries therefore show decided/total, not a percentage over
the small decided subset.
WCAG 2.2 Level AA is the worldwide core. Country standards — France's RGAA, the
US Section 508, the EU EN 301 549 — are pluggable standards packs that map their
criteria onto WCAG. Add --standard rgaa to re-key every command and every output —
audit included, findings and all; plug an external
pack at runtime with --pack ./pack.json (or a .ultra11yrc.json), no rebuild; or
contribute your country (see references/standards.md). Packs (and their concrete
implementation guidance — the RGAA SocialGouv/etalab good/bad patterns) can be
AI-ingested and gated by pack check so a fabricated mapping never passes — see
references/packs.md and references/guidance.md.
Most of the work is yours. Of the 55 WCAG 2.2 AA success criteria, the engine can decide
3 outright; 14 need a rendered page (scan) and 38 are judgment calls the agent
adjudicates. So the engine's clean run is a starting point, never a verdict — and
references/adjudication.md is the page you actually work from.
Core rules:
- Never invent a non-conformity: every
NCcites a real, resolvable element (checkverifies it).- Native HTML first, ARIA last; never duplicate implicit semantics.
- Never invent a conformity either: a
Cis cited exactly like anNC— it names incitations[]the harvested evidence it cleared, each anchor resolvable and drawn from the evidence that criterion was actually shown. A criterion presented with no evidence cannot beCat all; it staysmanual. Neither can one whose reading was INCOMPLETE (evidenceComplete: false): a criterion you were shown part of can still fail on what you saw, but it can never be cleared over what you did not. And an adjudicatedCis never merged with an engine-decided one: reports list it separately and keep it out of the automatic pass rate. 3b. What you are shown is the whole population, said once per distinct thing. Evidence is one representative per CONTENT CLASS, carryingoccurrences, the other anchors inalsoAt, and thepagesit appears on — 887 links across 38 captured pages are 97 distinct (text, href) pairs. Ruling on the representative rules on all of them, which is what makes an honestCreachable; and a citation naming ANY occurrence of a class the criterion carries is accepted, so cite the one you actually opened.- Residual is explicit, never silently conforming: the AI agent adjudicates every judgment criterion itself (
verify --manual, gated), and the rendering criteria go toscan— which now CONCLUDESC(decidedBy: "scan") when every rule carrying a criterion ran on every page in scope and raised nothing, and stays silent when even one page's signals were incomplete. Any criterion still unproven stays "to assess manually" — no status without a recorded, justified verdict. To make that a gate rather than a hope,check --in <audit.json> --require-decidedfails while anything is still to assess, and--allow-undecided <file>takes the criteria you genuinely cannot decide, each with its reason (a named list, never a threshold).--require-decided=pagesholds EVERY page's own grid to the same bar: a criterion failing on one route is settled for the run and may still be nobody's verdict on the routes it never fired on, which is what a per-page deliverable is actually judged on. The emitted worklist is authoritative. It may deliberately contain a criterion whose run-wide status is alreadyNC, because that failure did not decide the criterion on the other pages. Such an item is not redundant: return one grounded verdict for it (or an explicitmanualreason), never leave it blank because the global tally looks decided.- Look the criterion up; never recall it.
criteria [--standard <pack>] <id>— orultra11y_criteriaover MCP — returns the criterion's own wording, its numbered tests, and the glossary definitions those tests cite. RGAA 8.3 is not "the page needs alangattribute"; it is test8.3.1with two alternative conditions, and the glossary decides what its terms mean. An auditor block that cites8.3.1after reading it is grounded; one that cites it from memory is a guess wearing a reference. Before auditing against a country standard, run the plan (ultra11y_method): it tells you how many of its criteria that still need adjudication to earn C — for RGAA, 104 of 106. Seereferences/mcp.md.- The FINAL rendered semantic HTML must be correct. The engine sees only source; a component library (DSFR/MUI…) or
.vue/.svelte/.astroSFC hides the real markup, so a green source audit is not proof. Verify the produced semantic HTML — install the zero-touch capture harvester (render --setup) so every component your tests render is serialized to.ultra11y/capturesand audited, withaudit --require-capturesgating the blind spots. Seereferences/automation.md/rendered.md.- Language: ALWAYS pass
--langmatching the language of your conversation with the user; ask the user when ambiguous. Without the flag the CLI auto-detects (repo<html lang>→ the active standard's default locale → English) — a scripted/CI fallback, not a substitute for passing--langyourself.- Technical tokens stay in English, even in French prose. In any French deliverable you write (report commentary, PRD, tracker tickets, judgment verdicts), attribute/ element/role names and their values are code, not prose — never translate them:
aria-livestaysaria-live(never « région live »), same fortabindex,alt,role="alert", landmark role names. The engine's own fr catalog follows this; match it. Normative standard vocabulary (RGAA wording such as « lien d'évitement ») keeps its official French.
The reference library
Domain knowledge first, then the tooling. Read the one that matches the question in front of you.
| Accessibility knowledge | |
|---|---|
references/adjudication.md | How to decide each of the 52 criteria the engine hands you — the decision rule, when NA is legitimate, the questions |
references/naming.md | Accessible-name computation, 2.5.3 label-in-name, visually-hidden CSS, naming images/SVG/figures |
references/widgets.md | ARIA APG composite patterns, keyboard contracts, focus management, SPA route changes, live regions |
references/forms-and-errors.md | Labels and instructions, the autocomplete token list, error identification/suggestion/prevention, redundant entry, accessible authentication |
references/structure.md | Headings, landmarks, lists and dl, simple and complex tables, meaningful sequence |
references/media-and-motion.md | Captions/audio description/transcripts, pointer gestures, target size, orientation, reflow, reduced motion |
references/forbidden-patterns.md | 15 anti-patterns: bad → why → native fix → criterion |
references/focus-and-logic.md | The focus/keyboard criteria you own, and how to reason about them |
references/criteria.md | The 55 criteria: level, automatability class, engine rules, pack mapping (generated) |
references/rgaa-automation.md | RGAA 4.1.2: generated 106-criterion / 258-test matrix, decisive rules, candidate signals and silence policy |
references/act.md | What the engine is measured to catch — and what it demonstrably does not (generated) |
| Running the tool | |
|---|---|
references/runbook.md | The reliable run: the one recipe, the transport to use and why, and what a pass costs. Read this before paying for an adjudication |
references/claude-code-report.md | Claude Code's detailed publication route: rendered scope, direct adjudication, adversarial verification, Markdown + HTML + per-page evidence |
references/audit.md | The audit loop end to end, and the normative page sample |
references/judgment.md | The adjudication + refutation worklists, verdict vocabulary, fail-closed fold |
references/verify.md | The check/verify gates |
references/false-positives.md | Where a finding may still be wrong, and how to refute it |
references/rendered.md · references/automation.md | Auditing produced HTML, captures, hooks and CI |
references/harnesses.md | Where this runs: Claude Code, Codex CLI, OpenCode, and the AGENTS.md fallback |
references/orchestrators.md | Driving the engine from a workflow engine / non-GitHub CI: pinning, the six-step pipeline, exit codes, the ticket set as JSON for any tracker |
references/ci.md | --format sarif|github: inline PR annotations, code scanning, job summary |
references/pages.md | Page snapshots + the per-page criterion grid (pages), and its honesty rules |
references/e2e.md | render --e2e: auditing a page during your Playwright/Cypress run |
references/devtools.md | dev: the live overlay while you build, and the per-page dashboard |
references/extension.md | The browser extension: audit the page you are looking at, through the side-car |
references/dynamic.md | The scan tier: runtimes, probes, authenticated pages |
references/scale.md | Focusing an audit on a large repository |
references/fix.md · references/correction.md | Applying fixes, by priority, without regressions |
references/prd.md | The auditor block as a backlog (markdown) |
references/tickets.md | Filing that backlog as tickets: GitHub, GitLab, Jira, and at which granularity |
references/standards.md · references/packs.md · references/guidance.md | Country standards, authoring a pack, implementation guidance |
references/mcp.md | The standards as a rule engine: look a criterion up instead of recalling it — its numbered tests, the terms it defines, the fix, and the work plan. As MCP tools and std:// resources, or the same data on the CLI |
references/methodology.md | Statuses, pass rate, severities, report format |
references/cross-file.md | --graph: imports and cross-file rules |
references/orchestration.md | Fanning the judgment phases out across subagents |
Choose by situation
- "Produce a detailed audit/report in Claude Code" → use the active Claude session as the
auditor, close the rendered and judgment criteria, challenge its claims, then publish the
dated Markdown report, printable HTML and evidenced per-page dossiers. Read
references/claude-code-report.md. This is the interactive publication profile; finish with its full deliverable set. The compact summary is the CI branch below. - "Audit / compliance report" →
node scripts/ultra11y.mjs audit … --json, thenreport(synthesis table + one auditor conformance block per NC criterion — same blockprdandticketsuse), thencheck; readreferences/audit.md. - "Code rendered by a library (DSFR, MUI…) or a
.vue/.svelte/.astroSFC / avoid false negatives" → audit the produced HTML, not the source template. Easiest: install the zero-touch capture harvester (render --setup) so your tests serialize every rendered component to.ultra11y/captures(auto-ingested, findings attributed to the source component;render --coverageandaudit --require-capturestrack blind spots). Orrender(build→audit recipe or SSR snapshot--scaffold) thenauditon the output, andscanfor computed rendering. SFC-source findings are flaggedpreliminary(ascope.sourceTemplatecaveat); readreferences/rendered.md. - "A finding looks wrong / false positive on a component" → the engine auto-suppresses
most component false positives (slot/prop-injected names, component children, dynamic
bindings, conditional headings) and marks SFC/library-source findings
preliminary; confirm or refute the rest withverify --apply; readreferences/false-positives.md. - "Large repo / audit smartly" → focus:
--changed(git diff), template prioritization, dedup,--max-files; readreferences/scale.md. - "Cross-file analysis (tree + dependencies), JSX/TSX as a real AST" →
audit --graphresolves imports and applies cross-file rules (an icon-only component used without a name, an anchor target in another file…), no browser; readreferences/cross-file.md. - "Generate the fix markdown / PRDs" →
prd(the SAME auditor conformance blockreport's NC section renders — theme/criterion/test/WCAG+level/ finding/expected/verification in the active standard's vocabulary — as a backlog);--split criterion,--format docfor a product-requirements doc,--format remediationfor the legacy dev backlog). It writes markdown only; readreferences/prd.md. - "Open tickets / issues for this (GitHub · GitLab · Jira)" →
tickets, a separate command that reads the audit and files it, writing no markdown.--grainpicks what one ticket is: per criterion (default), per page, per page+criterion, per file, or one consolidated. De-dupe is by exact title, so re-running never duplicates. Always--dry-runfirst; readreferences/tickets.md. - "Plug or author a standards pack (RGAA & beyond), AI-ingest external rules" →
--pack/.ultra11yrc.jsonto load at runtime,pack checkto gate it (the anti-hallucination guardrail),pack scaffoldto start one; concrete before/after implementation guidance attaches to findings/PRD; readreferences/packs.mdandreferences/guidance.md. - "Adjudicate the judgment criteria (judgment phase)" →
verify --manual --in audit.jsonemits an ADJUDICATION worklist (ADJUDICATE.todo.json+ADJUDICATE.md), one item per residual criterion, pre-loaded with the engine's harvested evidence (every alt, link text + context, literal colour pairs, control labels, heading outline, ARIA state, tabindex, lang-of-parts); the AI agent fills eachverdict—C/NA(with ajustification),NC(with a groundable finding), ormanual(with areason) — thenverify --apply … --in audit.jsonfolds them back FAIL-CLOSED; readreferences/judgment.md. - "Many items to adjudicate/verify (fan the judgment out to subagents)" →
orchestrate --run <dir>emits, from the run's CURRENT worklists, one launchable multi-agent workflow per ready phase + theagents/<role>.mddispatch contracts + a sequentialRUNBOOK.md— the default execution path on a subagent-capable harness; see Orchestration — route by harness below. - "Focus, keyboard & interaction logic (the interaction-logic part)" → the engine marks
focus order/visible/trap and on-focus/on-input criteria as residual risks; the AI agent reads
the full component source and adjudicates the keyboard/focus behaviour (visible-focus and the
other rendered criteria go to
scan); readreferences/focus-and-logic.md. - "Put the fixes in place" →
fix(dry-run by default,--writeapplies the safe codemods, proposes the rest without inventing anything); readreferences/fix.md. - "Fix by priority, no regressions (correction phase)" →
fix(--write,--iterate) + theprdbacklog, blocking→major→minor; readreferences/correction.md. - "Automatic repo gate (hook / CI)" →
init --hookwrites a git pre-commit gate over the strict staged snapshot (audits the exact index blobs, auto-applies safe fixes and re-stages them, blocks only on judgment issues);init --baseline/--ciis the opt-in "block only NEW non-conformities" variant. Installed as a Claude Code plugin, a third gate runs thereview-a11yskill (not just the engine) on a pending commit/push/PR — that one is shipped by the plugin, not byinit. For library/SFC code, commit rendered captures (render --setup) and stage them so the real semantic HTML is what's checked (audit --require-captures); readreferences/automation.md. - "Show me the problems while I build the page" →
dev --nextwrites a one-line overlay component anddevstarts a loopback side-car: a floating panel lists the current page's non-conformities, each opening itsfile:linein your editor, andhttp://127.0.0.1:4111accumulates the per-page grid as you browse. Renders nothing outside development; readreferences/devtools.md. - "Check a page during our E2E tests" →
render --e2ewrites Playwright/Cypress fixtures that audit the page as your test left it (logged in, form filled, modal open) — state a separatescanrun does not have — and persist each checked page as a snapshot.failOngates the test;failOn: falserecords without failing, which is how you adopt it on an existing backlog; readreferences/e2e.md. - "Audit the real PAGE, not the component" → a page snapshot
(
.ultra11y/pages/<id>/) is the whole rendered document plus the browser-only signals (computed styles, boxes, a11y tree, screenshot), ingested byauditautomatically. Because it is a FULL document, the page-scoped rules run on it — that is where RGAA 8.3 (lang), 8.5/8.6 (title) and 12.6 become decidable at all; readreferences/pages.md. - "Decide the rendering criteria without a browser in CI" → once a page snapshot exists,
the rendered tier runs inside the ordinary
audit: computed-style contrast (RGAA 3.2/10.5), screenshot-measured contrast for text over a gradient/image (the CSSOM blind spot axe-core shares), and colour-only link identification (RGAA 10.6, previously undecidable). Each leaves the criterionmanualwhen it cannot measure — never a guess; readreferences/pages.md. - "Give me the RGAA grid, page by page" →
pages --in audits/audit-latest.json --standard rgaa(also embedded inreport): one row per criterion, one column per page URL, rebuilt from the committed audit.json alone. Two rules hold: a finding is attributed to a page only when something SAYS so (else it is reported as unattributed, never spread), andCby silence is earned only by a page whose real DOM was audited — a source-only page keeps its undecided criteria « à évaluer »; readreferences/pages.md. - "I want a report PER PAGE, not a matrix" →
pages --in audits/audit-latest.json --standard rgaa --format report --split page --out audits/pages: one dossier per page — identity, its screenshot, its rate, EVERY criterion of the standard with its status on that page, then each non-conformity as the ordinary auditor block. Nothing is re-decided (the same projection as the grid) and no format is invented (the same auditor block as the report), so a page sheet and the compliance report cannot disagree.checkgates it against invented criteria like any other report; readreferences/pages.md. - "Which pages should I even audit?" →
pages discover --crawl http://localhost:3000 --write(or--sitemap <url>) writes thesample.pagesblock for you, taking each page's name from its served<title>and NEVER overwriting the auth/storageState/notes already declared. Thensample checklints the coverage andscan --samplescans and snapshots it. A client-rendered SPA exposes no routes in its served HTML — use a sitemap there; readreferences/dynamic.md. - "Audit these URLs page by page, in CI" → the shipped Action takes
urls,sitemap,crawlorsample, snapshots every page it visits and writes every criterion's status under every page both in the job summary and inaudits/pages-status.md. Usepages-report: compactfor that status-only package: the Markdown, page JSON, source audit and verdict ledger only (no adjudication worklists, remediation report, HTML or crops), or keep the defaulttruefor the detailed page dossiers too. Request apages/fullPR comment when the same grid is wanted on the pull request; readreferences/ci.md. - "Nobody is here to rule on the judgment criteria" →
judge --in audits/audit-latest.json --standard rgaa [--apply]adjudicates them with a model, for a run with no coding agent in the loop (CI, the extension, an E2E run). It is a CALLER, not a second judge: same worklist, same evidence, same prompt, and the verdicts pass the same fail-closed gate yours do. Use--runner apiwith an API key, or--runner claude|codexwith that local CLI's existing subscription login (--runner clistill aliases Claude). Inside an agent, preferverify --manualand rule the worklist yourself; readreferences/judgment.md. - "Show the findings ON the pull request, not just a red job" →
--format sarif(upload to code scanning → inline annotations at the right file:line) or--format github(::error::workflow commands + a$GITHUB_STEP_SUMMARYtable) — fromreport --standard rgaawhen you want the pack's criteria rather than WCAG's; readreferences/ci.md. - "Make this code accessible / review it" → audit the snippet
(
audit - < component.html) native-first; readreferences/authoring.mdandreferences/forbidden-patterns.md. - "What does criterion X mean" →
criteria(e.g.criteria 1.4.3, orcriteria --standard rgaa 8.3); seereferences/criteria.md. - "How trustworthy is a given check / what does the engine NOT catch" → the engine is
scored against the W3C ACT-Rules test corpus (~1 100 third-party examples): which
checks are consistent, where recall is partial, which deviations are deliberate, and
which statically-decidable rules are still missing — read
references/act.md. Use it to calibrate how much of a criterion the engine really settles before you adjudicate. - "Country standard (RGAA, Section 508, EN 301 549)" →
--standard <pack>onreport/prd/criteria/check/verify; seereferences/standards.mdandreferences/methodology.md. For a country-standard audit, PROPOSE the scan by default: such an audit is normatively defined over a declared page sample, and an un-scanned--standard rgaareport is marked partial — say so rather than implying full coverage. The sample workflow (sample check,scan --sample,--merge) is inreferences/audit.md. - "High-assurance audit" →
verify --report … --semantic; seereferences/verify.md. - "Check contrast / rendering (dynamic tier)" →
scan <url> --merge …runs axe-core in a headless browser and, on the local runtime, probes focus visibility, 200% zoom, text spacing and content-on-hover, with bounded stateful interactions and authenticated pages; readreferences/dynamic.md. - "Close the criteria that came back
needs-rendered-dom" → they did not need a better model, they needed a browser.scanpersists WHAT IT MEASURED beside each snapshot (probes.json,axe.json), so a page it zoomed, reflowed and tabbed through can come back conforming rather than « à évaluer » — RGAA 3.2/3.3/10.4/10.11/10.12 close on the measurement, and 10.1/12.8 become adjudicable from the real DOM.verify --manualwarns before you spend a pass,check --require-renderedrefuses, and the crawl is unbounded by default (--max 0/--depth 0), announcing every page it reaches. Readreferences/dynamic.mdandreferences/pages.md.
Orchestration — route by harness
The judgment phases fan out: ADJUDICATE.todo.json (one item per residual criterion) and
VERIFY.todo.json (one entry per NC claim) are independent per-item worklists, and
orchestrate --run <dir> emits a launchable workflow, per-role dispatch contracts and a
sequential RUNBOOK from whichever worklists currently exist. Fan-out is an optimization,
never a requirement: the gates are harness-independent, every phase has a sequential
fallback with identical artifacts, and the fail-closed verify --apply fold always stays
with you. Read references/orchestration.md for the routing table and the rules.
Command cheat sheet
The full surface is in --help; these are the lines you reach for.
node scripts/ultra11y.mjs audit "src/**/*.tsx" --graph --out audits --json # the default audit (AST + cross-file, persisted)
node scripts/ultra11y.mjs audit - < component.html # a snippet via stdin
node scripts/ultra11y.mjs audit --changed --json # only the git diff (large repo)
node scripts/ultra11y.mjs audit --staged --fail-on blocking # gate EXACTLY the staged snapshot (pre-commit)
node scripts/ultra11y.mjs audit "dist/**/*.html" # the RENDERED HTML (reliable for DSFR/MUI…)
node scripts/ultra11y.mjs render --setup # install the capture harvester (tests → .ultra11y/captures)
node scripts/ultra11y.mjs audit --require-captures # gate the components with no rendered capture
node scripts/ultra11y.mjs scan http://localhost:3000 --runtime local --cwd . --merge audits/audit-latest.json
node scripts/ultra11y.mjs check --in audits/audit-latest.json --require-rendered # gate: a rendering criterion left open by a run that rendered nothing
node scripts/ultra11y.mjs pages --in audits/audit-latest.json --standard rgaa --json --out audits # the per-page grid, for a machine (audits/pages.json)
node scripts/ultra11y.mjs verify --report audits/wcag-YYYY-MM-DD.md --in audits/audit-latest.json --manual --out audits
node scripts/ultra11y.mjs verify --apply audits/ADJUDICATE.todo.json --in audits/audit-latest.json --out audits
node scripts/ultra11y.mjs orchestrate --run audits # fan the judgment phases out (--eco for the sequential path)
node scripts/ultra11y.mjs report --in audits/audit-latest.json --out audits # → audits/wcag-YYYY-MM-DD.md
node scripts/ultra11y.mjs prd --in audits/audit-latest.json # the same auditor block as a backlog
node scripts/ultra11y.mjs tickets --in audits/audit-latest.json --dry-run # file it: GitHub/GitLab/Jira
node scripts/ultra11y.mjs check --report audits/wcag-YYYY-MM-DD.md # integrity gate
node scripts/ultra11y.mjs criteria 1.4.3 # one success criterion (--list for all)
node scripts/ultra11y.mjs fix "src/**/*.html" --write --iterate # apply the safe codemods to a fixpoint
node scripts/ultra11y.mjs init --hook # pre-commit gate (--baseline for the regression variant)
node scripts/ultra11y.mjs pack check ./packs/section508.json # gate an (AI-)authored standards pack
--standard rgaa goes on audit too, and then everything speaks RGAA. It is honoured by
audit/scan/fix as well as report/prd/tickets/criteria/check/verify/judge,
and .ultra11yrc.json { "standard": "rgaa" } makes it the default for all of them. Under it,
audit prints an RGAA-titled summary tabulated by thématique with findings tagged [8.4]
rather than [3.1.1], in the pack's own language; --json and --out write a pack-keyed
document (106 criteria, 13 themes) carrying the WCAG core inside core for the pipeline; and
CI annotations, SARIF, the report, the PRD and the per-page sheets name RGAA criteria and
nothing else — no WCAG cross-reference anywhere in a pack deliverable. The engine still keys
on success criteria internally, because a pack criterion is DEFINED as a projection of them;
that is plumbing, and no reader sees it. Add --pack ./pack.json to load a standard at
runtime, and --json anywhere for machine output.
--lang follows the conversation (pass it explicitly — Core rule 7).
The loop: audit → render → judge → fix → re-audit
To converge on conformance (not a single pass), chain the steps, letting the agent drive the judgment and content stages:
-
Audit the source (
audit … --graph) for a first map; on library-rendered code, audit the render (render→ build/SSR →audit) for reliable verdicts (otherwise the scope-risk note reminds you). 1b. RENDER BEFORE YOU ADJUDICATE — this step is not optional when a rendering criterion is open.scan <url|file> --runtime local --merge audits/audit-latest.json --out audits(orscan --sample) drives a real browser, persists each page to.ultra11y/pages/<id>/with what it measured (probes.json,axe.json), and folds it in. Skipping it does not leave those criteria merely open — it makes them undecidable by anyone: no reading of the source settles computed contrast, 200 % zoom, 320 px reflow or text spacing, so every adjudication pass over them costs a model and returnsneeds-rendered-dom. Measured on one RGAA run over a two-file fixture: 80 criteria to adjudicate from source alone, 41 once a single page was scanned — and of the seven a three-pass, $24.90 cascade was left holding, six closed on the measurement.verify --manualnow says so before you spend anything, andcheck --in <audit.json> --require-renderedmakes it a gate. -
Adjudicate & refute with
verify, two worklists. (a)verify --manual --in audit.jsonemitsADJUDICATE.todo.json— one item per residual judgment criterion, pre-loaded with the engine's harvested evidence — which the AI agent rules on (C/NC/NA, ormanualwith areasonwhen it trulyneeds-rendered-dom), each verdict carrying ajustificationor a groundable finding;verify --apply … --in audit.jsonfolds them back FAIL-CLOSED (agent NCs become realagent:<sc>findings that re-render in §2). (b)verify --report … [--semantic]buildsVERIFY.todo.json, which attacks both directions. It refutes anypreliminary/SFC/library-source non-conformity the rendered DOM disproves, and — when a verdict ledger exists, which is the default — it also puts the ledger's agent-adjudicated conformities on trial, one item per citation, question inverted: does this evidence establish the criterion, or only show that its subject exists? Nothing used to challenge aC, so a criterion cleared because its subject was PRESENT rather than RIGHT shipped as a conformance claim. Same verdicts (supported/partial/refuted/unsupported);verify --applydrops the refuted/unsupported non-conformities and sends a refuted conformity's criterion back to « à évaluer » — never to NC, because refuting a conformity proves nothing against the criterion.--no-conformitiesopts out. This includes focus & interaction logic (read the full component source: keyboard operability, focus order/visibility, traps, on-focus/on-input changes; seereferences/focus-and-logic.md) and the per-rule traps inreferences/false-positives.md. Both worklists fan out (orchestrate --run <dir> --phase adjudicate|verify-report— see Orchestration — route by harness); the--applyfold always stays with you. Rule each criterion against its OWN text, not your memory of it.verify --manualalso writes one small brief per criterion (adjudicate/<criteriaId>.md) carrying the criterion's official wording, its numbered tests and — under a country standard — that standard's own test methodology (RGAA publishes one for all 258 of its tests), plus its glossary terms, technical note and particular cases. Read that brief; under--standard rgaait is RGAA that decides, never the WCAG success criterion behind it. Each brief also cites the criterion's official page: if a wording stays ambiguous and you have a web tool you may go read it — never to contradict the vendored text, and a web page is never an acceptablenormativeRef(--no-webdrops the offer; CI drops it by default). -
Fix by priority:
fix --write --iteratefor the mechanical part (anti-regression gate), then hand-apply the judgment/content fixes (alt, labels, structure) guided byreferences/correction.md. -
Re-audit (on the render where relevant) and repeat.
-
Deliver the auditor block.
report(compliance doc: synthesis + one auditor conformance block — theme/criterion/test/WCAG+level/finding/expected/verification — per NC criterion) andprd(the same blocks as an actionable backlog;ticketsfiling one GitHub issue per criterion with that identical block) are two views of the ONE building block, in the language of this conversation (pass--langexplicitly — Core rule 7). -
Hand the change over to
review-a11y, in a subagent. Once the deliverable is out, the code you just fixed still has to be reviewed as a change, and that is the other skill's job — see Handing over toreview-a11ybelow.
Stop when check and verify --apply are green again and only explicitly-named
residual risks remain. (To automate the outer cadence, the harness /loop command can
re-run this cycle.)
Handing over to review-a11y
This skill does the analysis end to end: audit, render, adjudicate, fix, re-audit, deliver.
When it has produced fixes, dispatch review-a11y on the code under change as a subagent,
and report what it returns.
Use the host's native subagent capability: Codex dispatches a subagent with the prompt below;
Claude Code can use Agent(...). If the host exposes neither, follow the sequential route below.
Use the review-a11y skill on the working diff. Return its report verbatim.
Two skills, two scopes, and the split is not cosmetic:
| scope | asks | |
|---|---|---|
ultra11y (this one) | the repository, a page sample, a standard | does this product conform, and where is the proof? |
review-a11y | exactly the code under change (staged, diff, or branch vs merge-base) | is this change safe to merge? |
Why a subagent rather than an inline pass. A full audit fills a context with a 3 MB
engine's output, dozens of reference files and a page sample; a review has to read the diff
closely with none of that in the way. Handing it over keeps the review's judgment
independent of the audit that just ran — the same reason verify refutes findings instead
of trusting them. If the harness has no subagent tool, invoke review-a11y directly: the
routing table in references/orchestration.md names the fallbacks, and the review is
identical either way.
Do not paraphrase its verdict. It returns a severity-ranked review with file:line and a
one-line verdict; that is the deliverable, not a summary of it.
Combining engine, judgment and residual risk
audit classes each criterion C/NC/NA for the static subset and manual for the rest,
each carrying an automatability class that says WHY it is manual and how to close it:
static (the engine decides), needs-rendering (scan), judgment (you, from
the harvested evidence and the decision protocol). Engine NCs are confirmed candidates
with a cited file:line; a preliminary: true finding (SFC/library source) is provisional —
confirm it against the render or refute it. A criterion is never silently marked conforming:
the report is complete only when every applicable criterion is a justified C/NC/NA and
every residual risk is named. See references/methodology.md.
Advisory recommendations are a distinct class. A good-practice signal with NO failing
normative test renders under a dedicated section and can never flip a criterion to NC nor
enter conformancePct. An NC needs a normativeRef; a recommendation does not. Do not
promote one to the other — see references/false-positives.md.
Do not
- Invent a non-conformity the engine did not find and you cannot see (contrast on
inline literal colours is decided statically; computed contrast — external CSS,
variables — goes through
scan(Docker tier) or is verified at render before being declared). - Add ARIA that duplicates native semantics.
- Mark a rendering/judgment criterion "conforming" without a recorded, gated justification
(agent adjudication via
verify --manual, orscanevidence). - Hand-edit
references/criteria.md(generated from the WCAG dataset viacriteria --generate).
Scope
Static engine: offline, deterministic, install-free; inputs are HTML + JSX/TSX (real AST,
cross-file analysis via --graph) + stdin. The rendering criteria (computed contrast,
reflow) are covered by the optional scan tier (axe-core, Docker or --runtime local).
The local runtime additionally probes focus visibility (2.4.7), 200% text zoom (1.4.4),
text spacing (1.4.12) and content-on-hover (1.4.13) — observed in the rendered page, raised
as NC only when the failure is seen (a clean probe leaves the SC manual, never silently
conforming); reading order and alt relevance are the AI agent's judgment, adjudicated from the
harvested evidence and gated (verify --manual).
Data: WCAG 2.2 ©
W3C (W3C Document License); the RGAA pack is RGAA 4.1.2 © DINUM, Licence Ouverte / Etalab
2.0 (see NOTICE).