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
NC cites a real, resolvable element (check verifies it).
- Native HTML first, ARIA last; never duplicate implicit semantics.
- Never invent a conformity either: a
C is cited exactly like an NC — it names in
citations[] 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 be C at all; it stays manual. 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 adjudicated C is
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, carrying occurrences, the other anchors in
alsoAt, and the pages it 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 honest C reachable; 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 to
scan — which now CONCLUDES C (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-decided fails 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=pages holds 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 already NC, 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
explicit manual reason), never leave it blank because the global tally looks decided.
- Look the criterion up; never recall it.
criteria [--standard <pack>] <id> — or
ultra11y_criteria over 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 a
lang attribute"; it is test 8.3.1 with two alternative conditions, and the glossary
decides what its terms mean. An auditor block that cites 8.3.1 after 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. See references/mcp.md.
- The FINAL rendered semantic HTML must be correct. The engine sees only source; a
component library (DSFR/MUI…) or
.vue/.svelte/.astro SFC 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/captures and audited, with audit --require-captures gating
the blind spots. See references/automation.md / rendered.md.
- Language: ALWAYS pass
--lang matching 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 --lang yourself.
- 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-live stays aria-live (never « région live »), same for tabindex, 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, then
report (synthesis table + one auditor conformance block per NC criterion — same
block prd and tickets use), then check; read references/audit.md.
- "Code rendered by a library (DSFR, MUI…) or a
.vue/.svelte/.astro SFC / 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 --coverage and audit --require-captures track blind spots). Or
render (build→audit recipe or SSR snapshot --scaffold) then audit on the output, and
scan for computed rendering. SFC-source findings are flagged preliminary (a
scope.sourceTemplate caveat); read references/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 with verify --apply; read references/false-positives.md.
- "Large repo / audit smartly" → focus:
--changed (git diff), template
prioritization, dedup, --max-files; read references/scale.md.
- "Cross-file analysis (tree + dependencies), JSX/TSX as a real AST" →
audit --graph resolves imports and applies cross-file rules (an icon-only component
used without a name, an anchor target in another file…), no browser; read
references/cross-file.md.
- "Generate the fix markdown / PRDs" →
prd (the SAME auditor
conformance block report's NC section renders — theme/criterion/test/WCAG+level/
finding/expected/verification in the active standard's vocabulary — as a backlog);
--split criterion, --format doc for a product-requirements doc, --format remediation
for the legacy dev backlog). It writes markdown only; read
references/prd.md.
- "Open tickets / issues for this (GitHub · GitLab · Jira)" →
tickets, a separate
command that reads the audit and files it, writing no markdown. --grain picks 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-run first; read references/tickets.md.
- "Plug or author a standards pack (RGAA & beyond), AI-ingest external rules" →
--pack/.ultra11yrc.json to load at runtime, pack check to gate it (the
anti-hallucination guardrail), pack scaffold to start one; concrete before/after
implementation guidance attaches to findings/PRD; read references/packs.md and
references/guidance.md.
- "Adjudicate the judgment criteria (judgment phase)" →
verify --manual --in audit.json
emits 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 each verdict — C/NA (with a justification), NC
(with a groundable finding), or manual (with a reason) — then verify --apply … --in audit.json folds them back FAIL-CLOSED; read references/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 + the agents/<role>.md dispatch contracts +
a sequential RUNBOOK.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); read references/focus-and-logic.md.
- "Put the fixes in place" →
fix (dry-run by default, --write applies the safe
codemods, proposes the rest without inventing anything); read references/fix.md.
- "Fix by priority, no regressions (correction phase)" →
fix (--write,
--iterate) + the prd backlog, blocking→major→minor; read references/correction.md.
- "Automatic repo gate (hook / CI)" →
init --hook writes 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/--ci is the opt-in
"block only NEW non-conformities" variant. Installed as a Claude Code plugin, a third
gate runs the review-a11y skill (not just the engine) on a pending commit/push/PR —
that one is shipped by the plugin, not by init. For library/SFC code, commit rendered
captures (render --setup) and stage them so the real semantic HTML is what's
checked (audit --require-captures); read references/automation.md.
- "Show me the problems while I build the page" →
dev --next writes a one-line overlay
component and dev starts a loopback side-car: a floating panel lists the current page's
non-conformities, each opening its file:line in your editor, and http://127.0.0.1:4111
accumulates the per-page grid as you browse. Renders nothing outside development; read
references/devtools.md.
- "Check a page during our E2E tests" →
render --e2e writes Playwright/Cypress fixtures
that audit the page as your test left it (logged in, form filled, modal open) — state a
separate scan run does not have — and persist each checked page as a snapshot. failOn
gates the test; failOn: false records without failing, which is how you adopt it on an
existing backlog; read references/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 by audit automatically. 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; read references/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 criterion manual when it cannot measure — never a guess;
read references/pages.md.
- "Give me the RGAA grid, page by page" →
pages --in audits/audit-latest.json --standard rgaa (also embedded in report): 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), and C by silence is
earned only by a page whose real DOM was audited — a source-only page keeps its undecided
criteria « à évaluer »; read references/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. check gates it against
invented criteria like any other report; read references/pages.md.
- "Which pages should I even audit?" →
pages discover --crawl http://localhost:3000 --write (or --sitemap <url>) writes the sample.pages block for you, taking each page's
name from its served <title> and NEVER overwriting the auth/storageState/notes already
declared. Then sample check lints the coverage and scan --sample scans and snapshots it.
A client-rendered SPA exposes no routes in its served HTML — use a sitemap there; read
references/dynamic.md.
- "Audit these URLs page by page, in CI" → the shipped Action takes
urls, sitemap,
crawl or sample, snapshots every page it visits and writes every criterion's status under
every page both in the job summary and in audits/pages-status.md. Use
pages-report: compact for 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 default true for the detailed page dossiers too. Request a pages/full PR
comment when the same grid is wanted on the pull request; read
references/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 api with an API key, or --runner claude|codex with that local CLI's existing
subscription login (--runner cli still aliases Claude). Inside an agent, prefer
verify --manual and rule the worklist yourself; read references/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_SUMMARY table) — from report --standard rgaa when you want the pack's criteria rather than WCAG's; read
references/ci.md.
- "Make this code accessible / review it" → audit the snippet
(
audit - < component.html) native-first; read references/authoring.md and
references/forbidden-patterns.md.
- "What does criterion X mean" →
criteria (e.g. criteria 1.4.3, or
criteria --standard rgaa 8.3); see references/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> on
report/prd/criteria/check/verify; see references/standards.md and
references/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 rgaa report is marked partial — say so rather than implying
full coverage. The sample workflow (sample check, scan --sample, --merge) is in
references/audit.md.
- "High-assurance audit" →
verify --report … --semantic; see references/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;
read references/dynamic.md.
- "Close the criteria that came back
needs-rendered-dom" → they did not need a better
model, they needed a browser. scan persists 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 --manual warns
before you spend a pass, check --require-rendered refuses, and the crawl is unbounded by
default (--max 0 / --depth 0), announcing every page it reaches. Read
references/dynamic.md and references/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
(or scan --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 returns needs-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 --manual now says so before you spend anything, and
check --in <audit.json> --require-rendered makes it a gate.
Adjudicate & refute with verify, two worklists. (a) verify --manual --in audit.json
emits ADJUDICATE.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, or manual with a
reason when it truly needs-rendered-dom), each verdict carrying a justification or a
groundable finding; verify --apply … --in audit.json folds them back FAIL-CLOSED (agent NCs
become real agent:<sc> findings that re-render in §2). (b) verify --report … [--semantic]
builds VERIFY.todo.json, which attacks both directions. It refutes any
preliminary/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 a
C, so a criterion cleared because its subject was PRESENT rather than RIGHT shipped as a
conformance claim. Same verdicts (supported/partial/refuted/unsupported);
verify --apply drops 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-conformities opts out. This
includes focus & interaction logic (read the full component source: keyboard
operability, focus order/visibility, traps, on-focus/on-input changes; see
references/focus-and-logic.md) and the per-rule traps in references/false-positives.md.
Both worklists fan out (orchestrate --run <dir> --phase adjudicate|verify-report —
see Orchestration — route by harness); the --apply fold always stays with you.
Rule each criterion against its OWN text, not your memory of it. verify --manual also
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 rgaa it 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 acceptable
normativeRef (--no-web drops the offer; CI drops it by default).
Fix by priority: fix --write --iterate for the mechanical part (anti-regression
gate), then hand-apply the judgment/content fixes (alt, labels, structure) guided by
references/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) and prd (the same blocks as an actionable backlog; tickets
filing one GitHub issue per criterion with that identical block) are two views of the
ONE building block, in the language of this conversation (pass --lang explicitly —
Core rule 7).
Offer a review of the change. Suggest /review-a11y on the working diff
after delivering fixes. Wait for the user to invoke it explicitly.
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.)
Offering review-a11y
This skill audits, fixes, and verifies the selected scope. After producing fixes,
suggest a separate /review-a11y invocation to review the working diff. Include
that scope in the handoff and wait for the user to invoke the skill. A request to
run ultra11y alone does not invoke review-a11y.
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, or scan evidence).
- Hand-edit
references/criteria.md (generated from the WCAG dataset via criteria --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).
1---2name: ultra11y3description: Audit and improve accessibility against WCAG 2.2 AA or country standards with reports and verified findings.4license: MIT5---67# ultra11y — audit WCAG 2.2 AA and write accessible markup89On accessibility, an automated tool only sees part of the problem. `ultra11y` owns10that with a **division of labour**: the deterministic, install-free engine11(`node scripts/ultra11y.mjs <command>` — no `npm install`, no key; the JSX/TSX parser12is embedded in the bundle) does the *mechanical* work — detect the machine-checkable13non-conformities and tie each to the right **WCAG success criterion** — and **the active coding14agent** *adjudicates the judgment criteria itself* — alt relevance, link15purpose in context, reading order — statically, from the evidence the engine harvests16(`verify --manual`), each verdict gated by `verify`/`check`. Only the truly **rendered-DOM**17criteria (computed contrast, visible focus, zoom/reflow, content-on-hover) fall to the `scan`18tier (axe-core in a real browser); a human is at most optional oversight. Gates stop any19hallucinated non-conformity from surviving, and nothing is ever silently "conforming".2021**Do not confuse rules with criteria.** The bundle ships 93 static checks, WCAG AA has 5522success criteria, and RGAA has 106 criteria. Several rules may evidence one criterion, one23rule may contribute to several pack criteria, and most criteria remain open until a scan or24an adjudication. The only honest run-level coverage is the grid's live provenance tally25(`moteur + scan + agent`, plus any named undecidable residue), never a fixed promise such as26“48 agent criteria”. GitHub summaries therefore show `decided/total`, not a percentage over27the small decided subset.2829**WCAG 2.2 Level AA is the worldwide core.** Country standards — France's **RGAA**, the30US **Section 508**, the EU **EN 301 549** — are pluggable *standards packs* that map their31criteria onto WCAG. Add `--standard rgaa` to re-key **every command and every output** —32`audit` included, findings and all; **plug an external33pack at runtime** with `--pack ./pack.json` (or a `.ultra11yrc.json`), no rebuild; or34contribute your country (see `references/standards.md`). Packs (and their concrete35**implementation guidance** — the RGAA SocialGouv/etalab good/bad patterns) can be36**AI-ingested** and gated by `pack check` so a fabricated mapping never passes — see37`references/packs.md` and `references/guidance.md`.3839**Most of the work is yours.** Of the 55 WCAG 2.2 AA success criteria, the engine can decide40**3** outright; **14** need a rendered page (`scan`) and **38** are judgment calls the agent41adjudicates. So the engine's clean run is a starting point, never a verdict — and42`references/adjudication.md` is the page you actually work from.4344> **Core rules:**45> 1. **Never invent a non-conformity**: every `NC` cites a real, resolvable element (`check` verifies it).46> 2. **Native HTML first, ARIA last**; never duplicate implicit semantics.47> 3. **Never invent a conformity either**: a `C` is cited exactly like an `NC` — it names in48> `citations[]` the harvested evidence it cleared, each anchor resolvable and drawn from49> the evidence that criterion was actually shown. A criterion presented with *no* evidence50> cannot be `C` at all; it stays `manual`. Neither can one whose reading was INCOMPLETE51> (`evidenceComplete: false`): a criterion you were shown part of can still fail on what52> you saw, but it can never be cleared over what you did not. And an adjudicated `C` is53> never merged with an engine-decided one: reports list it separately and keep it out of54> the automatic pass rate.55> 3b. **What you are shown is the whole population, said once per distinct thing.** Evidence56> is one representative per CONTENT CLASS, carrying `occurrences`, the other anchors in57> `alsoAt`, and the `pages` it appears on — 887 links across 38 captured pages are 9758> distinct (text, href) pairs. Ruling on the representative rules on all of them, which is59> what makes an honest `C` reachable; and a citation naming ANY occurrence of a class the60> criterion carries is accepted, so cite the one you actually opened.61> 4. **Residual is explicit, never silently conforming**: the AI agent *adjudicates* every62> *judgment* criterion itself (`verify --manual`, gated), and the *rendering* criteria go to63> `scan` — which now CONCLUDES `C` (`decidedBy: "scan"`) when every rule carrying a64> criterion ran on every page in scope and raised nothing, and stays silent when even one65> page's signals were incomplete. Any criterion still unproven stays "to assess manually" —66> no status without a recorded, justified verdict. To make that a gate rather than a hope,67> `check --in <audit.json> --require-decided` fails while anything is still to assess, and68> `--allow-undecided <file>` takes the criteria you genuinely cannot decide, each with its69> reason (a named list, never a threshold). `--require-decided=pages` holds EVERY page's own70> grid to the same bar: a criterion failing on one route is settled for the run and may still71> be nobody's verdict on the routes it never fired on, which is what a per-page deliverable is72> actually judged on.73> **The emitted worklist is authoritative.** It may deliberately contain a criterion whose74> run-wide status is already `NC`, because that failure did not decide the criterion on the75> other pages. Such an item is not redundant: return one grounded verdict for it (or an76> explicit `manual` reason), never leave it blank because the global tally looks decided.77> 5. **Look the criterion up; never recall it.** `criteria [--standard <pack>] <id>` — or78> `ultra11y_criteria` over MCP — returns the criterion's own wording, its **numbered79> tests**, and the glossary definitions those tests cite. RGAA 8.3 is not "the page needs a80> `lang` attribute"; it is test `8.3.1` with two alternative conditions, and the glossary81> decides what its terms mean. An auditor block that cites `8.3.1` after reading it is82> grounded; one that cites it from memory is a guess wearing a reference. Before auditing83> against a country standard, run the plan (`ultra11y_method`): it tells you how many of its84> criteria that still need adjudication to earn C — for RGAA, **104 of 106**. See `references/mcp.md`.85> 6. **The FINAL rendered semantic HTML must be correct.** The engine sees only source; a86> component library (DSFR/MUI…) or `.vue`/`.svelte`/`.astro` SFC hides the real markup, so87> a green source audit is not proof. Verify the produced semantic HTML — install the88> zero-touch **capture** harvester (`render --setup`) so every component your tests render89> is serialized to `.ultra11y/captures` and audited, with `audit --require-captures` gating90> the blind spots. See `references/automation.md` / `rendered.md`.91> 7. **Language**: ALWAYS pass `--lang` matching the language of your conversation with92> the user; ask the user when ambiguous. Without the flag the CLI auto-detects (repo93> `<html lang>` → the active standard's default locale → English) — a scripted/CI94> fallback, not a substitute for passing `--lang` yourself.95> 8. **Technical tokens stay in English, even in French prose.** In any French deliverable96> you write (report commentary, PRD, tracker tickets, judgment verdicts), attribute/97> element/role names and their values are code, not prose — never translate them:98> `aria-live` stays `aria-live` (never « région live »), same for `tabindex`, `alt`,99> `role="alert"`, landmark role names. The engine's own fr catalog follows this; match100> it. Normative standard vocabulary (RGAA wording such as « lien d'évitement ») keeps101> its official French.102103## The reference library104105Domain knowledge first, then the tooling. Read the one that matches the question in front of you.106107| Accessibility knowledge | |108|---|---|109| `references/adjudication.md` | How to decide each of the 52 criteria the engine hands you — the decision rule, when NA is legitimate, the questions |110| `references/naming.md` | Accessible-name computation, 2.5.3 label-in-name, visually-hidden CSS, naming images/SVG/figures |111| `references/widgets.md` | ARIA APG composite patterns, keyboard contracts, focus management, SPA route changes, live regions |112| `references/forms-and-errors.md` | Labels and instructions, the `autocomplete` token list, error identification/suggestion/prevention, redundant entry, accessible authentication |113| `references/structure.md` | Headings, landmarks, lists and `dl`, simple and complex tables, meaningful sequence |114| `references/media-and-motion.md` | Captions/audio description/transcripts, pointer gestures, target size, orientation, reflow, reduced motion |115| `references/forbidden-patterns.md` | 15 anti-patterns: bad → why → native fix → criterion |116| `references/focus-and-logic.md` | The focus/keyboard criteria you own, and how to reason about them |117| `references/criteria.md` | The 55 criteria: level, automatability class, engine rules, pack mapping (generated) |118| `references/rgaa-automation.md` | RGAA 4.1.2: generated 106-criterion / 258-test matrix, decisive rules, candidate signals and silence policy |119| `references/act.md` | What the engine is measured to catch — and what it demonstrably does not (generated) |120121| Running the tool | |122|---|---|123| `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 |124| `references/claude-code-report.md` | Claude Code's detailed publication route: rendered scope, direct adjudication, adversarial verification, Markdown + HTML + per-page evidence |125| `references/audit.md` | The audit loop end to end, and the normative page sample |126| `references/judgment.md` | The adjudication + refutation worklists, verdict vocabulary, fail-closed fold |127| `references/verify.md` | The `check`/`verify` gates |128| `references/false-positives.md` | Where a finding may still be wrong, and how to refute it |129| `references/rendered.md` · `references/automation.md` | Auditing produced HTML, captures, hooks and CI |130| `references/harnesses.md` | Where this runs: Claude Code, Codex CLI, OpenCode, and the AGENTS.md fallback |131| `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 |132| `references/ci.md` | `--format sarif\|github`: inline PR annotations, code scanning, job summary |133| `references/pages.md` | Page snapshots + the per-page criterion grid (`pages`), and its honesty rules |134| `references/e2e.md` | `render --e2e`: auditing a page during your Playwright/Cypress run |135| `references/devtools.md` | `dev`: the live overlay while you build, and the per-page dashboard |136| `references/extension.md` | The browser extension: audit the page you are looking at, through the side-car |137| `references/dynamic.md` | The `scan` tier: runtimes, probes, authenticated pages |138| `references/scale.md` | Focusing an audit on a large repository |139| `references/fix.md` · `references/correction.md` | Applying fixes, by priority, without regressions |140| `references/prd.md` | The auditor block as a backlog (markdown) |141| `references/tickets.md` | Filing that backlog as tickets: GitHub, GitLab, Jira, and at which granularity |142| `references/standards.md` · `references/packs.md` · `references/guidance.md` | Country standards, authoring a pack, implementation guidance |143| `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 |144| `references/methodology.md` | Statuses, pass rate, severities, report format |145| `references/cross-file.md` | `--graph`: imports and cross-file rules |146| `references/orchestration.md` | Fanning the judgment phases out across subagents |147148## Choose by situation149150- **"Produce a detailed audit/report in Claude Code"** → use the active Claude session as the151 auditor, close the rendered and judgment criteria, challenge its claims, then publish the152 dated Markdown report, printable HTML and evidenced per-page dossiers. Read153 **`references/claude-code-report.md`**. This is the interactive publication profile; finish154 with its full deliverable set. The compact summary is the CI branch below.155- **"Audit / compliance report"** → `node scripts/ultra11y.mjs audit … --json`, then156 `report` (synthesis table + one **auditor conformance block** per NC criterion — same157 block `prd` and `tickets` use), then `check`; read **`references/audit.md`**.158- **"Code rendered by a library (DSFR, MUI…) or a `.vue`/`.svelte`/`.astro` SFC / avoid159 false negatives"** → audit the **produced HTML**, not the source template. Easiest:160 install the zero-touch **capture** harvester (`render --setup`) so your tests serialize161 every rendered component to `.ultra11y/captures` (auto-ingested, findings attributed to the162 source component; `render --coverage` and `audit --require-captures` track blind spots). Or163 `render` (build→audit recipe or SSR snapshot `--scaffold`) then `audit` on the output, and164 `scan` for computed rendering. SFC-source findings are flagged `preliminary` (a165 `scope.sourceTemplate` caveat); read **`references/rendered.md`**.166- **"A finding looks wrong / false positive on a component"** → the engine auto-suppresses167 most component false positives (slot/prop-injected names, component children, dynamic168 bindings, conditional headings) and marks SFC/library-source findings `preliminary`;169 confirm or refute the rest with `verify --apply`; read **`references/false-positives.md`**.170- **"Large repo / audit smartly"** → focus: `--changed` (git diff), template171 prioritization, dedup, `--max-files`; read **`references/scale.md`**.172- **"Cross-file analysis (tree + dependencies), JSX/TSX as a real AST"** →173 `audit --graph` resolves imports and applies cross-file rules (an icon-only component174 used without a name, an anchor target in another file…), no browser; read175 **`references/cross-file.md`**.176- **"Generate the fix markdown / PRDs"** → `prd` (the SAME auditor177 conformance block `report`'s NC section renders — theme/criterion/test/WCAG+level/178 finding/expected/verification in the active standard's vocabulary — as a backlog);179 `--split criterion`, `--format doc` for a product-requirements doc, `--format remediation`180 for the legacy dev backlog). It writes markdown only; read181 **`references/prd.md`**.182- **"Open tickets / issues for this (GitHub · GitLab · Jira)"** → `tickets`, a separate183 command that reads the audit and files it, writing no markdown. `--grain` picks what one184 ticket is: per criterion (default), per page, per page+criterion, per file, or one185 consolidated. De-dupe is by exact title, so re-running never duplicates. Always186 `--dry-run` first; read **`references/tickets.md`**.187- **"Plug or author a standards pack (RGAA & beyond), AI-ingest external rules"** →188 `--pack`/`.ultra11yrc.json` to load at runtime, `pack check` to gate it (the189 anti-hallucination guardrail), `pack scaffold` to start one; concrete before/after190 implementation guidance attaches to findings/PRD; read **`references/packs.md`** and191 **`references/guidance.md`**.192- **"Adjudicate the judgment criteria (judgment phase)"** → `verify --manual --in audit.json`193 emits an ADJUDICATION worklist (`ADJUDICATE.todo.json` + `ADJUDICATE.md`), one item per194 residual criterion, pre-loaded with the engine's harvested evidence (every alt, link text +195 context, literal colour pairs, control labels, heading outline, ARIA state, tabindex,196 lang-of-parts); the AI agent fills each `verdict` — `C`/`NA` (with a `justification`), `NC`197 (with a groundable finding), or `manual` (with a `reason`) — then `verify --apply … --in198 audit.json` folds them back FAIL-CLOSED; read **`references/judgment.md`**.199- **"Many items to adjudicate/verify (fan the judgment out to subagents)"** →200 `orchestrate --run <dir>` emits, from the run's CURRENT worklists, one launchable201 multi-agent workflow per ready phase + the `agents/<role>.md` dispatch contracts +202 a sequential `RUNBOOK.md` — the default execution path on a subagent-capable harness;203 see **Orchestration — route by harness** below.204- **"Focus, keyboard & interaction logic (the interaction-logic part)"** → the engine marks205 focus order/visible/trap and on-focus/on-input criteria as residual risks; the AI agent reads206 the full component source and adjudicates the keyboard/focus behaviour (visible-focus and the207 other rendered criteria go to `scan`); read **`references/focus-and-logic.md`**.208- **"Put the fixes in place"** → `fix` (dry-run by default, `--write` applies the safe209 codemods, proposes the rest without inventing anything); read **`references/fix.md`**.210- **"Fix by priority, no regressions (correction phase)"** → `fix` (`--write`,211 `--iterate`) + the `prd` backlog, blocking→major→minor; read **`references/correction.md`**.212- **"Automatic repo gate (hook / CI)"** → `init --hook` writes a git pre-commit gate over213 the **strict staged snapshot** (audits the exact index blobs, auto-applies safe fixes and214 re-stages them, blocks only on judgment issues); `init --baseline`/`--ci` is the opt-in215 "block only NEW non-conformities" variant. Installed as a **Claude Code plugin**, a third216 gate runs the `review-a11y` *skill* (not just the engine) on a pending commit/push/PR —217 that one is shipped by the plugin, not by `init`. For library/SFC code, commit rendered218 **captures** (`render --setup`) and stage them so the real semantic HTML is what's219 checked (`audit --require-captures`); read **`references/automation.md`**.220- **"Show me the problems while I build the page"** → `dev --next` writes a one-line overlay221 component and `dev` starts a loopback side-car: a floating panel lists the current page's222 non-conformities, each opening its `file:line` in your editor, and `http://127.0.0.1:4111`223 accumulates the per-page grid as you browse. Renders nothing outside development; read224 **`references/devtools.md`**.225- **"Check a page during our E2E tests"** → `render --e2e` writes Playwright/Cypress fixtures226 that audit the page **as your test left it** (logged in, form filled, modal open) — state a227 separate `scan` run does not have — and persist each checked page as a snapshot. `failOn`228 gates the test; `failOn: false` records without failing, which is how you adopt it on an229 existing backlog; read **`references/e2e.md`**.230- **"Audit the real PAGE, not the component"** → a **page snapshot**231 (`.ultra11y/pages/<id>/`) is the whole rendered document plus the browser-only signals232 (computed styles, boxes, a11y tree, screenshot), ingested by `audit` automatically. Because233 it is a FULL document, the page-scoped rules run on it — that is where RGAA 8.3 (lang),234 8.5/8.6 (title) and 12.6 become decidable at all; read **`references/pages.md`**.235- **"Decide the rendering criteria without a browser in CI"** → once a page snapshot exists,236 the **rendered tier** runs inside the ordinary `audit`: computed-style contrast237 (RGAA 3.2/10.5), screenshot-measured contrast for text over a gradient/image (the CSSOM238 blind spot axe-core shares), and colour-only link identification (**RGAA 10.6**, previously239 undecidable). Each leaves the criterion `manual` when it cannot measure — never a guess;240 read **`references/pages.md`**.241- **"Give me the RGAA grid, page by page"** → `pages --in audits/audit-latest.json --standard242 rgaa` (also embedded in `report`): one row per criterion, one column per page **URL**, rebuilt from243 the committed audit.json alone. Two rules hold: a finding is attributed to a page only when244 something SAYS so (else it is reported as unattributed, never spread), and `C` by silence is245 earned only by a page whose real DOM was audited — a source-only page keeps its undecided246 criteria « à évaluer »; read **`references/pages.md`**.247- **"I want a report PER PAGE, not a matrix"** → `pages --in audits/audit-latest.json248 --standard rgaa --format report --split page --out audits/pages`: one dossier per page —249 identity, its screenshot, its rate, EVERY criterion of the standard with its status on that250 page, then each non-conformity as the ordinary auditor block. Nothing is re-decided (the251 same projection as the grid) and no format is invented (the same auditor block as the252 report), so a page sheet and the compliance report cannot disagree. `check` gates it against253 invented criteria like any other report; read **`references/pages.md`**.254- **"Which pages should I even audit?"** → `pages discover --crawl http://localhost:3000255 --write` (or `--sitemap <url>`) writes the `sample.pages` block for you, taking each page's256 name from its served `<title>` and NEVER overwriting the auth/storageState/notes already257 declared. Then `sample check` lints the coverage and `scan --sample` scans and snapshots it.258 A client-rendered SPA exposes no routes in its served HTML — use a sitemap there; read259 **`references/dynamic.md`**.260- **"Audit these URLs page by page, in CI"** → the shipped Action takes `urls`, `sitemap`,261 `crawl` or `sample`, snapshots every page it visits and writes every criterion's status under262 every page both in the job summary and in `audits/pages-status.md`. Use263 `pages-report: compact` for that status-only package: the Markdown, page JSON, source audit264 and verdict ledger only (no adjudication worklists, remediation report, HTML or crops),265 or keep the default `true` for the detailed page dossiers too. Request a `pages`/`full` PR266 comment when the same grid is wanted on the pull request; read267 **`references/ci.md`**.268- **"Nobody is here to rule on the judgment criteria"** → `judge --in audits/audit-latest.json269 --standard rgaa [--apply]` adjudicates them with a model, for a run with no coding agent in270 the loop (CI, the extension, an E2E run). It is a CALLER, not a second judge: same worklist,271 same evidence, same prompt, and the verdicts pass the same fail-closed gate yours do. Use272 `--runner api` with an API key, or `--runner claude|codex` with that local CLI's existing273 subscription login (`--runner cli` still aliases Claude). Inside an agent, prefer274 `verify --manual` and rule the worklist yourself; read **`references/judgment.md`**.275- **"Show the findings ON the pull request, not just a red job"** → `--format sarif` (upload276 to code scanning → inline annotations at the right file:line) or `--format github`277 (`::error::` workflow commands + a `$GITHUB_STEP_SUMMARY` table) — from `report278 --standard rgaa` when you want the pack's criteria rather than WCAG's; read279 **`references/ci.md`**.280- **"Make this code accessible / review it"** → audit the snippet281 (`audit - < component.html`) native-first; read **`references/authoring.md`** and282 **`references/forbidden-patterns.md`**.283- **"What does criterion X mean"** → `criteria` (e.g. `criteria 1.4.3`, or284 `criteria --standard rgaa 8.3`); see **`references/criteria.md`**.285- **"How trustworthy is a given check / what does the engine NOT catch"** → the engine is286 scored against the **W3C ACT-Rules test corpus** (~1 100 third-party examples): which287 checks are consistent, where recall is partial, which deviations are deliberate, and288 which statically-decidable rules are still missing — read **`references/act.md`**. Use it289 to calibrate how much of a criterion the engine really settles before you adjudicate.290- **"Country standard (RGAA, Section 508, EN 301 549)"** → `--standard <pack>` on291 `report`/`prd`/`criteria`/`check`/`verify`; see **`references/standards.md`** and292 **`references/methodology.md`**. **For a country-standard audit, PROPOSE the scan by293 default**: such an audit is normatively defined over a declared page **sample**, and an294 un-scanned `--standard rgaa` report is marked **partial** — say so rather than implying295 full coverage. The sample workflow (`sample check`, `scan --sample`, `--merge`) is in296 **`references/audit.md`**.297- **"High-assurance audit"** → `verify --report … --semantic`; see **`references/verify.md`**.298- **"Check contrast / rendering (dynamic tier)"** → `scan <url> --merge …` runs axe-core in a299 headless browser and, on the local runtime, probes focus visibility, 200% zoom, text300 spacing and content-on-hover, with bounded stateful interactions and authenticated pages;301 read **`references/dynamic.md`**.302- **"Close the criteria that came back `needs-rendered-dom`"** → they did not need a better303 model, they needed a browser. `scan` persists WHAT IT MEASURED beside each snapshot304 (`probes.json`, `axe.json`), so a page it zoomed, reflowed and tabbed through can come back305 **conforming** rather than « à évaluer » — RGAA 3.2/3.3/10.4/10.11/10.12 close on the306 measurement, and 10.1/12.8 become adjudicable from the real DOM. `verify --manual` warns307 before you spend a pass, `check --require-rendered` refuses, and the crawl is **unbounded by308 default** (`--max 0` / `--depth 0`), announcing every page it reaches. Read309 **`references/dynamic.md`** and **`references/pages.md`**.310311## Orchestration — route by harness312313The judgment phases fan out: `ADJUDICATE.todo.json` (one item per residual criterion) and314`VERIFY.todo.json` (one entry per NC claim) are independent per-item worklists, and315`orchestrate --run <dir>` emits a launchable workflow, per-role dispatch contracts and a316sequential RUNBOOK from whichever worklists currently exist. Fan-out is an optimization,317never a requirement: the gates are harness-independent, every phase has a sequential318fallback with identical artifacts, and the fail-closed `verify --apply` fold always stays319with you. Read **`references/orchestration.md`** for the routing table and the rules.320321## Command cheat sheet322323The full surface is in `--help`; these are the lines you reach for.324325```326node scripts/ultra11y.mjs audit "src/**/*.tsx" --graph --out audits --json # the default audit (AST + cross-file, persisted)327node scripts/ultra11y.mjs audit - < component.html # a snippet via stdin328node scripts/ultra11y.mjs audit --changed --json # only the git diff (large repo)329node scripts/ultra11y.mjs audit --staged --fail-on blocking # gate EXACTLY the staged snapshot (pre-commit)330node scripts/ultra11y.mjs audit "dist/**/*.html" # the RENDERED HTML (reliable for DSFR/MUI…)331node scripts/ultra11y.mjs render --setup # install the capture harvester (tests → .ultra11y/captures)332node scripts/ultra11y.mjs audit --require-captures # gate the components with no rendered capture333node scripts/ultra11y.mjs scan http://localhost:3000 --runtime local --cwd . --merge audits/audit-latest.json334node scripts/ultra11y.mjs check --in audits/audit-latest.json --require-rendered # gate: a rendering criterion left open by a run that rendered nothing335node scripts/ultra11y.mjs pages --in audits/audit-latest.json --standard rgaa --json --out audits # the per-page grid, for a machine (audits/pages.json)336node scripts/ultra11y.mjs verify --report audits/wcag-YYYY-MM-DD.md --in audits/audit-latest.json --manual --out audits337node scripts/ultra11y.mjs verify --apply audits/ADJUDICATE.todo.json --in audits/audit-latest.json --out audits338node scripts/ultra11y.mjs orchestrate --run audits # fan the judgment phases out (--eco for the sequential path)339node scripts/ultra11y.mjs report --in audits/audit-latest.json --out audits # → audits/wcag-YYYY-MM-DD.md340node scripts/ultra11y.mjs prd --in audits/audit-latest.json # the same auditor block as a backlog341node scripts/ultra11y.mjs tickets --in audits/audit-latest.json --dry-run # file it: GitHub/GitLab/Jira342node scripts/ultra11y.mjs check --report audits/wcag-YYYY-MM-DD.md # integrity gate343node scripts/ultra11y.mjs criteria 1.4.3 # one success criterion (--list for all)344node scripts/ultra11y.mjs fix "src/**/*.html" --write --iterate # apply the safe codemods to a fixpoint345node scripts/ultra11y.mjs init --hook # pre-commit gate (--baseline for the regression variant)346node scripts/ultra11y.mjs pack check ./packs/section508.json # gate an (AI-)authored standards pack347```348**`--standard rgaa` goes on `audit` too, and then everything speaks RGAA.** It is honoured by349`audit`/`scan`/`fix` as well as `report`/`prd`/`tickets`/`criteria`/`check`/`verify`/`judge`,350and `.ultra11yrc.json { "standard": "rgaa" }` makes it the default for all of them. Under it,351`audit` prints an RGAA-titled summary tabulated by thématique with findings tagged `[8.4]`352rather than `[3.1.1]`, in the pack's own language; `--json` and `--out` write a pack-keyed353document (106 criteria, 13 themes) carrying the WCAG core inside `core` for the pipeline; and354CI annotations, SARIF, the report, the PRD and the per-page sheets name RGAA criteria and355nothing else — no WCAG cross-reference anywhere in a pack deliverable. The engine still keys356on success criteria internally, because a pack criterion is DEFINED as a projection of them;357that is plumbing, and no reader sees it. Add `--pack ./pack.json` to load a standard at358runtime, and `--json` anywhere for machine output.359`--lang` follows the conversation (pass it explicitly — Core rule 7).360361## The loop: audit → render → judge → fix → re-audit362363To converge on conformance (not a single pass), chain the steps, letting the agent364drive the judgment and content stages:3653661. **Audit** the source (`audit … --graph`) for a first map; on library-rendered code,367 **audit the render** (`render` → build/SSR → `audit`) for reliable verdicts (otherwise368 the scope-risk note reminds you).3691b. **RENDER BEFORE YOU ADJUDICATE — this step is not optional when a rendering criterion is370 open.** `scan <url|file> --runtime local --merge audits/audit-latest.json --out audits`371 (or `scan --sample`) drives a real browser, persists each page to `.ultra11y/pages/<id>/`372 **with what it measured** (`probes.json`, `axe.json`), and folds it in. Skipping it does not373 leave those criteria merely open — it makes them *undecidable by anyone*: no reading of the374 source settles computed contrast, 200 % zoom, 320 px reflow or text spacing, so every375 adjudication pass over them costs a model and returns `needs-rendered-dom`. Measured on one376 RGAA run over a two-file fixture: **80 criteria to adjudicate from source alone, 41 once a377 single page was scanned** — and of the seven a three-pass, $24.90 cascade was left holding,378 six closed on the measurement. `verify --manual` now says so before you spend anything, and379 `check --in <audit.json> --require-rendered` makes it a gate.3802. **Adjudicate & refute** with `verify`, two worklists. (a) `verify --manual --in audit.json`381 emits `ADJUDICATE.todo.json` — one item per residual *judgment* criterion, pre-loaded with the382 engine's harvested evidence — which the AI agent rules on (`C`/`NC`/`NA`, or `manual` with a383 `reason` when it truly `needs-rendered-dom`), each verdict carrying a `justification` or a384 groundable finding; `verify --apply … --in audit.json` folds them back FAIL-CLOSED (agent NCs385 become real `agent:<sc>` findings that re-render in §2). (b) `verify --report … [--semantic]`386 builds `VERIFY.todo.json`, which attacks **both directions**. It refutes any387 `preliminary`/SFC/library-source **non-conformity** the rendered DOM disproves, and — when a388 verdict ledger exists, which is the default — it also puts the ledger's agent-adjudicated389 **conformities** on trial, one item per citation, question inverted: *does this evidence390 establish the criterion, or only show that its subject exists?* Nothing used to challenge a391 `C`, so a criterion cleared because its subject was PRESENT rather than RIGHT shipped as a392 conformance claim. Same verdicts (`supported`/`partial`/`refuted`/`unsupported`);393 `verify --apply` drops the refuted/unsupported non-conformities and sends a refuted394 conformity's criterion back to « à évaluer » — never to NC, because refuting a conformity395 proves nothing against the criterion. `--no-conformities` opts out. This396 includes **focus & interaction logic** (read the full component source: keyboard397 operability, focus order/visibility, traps, on-focus/on-input changes; see398 `references/focus-and-logic.md`) and the per-rule traps in `references/false-positives.md`.399 Both worklists fan out (`orchestrate --run <dir> --phase adjudicate|verify-report` —400 see **Orchestration — route by harness**); the `--apply` fold always stays with you.401 **Rule each criterion against its OWN text, not your memory of it.** `verify --manual` also402 writes one small brief per criterion (`adjudicate/<criteriaId>.md`) carrying the criterion's403 official wording, its numbered tests and — under a country standard — that standard's own404 **test methodology** (RGAA publishes one for all 258 of its tests), plus its glossary terms,405 technical note and particular cases. Read that brief; under `--standard rgaa` it is RGAA406 that decides, never the WCAG success criterion behind it. Each brief also cites the407 criterion's official page: if a wording stays ambiguous and you have a web tool you may go408 read it — never to contradict the vendored text, and a web page is never an acceptable409 `normativeRef` (`--no-web` drops the offer; CI drops it by default).4103. **Fix** by priority: `fix --write --iterate` for the mechanical part (anti-regression411 gate), then hand-apply the judgment/content fixes (alt, labels, structure) guided by412 `references/correction.md`.4134. **Re-audit** (on the render where relevant) and repeat.4145. **Deliver the auditor block.** `report` (compliance doc: synthesis + one auditor415 conformance block — theme/criterion/test/WCAG+level/finding/expected/verification —416 per NC criterion) and `prd` (the same blocks as an actionable backlog; `tickets`417 filing one GitHub issue per criterion with that identical block) are two views of the418 ONE building block, in the language of this conversation (pass `--lang` explicitly —419 Core rule 7).4204216. **Offer a review of the change.** Suggest `/review-a11y` on the working diff422 after delivering fixes. Wait for the user to invoke it explicitly.423424**Stop** when `check` and `verify --apply` are green again and only explicitly-named425residual risks remain. (To automate the outer cadence, the harness `/loop` command can426re-run this cycle.)427428## Offering `review-a11y`429430This skill audits, fixes, and verifies the selected scope. After producing fixes,431suggest a separate `/review-a11y` invocation to review the working diff. Include432that scope in the handoff and wait for the user to invoke the skill. A request to433run `ultra11y` alone does not invoke `review-a11y`.434435## Combining engine, judgment and residual risk436437`audit` classes each criterion `C`/`NC`/`NA` for the static subset and `manual` for the rest,438each carrying an `automatability` class that says WHY it is manual and how to close it:439**`static`** (the engine decides), **`needs-rendering`** (`scan`), **`judgment`** (you, from440the harvested evidence and the decision protocol). Engine `NC`s are confirmed candidates441with a cited `file:line`; a `preliminary: true` finding (SFC/library source) is provisional —442confirm it against the render or refute it. A criterion is never silently marked conforming:443the report is complete only when every applicable criterion is a justified `C`/`NC`/`NA` and444every residual risk is named. See **`references/methodology.md`**.445446**Advisory recommendations are a distinct class.** A good-practice signal with NO failing447normative test renders under a dedicated section and can never flip a criterion to `NC` nor448enter `conformancePct`. An `NC` needs a `normativeRef`; a recommendation does not. Do not449promote one to the other — see **`references/false-positives.md`**.450451## Do not452453- Invent a non-conformity the engine did not find and you cannot see (contrast on454 **inline literal colours** is decided statically; **computed** contrast — external CSS,455 variables — goes through `scan` (Docker tier) or is verified at render before being declared).456- Add ARIA that duplicates native semantics.457- Mark a rendering/judgment criterion "conforming" without a recorded, gated justification458 (agent adjudication via `verify --manual`, or `scan` evidence).459- Hand-edit `references/criteria.md` (generated from the WCAG dataset via `criteria --generate`).460461## Scope462463Static engine: offline, deterministic, install-free; inputs are HTML + JSX/TSX (real AST,464cross-file analysis via `--graph`) + stdin. The **rendering** criteria (computed contrast,465reflow) are covered by the optional `scan` tier (axe-core, Docker **or** `--runtime local`).466The local runtime additionally **probes** focus visibility (2.4.7), 200% text zoom (1.4.4),467text spacing (1.4.12) and content-on-hover (1.4.13) — observed in the rendered page, raised468as NC only when the failure is seen (a clean probe leaves the SC `manual`, never silently469conforming); reading order and alt relevance are the AI agent's judgment, adjudicated from the470harvested evidence and gated (`verify --manual`).471Data: WCAG 2.2 ©472W3C (W3C Document License); the RGAA pack is RGAA 4.1.2 © DINUM, Licence Ouverte / Etalab4732.0 (see `NOTICE`).