ultra11y 2.22.0 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +67 -85
  2. package/package.json +6 -2
  3. package/scripts/build-pack-rgaa.mjs +95 -1
  4. package/scripts/build-standards.mjs +5 -1
  5. package/scripts/ultra11y.mjs +2972 -1694
  6. package/skills/review-a11y/SKILL.md +1 -1
  7. package/skills/review-a11y/scripts/ultra11y.mjs +2972 -1694
  8. package/skills/ultra11y/SKILL.md +38 -2
  9. package/skills/ultra11y/references/ci.md +165 -0
  10. package/skills/ultra11y/references/criteria.md +5 -5
  11. package/skills/ultra11y/references/devtools.md +67 -0
  12. package/skills/ultra11y/references/e2e.md +112 -0
  13. package/skills/ultra11y/references/judgment.md +25 -0
  14. package/skills/ultra11y/references/pages.md +205 -0
  15. package/skills/ultra11y/scripts/ultra11y.mjs +2972 -1694
  16. package/src/adjudicate.ts +270 -45
  17. package/src/annotate.ts +158 -0
  18. package/src/audit.ts +8 -1
  19. package/src/capture.ts +4 -0
  20. package/src/cli.ts +469 -92
  21. package/src/criteria.ts +58 -0
  22. package/src/data/standards/rgaa.json +155 -30
  23. package/src/data/wcag.json +5 -5
  24. package/src/dev.ts +427 -0
  25. package/src/e2e.ts +303 -0
  26. package/src/gh.ts +72 -0
  27. package/src/init.ts +39 -7
  28. package/src/messages.ts +87 -0
  29. package/src/orchestrate-templates.ts +12 -4
  30. package/src/pages.ts +283 -0
  31. package/src/parse/html.ts +12 -0
  32. package/src/pixel.ts +217 -0
  33. package/src/prd.ts +37 -0
  34. package/src/report.ts +28 -2
  35. package/src/rules/registry.ts +2 -0
  36. package/src/rules/rendered.ts +379 -0
  37. package/src/rules/rule.ts +3 -0
  38. package/src/sarif.ts +196 -0
  39. package/src/scan-local.ts +4 -0
  40. package/src/snapshot.ts +442 -0
  41. package/src/standards/derive.ts +32 -1
  42. package/src/types.ts +161 -1
  43. package/src/util.ts +18 -0
  44. package/src/mcp/handlers.ts +0 -388
  45. package/src/mcp/http.ts +0 -260
  46. package/src/mcp/prompts.ts +0 -183
  47. package/src/mcp/protocol.ts +0 -187
  48. package/src/mcp/resources.ts +0 -136
  49. package/src/mcp/server.ts +0 -228
  50. package/src/mcp/stdio.ts +0 -119
  51. package/src/mcp/tools.ts +0 -347
  52. package/src/project-lock.ts +0 -42
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: ultra11y
3
- description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibility and produce a dated auditor-conformance report, OR to AUTHOR/REVIEW accessible markup (native-HTML-first, ARIA last). An install-free engine (`node scripts/ultra11y.mjs`, no keys) runs 72 static checks tied to WCAG criteria — alt/lang/title, unlabeled fields, empty links/buttons, tables, headings, ARIA vocabulary, label-in-name, autocomplete — measured against the official W3C ACT corpus. The engine decides 3 of the 55 criteria; the AI agent adjudicates the 38 judgment ones from harvested evidence + a per-criterion decision protocol (`verify --manual`, gated, fannable via `orchestrate`), and the 14 rendering ones go to `scan` — never silently conforming. Library/SFC code is audited as RENDERED captures (`render --setup`); country standards are pluggable packs (`--standard rgaa`, `scan --sample`). check/verify reject invented non-conformities. Triggers: 'audit WCAG/a11y', 'make accessible', 'fix a11y', 'audit RGAA'."
3
+ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibility and produce a dated auditor-conformance report, OR to AUTHOR/REVIEW accessible markup (native-HTML-first, ARIA last). An install-free engine (`node scripts/ultra11y.mjs`, no keys) runs 78 static checks tied to WCAG criteria — alt/lang/title, unlabeled fields, empty links/buttons, tables, headings, ARIA vocabulary, label-in-name, autocomplete — measured against the official W3C ACT corpus. The engine decides 3 of the 55 criteria; the AI agent adjudicates the 38 judgment ones from harvested evidence + a per-criterion decision protocol (`verify --manual`, gated, fannable via `orchestrate`), and the 14 rendering ones go to `scan` — never silently conforming. Library/SFC code is audited as RENDERED captures (`render --setup`); country standards are pluggable packs (`--standard rgaa`, `scan --sample`). check/verify reject invented non-conformities. Triggers: 'audit WCAG/a11y', 'make accessible', 'fix a11y', 'audit RGAA'."
4
4
  license: MIT
5
5
  metadata:
6
- version: 2.22.0
6
+ version: 2.23.0
7
7
  ---
8
8
 
9
9
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -83,6 +83,10 @@ Domain knowledge first, then the tooling. Read the one that matches the question
83
83
  | `references/verify.md` | The `check`/`verify` gates |
84
84
  | `references/false-positives.md` | Where a finding may still be wrong, and how to refute it |
85
85
  | `references/rendered.md` · `references/automation.md` | Auditing produced HTML, captures, hooks and CI |
86
+ | `references/ci.md` | `--format sarif\|github`: inline PR annotations, code scanning, job summary |
87
+ | `references/pages.md` | Page snapshots + the per-page criterion grid (`pages`), and its honesty rules |
88
+ | `references/e2e.md` | `render --e2e`: auditing a page during your Playwright/Cypress run |
89
+ | `references/devtools.md` | `dev`: the live overlay while you build, and the per-page dashboard |
86
90
  | `references/dynamic.md` | The `scan` tier: runtimes, probes, authenticated pages |
87
91
  | `references/scale.md` | Focusing an audit on a large repository |
88
92
  | `references/fix.md` · `references/correction.md` | Applying fixes, by priority, without regressions |
@@ -153,6 +157,38 @@ Domain knowledge first, then the tooling. Read the one that matches the question
153
157
  "block only NEW non-conformities" variant. For library/SFC code, commit rendered
154
158
  **captures** (`render --setup`) and stage them so the real semantic HTML is what's
155
159
  checked (`audit --require-captures`); read **`references/automation.md`**.
160
+ - **"Show me the problems while I build the page"** → `dev --next` writes a one-line overlay
161
+ component and `dev` starts a loopback side-car: a floating panel lists the current page's
162
+ non-conformities, each opening its `file:line` in your editor, and `http://127.0.0.1:4111`
163
+ accumulates the per-page grid as you browse. Renders nothing outside development; read
164
+ **`references/devtools.md`**.
165
+ - **"Check a page during our E2E tests"** → `render --e2e` writes Playwright/Cypress fixtures
166
+ that audit the page **as your test left it** (logged in, form filled, modal open) — state a
167
+ separate `scan` run does not have — and persist each checked page as a snapshot. `failOn`
168
+ gates the test; `failOn: false` records without failing, which is how you adopt it on an
169
+ existing backlog; read **`references/e2e.md`**.
170
+ - **"Audit the real PAGE, not the component"** → a **page snapshot**
171
+ (`.ultra11y/pages/<id>/`) is the whole rendered document plus the browser-only signals
172
+ (computed styles, boxes, a11y tree, screenshot), ingested by `audit` automatically. Because
173
+ it is a FULL document, the page-scoped rules run on it — that is where RGAA 8.3 (lang),
174
+ 8.5/8.6 (title) and 12.6 become decidable at all; read **`references/pages.md`**.
175
+ - **"Decide the rendering criteria without a browser in CI"** → once a page snapshot exists,
176
+ the **rendered tier** runs inside the ordinary `audit`: computed-style contrast
177
+ (RGAA 3.2/10.5), screenshot-measured contrast for text over a gradient/image (the CSSOM
178
+ blind spot axe-core shares), and colour-only link identification (**RGAA 10.6**, previously
179
+ undecidable). Each leaves the criterion `manual` when it cannot measure — never a guess;
180
+ read **`references/pages.md`**.
181
+ - **"Give me the RGAA grid, page by page"** → `pages --in audits/audit-latest.json --standard
182
+ rgaa` (also embedded in `report`): one row per criterion, one column per page, rebuilt from
183
+ the committed audit.json alone. Two rules hold: a finding is attributed to a page only when
184
+ something SAYS so (else it is reported as unattributed, never spread), and `C` by silence is
185
+ earned only by a page whose real DOM was audited — a source-only page keeps its undecided
186
+ criteria « à évaluer »; read **`references/pages.md`**.
187
+ - **"Show the findings ON the pull request, not just a red job"** → `--format sarif` (upload
188
+ to code scanning → inline annotations at the right file:line) or `--format github`
189
+ (`::error::` workflow commands + a `$GITHUB_STEP_SUMMARY` table) — from `report
190
+ --standard rgaa` when you want the pack's criteria rather than WCAG's; read
191
+ **`references/ci.md`**.
156
192
  - **"Make this code accessible / review it"** → audit the snippet
157
193
  (`audit - < component.html`) native-first; read **`references/authoring.md`** and
158
194
  **`references/forbidden-patterns.md`**.
@@ -0,0 +1,165 @@
1
+ # CI — the shipped action, SARIF, annotations, job summary
2
+
3
+ ## The action
4
+
5
+ The whole surface in one step. The engine ships **inside** the action (one bundled `.mjs`,
6
+ zero dependencies), so there is nothing to install, no `setup-node`, and no version that can
7
+ drift from the action's own.
8
+
9
+ ```yaml
10
+ permissions:
11
+ contents: read
12
+ security-events: write # SARIF upload; without it the annotations still report
13
+ pull-requests: write # only if you set `comment: 'true'`
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with: { fetch-depth: 0 } # the diff gate needs the base ref
18
+ - uses: maxgfr/ultra11y@v2 # or pin the exact version, as `init --ci` does
19
+ with:
20
+ since: auto # the PR's base branch
21
+ standard: rgaa
22
+ fail-on: blocking
23
+ baseline: audits/baseline.json
24
+ comment: 'true'
25
+ ```
26
+
27
+ Page by page, with a real browser, in the same step:
28
+
29
+ ```yaml
30
+ - uses: maxgfr/ultra11y@v2
31
+ with:
32
+ standard: rgaa
33
+ start: npm run start
34
+ wait-on: http://localhost:3000
35
+ urls: http://localhost:3000 http://localhost:3000/contact
36
+ # …or `sample: 'true'` to scan the sample declared in .ultra11yrc.json
37
+ ```
38
+
39
+ `ultra11y init --ci` writes a workflow using it, **pinned to the exact engine version that
40
+ generated the file** (`@v<that version>`) so a CI run stays reproducible. `@v2` is a moving major alias
41
+ the release workflow keeps pointing at the latest release, for teams who would rather take
42
+ fixes automatically. Never `@main`: it would change under you without a version to blame.
43
+
44
+ **Order matters, and it is deliberate**: the audit runs first, then SARIF, annotations, the
45
+ summary, the comment and the report — and the **gate runs last**. A failing audit has
46
+ therefore already produced every surface, so a red job is never a dead end. `fail-on: ''`
47
+ turns the gate off entirely (report-only), which is how you adopt this on an existing backlog;
48
+ `baseline` is the other way — only NEW non-conformities can fail.
49
+
50
+ The SARIF upload is `continue-on-error`: a repository without Advanced Security keeps its
51
+ annotations instead of failing. Each upload carries `category: ultra11y-<standard>`, so a WCAG
52
+ run and an RGAA run coexist rather than overwriting each other. The Markdown report goes
53
+ through `check` before being uploaded, so CI cannot publish a report citing an invented
54
+ criterion.
55
+
56
+ # The `--format` renderings
57
+
58
+ A gate tells a developer *that* the build failed; it does not put the non-conformity **on the
59
+ line of code that caused it**. These two renderings do — they are what the action posts, and
60
+ you can run them yourself outside it.
61
+
62
+ Nothing new is measured here. Both are projections of the same `AuditResult`.
63
+
64
+ ```
65
+ node scripts/ultra11y.mjs audit "src/**/*.tsx" --jsx --format sarif > a11y.sarif
66
+ node scripts/ultra11y.mjs audit "src/**/*.tsx" --jsx --format github
67
+ node scripts/ultra11y.mjs report --in audits/audit-latest.json --standard rgaa --format sarif > rgaa.sarif
68
+ ```
69
+
70
+ ## `--format sarif` — inline PR annotations via code scanning
71
+
72
+ SARIF 2.1.0, the format GitHub code scanning ingests. Upload it and every finding becomes an
73
+ annotation on the right file and line of the pull request, plus an entry in the Security tab.
74
+
75
+ ```yaml
76
+ - run: node scripts/ultra11y.mjs audit "src/**/*.tsx" --jsx --format sarif > a11y.sarif
77
+ - uses: github/codeql-action/upload-sarif@v3
78
+ with: { sarif_file: a11y.sarif }
79
+ ```
80
+
81
+ What the projection guarantees:
82
+
83
+ - **Levels.** `bloquant → error`, `majeur → warning`, `mineur → note`. An **advisory**
84
+ (non-normative recommendation) is **never** an `error`, whatever its severity rank — it is
85
+ always a `note`, so a recommendation cannot fail a reviewer's build on principle.
86
+ - **Stable alert identity.** `partialFingerprints` reuses `findingId()` — the *same* identity
87
+ the baseline gate uses — so an alert survives line drift instead of being closed and
88
+ reopened on every edit above it.
89
+ - **Rules table.** One entry per `ruleId`, tagged `accessibility`, `wcag:<sc>` and, under
90
+ `--standard`, `<pack>:<criterion>` (e.g. `rgaa:1.1`), with the WCAG *Understanding* page as
91
+ `helpUri`.
92
+ - **Repo-relative URIs.** An absolute path is relativised against the working directory;
93
+ GitHub matches alerts against the checkout and would silently drop an absolute one.
94
+ - **No invented locations.** A finding keyed to a **URL** (a `scan` result the host-anchor
95
+ resolver could not map back to source) is emitted with **no** physical location and its URL
96
+ in `properties.url`. Pinning it to a guessed `file:line` is exactly the class of error the
97
+ engine exists to refuse.
98
+
99
+ ## `--format github` — annotations without code scanning
100
+
101
+ Advanced Security is not available on every plan. This mode writes
102
+ `::error file=…,line=…,col=…,title=…::` workflow commands to **stdout**, which every GitHub
103
+ plan renders inline on the diff, and appends a Markdown job summary to
104
+ `$GITHUB_STEP_SUMMARY` (severity table, and the per-page synthesis once a page sample has
105
+ been scanned and merged). With no `$GITHUB_STEP_SUMMARY` set, the summary goes to stderr so a
106
+ local run still shows it.
107
+
108
+ URL-keyed findings are **skipped** for annotations — there is no repo line to point at — and
109
+ counted in the summary instead, so they are never silently dropped.
110
+
111
+ ## Which command to run it from
112
+
113
+ `audit` is always WCAG-keyed: it takes **no** `--standard`, by design (a pack is a derived
114
+ view, never a second source of truth). So:
115
+
116
+ | You want | Run |
117
+ |---|---|
118
+ | WCAG-keyed CI output, one shot | `audit … --format sarif` |
119
+ | **RGAA**-keyed CI output | `audit … --out audits` then `report --in audits/audit-latest.json --standard rgaa --format sarif` |
120
+ | Only what the PR introduced | `audit --since origin/main --baseline audits/baseline.json --format sarif` |
121
+
122
+ In `--baseline` gate mode the CI rendering covers **exactly the new findings** — the subject
123
+ of the gate is the regression, not the backlog, so a PR is annotated with what it introduced
124
+ and the existing backlog stays out of the diff.
125
+
126
+ Uploading two SARIF files in one workflow (a source audit and a page scan) is supported:
127
+ each run carries a distinct `automationDetails.id` (`ultra11y/<standard>/`), so GitHub keeps
128
+ them as separate analyses instead of one overwriting the other.
129
+
130
+ ## The sticky PR comment
131
+
132
+ `ULTRA11Y_PR_COMMENT=1` (what the action's `comment: 'true'` sets) posts the job summary as a
133
+ pull-request comment and **edits it in place** on every subsequent run — a CI job that appends
134
+ a fresh comment on each push turns a busy PR into a wall of stale audits. The comment is keyed
135
+ by an invisible marker that includes the standard, so a WCAG run and an RGAA run keep separate
136
+ comments instead of overwriting each other, and a human's comment is never adopted (an edit is
137
+ destructive, so the match has to be exact).
138
+
139
+ It is best-effort, like issue creation: off a pull request, or with no `gh`/no auth, it
140
+ reports `skipped` and the run carries on. A comment is never worth failing a build over. If
141
+ listing the existing comments fails (permissions, rate limit) it creates a new one — a
142
+ duplicate comment is a far smaller harm than dropping the report.
143
+
144
+ ## How this repo publishes itself (npm trusted publishing)
145
+
146
+ ultra11y's own release workflow holds **no npm token**. It publishes by OIDC: the job asks
147
+ GitHub for a short-lived id-token (`permissions: id-token: write`) and exchanges it with
148
+ registry.npmjs.org for a credential scoped to this package. Nothing long-lived is stored, so
149
+ there is no secret to leak or rotate.
150
+
151
+ Two things it depends on, both easy to break silently:
152
+
153
+ - **A trusted publisher** configured on npmjs.com for the package, naming this repository and
154
+ **this workflow file**. Rename `release.yml` and the exchange stops matching.
155
+ - **npm >= 11.5.1** on the runner. `@semantic-release/npm` only *pre-flights* the exchange;
156
+ the publish itself is `npm publish`, and the CLI does its own OIDC handshake. Node 24 still
157
+ ships an older 11.x, so the workflow upgrades the CLI explicitly.
158
+
159
+ The package must exist before a trusted publisher can be attached to it, so the **first**
160
+ publish is manual — `npm publish` from a logged-in shell, after `pnpm run build`. The name is
161
+ unscoped and `publishConfig.access` is set, so nothing else is needed. Every release after
162
+ that is tokenless.
163
+
164
+ If the exchange fails, `@semantic-release/npm` logs why and falls back to demanding a token —
165
+ it does not publish unauthenticated.
@@ -31,20 +31,20 @@ criteria that map to it. SC ids, titles and levels are derived from the W3C sour
31
31
  | 1.3.1 | Info and Relationships | A | judgment | fieldset-legend-missing, data-table-no-headers, table-caption-missing, layout-table-data-markup, heading-order-skip, h1-missing, h1-multiple, list-structure, dl-structure, empty-heading, label-for-dangling, missing-main-landmark, multiple-main-landmark, sortable-header-no-aria-sort, nav-landmark-missing, nav-landmark-unnamed, aria-required-parent, headers-attr-dangling, th-no-data-cells, radio-checkbox-group-ungrouped, table-empty-data-cell, css-generated-content-informative | 3.1, 5.1, 5.2, 5.4, 5.5, 5.6, 5.7, 5.8, 7.3, 8.9, 9.1, 9.2, 9.3, 9.4, 10.1, 10.2, 11.1, 11.5, 11.6, 11.7, 11.8, 12.6, 13.3, 13.4 |
32
32
  | 1.3.2 | Meaningful Sequence | A | judgment | — | 5.3, 8.10, 10.1, 10.3, 10.8, 13.3, 13.4 |
33
33
  | 1.3.3 | Sensory Characteristics | A | judgment | — | 10.9, 10.10 |
34
- | 1.3.4 | Orientation | AA | needs-rendering | — | 13.9 |
34
+ | 1.3.4 | Orientation | AA | needs-rendering | rendered-orientation-lock | 13.9 |
35
35
  | 1.3.5 | Identify Input Purpose | AA | judgment | field-purpose-incomplete, autocomplete-token-invalid | 11.13 |
36
36
 
37
37
  ## 1.4 Distinguishable
38
38
 
39
39
  | SC | Title | Level | Automatability | Rules | RGAA |
40
40
  |---|---|---|---|---|---|
41
- | 1.4.1 | Use of Color | A | needs-rendering | — | 3.1, 10.6, 10.7, 10.9, 10.10 |
41
+ | 1.4.1 | Use of Color | A | needs-rendering | rendered-link-colour-only | 3.1, 10.6, 10.7, 10.9, 10.10 |
42
42
  | 1.4.2 | Audio Control | A | static | autoplay-media | 4.10 |
43
- | 1.4.3 | Contrast (Minimum) | AA | needs-rendering | contrast-literal | 3.2, 10.5 |
43
+ | 1.4.3 | Contrast (Minimum) | AA | needs-rendering | contrast-literal, rendered-contrast, rendered-contrast-pixel | 3.2, 10.5 |
44
44
  | 1.4.4 | Resize Text | AA | needs-rendering | meta-viewport-zoom-block | 10.4 |
45
45
  | 1.4.5 | Images of Text | AA | needs-rendering | — | 1.8 |
46
46
  | 1.4.10 | Reflow | AA | needs-rendering | — | 10.11 |
47
- | 1.4.11 | Non-text Contrast | AA | needs-rendering | — | 3.3 |
47
+ | 1.4.11 | Non-text Contrast | AA | needs-rendering | rendered-nontext-contrast | 3.3 |
48
48
  | 1.4.12 | Text Spacing | AA | needs-rendering | — | 10.12 |
49
49
  | 1.4.13 | Content on Hover or Focus | AA | needs-rendering | — | 10.13 |
50
50
 
@@ -79,7 +79,7 @@ criteria that map to it. SC ids, titles and levels are derived from the W3C sour
79
79
  | 2.4.4 | Link Purpose (In Context) | A | judgment | link-empty-name, icon-only-control-unnamed | 6.1, 6.2 |
80
80
  | 2.4.5 | Multiple Ways | AA | judgment | — | 12.1, 12.3, 12.4 |
81
81
  | 2.4.6 | Headings and Labels | AA | judgment | — | 9.1, 11.1, 11.2 |
82
- | 2.4.7 | Focus Visible | AA | needs-rendering | — | 7.3, 10.7 |
82
+ | 2.4.7 | Focus Visible | AA | needs-rendering | rendered-focus-not-visible | 7.3, 10.7 |
83
83
  | 2.4.11 | Focus Not Obscured (Minimum) | AA | needs-rendering | — | — |
84
84
 
85
85
  ## 2.5 Input Modalities
@@ -0,0 +1,67 @@
1
+ # See the defects while you build — `dev`
2
+
3
+ An audit at the end of a sprint finds the same defect fifty times. The cheapest moment to fix
4
+ an accessibility problem is the moment you write it, in the page you are looking at.
5
+
6
+ ```
7
+ node scripts/ultra11y.mjs dev --next # write the overlay component (once)
8
+ node scripts/ultra11y.mjs dev # start the side-car, then run your app as usual
9
+ ```
10
+
11
+ Two halves, deliberately separate:
12
+
13
+ - **the side-car** — a `node:http` server (a builtin: the zero-dependency promise holds) on
14
+ `http://127.0.0.1:4111`. It receives collected pages, writes them as snapshots, audits them,
15
+ and serves the dashboard.
16
+ - **the overlay** — framework-agnostic vanilla JS, served by the side-car and loaded by the
17
+ app. A floating panel lists the current page's non-conformities; each links to its
18
+ `file:line` and opens it in your editor through Next's own `/__nextjs_launch-editor`
19
+ endpoint. It re-checks on every client-side navigation, not just the first paint.
20
+
21
+ ## Wiring it into Next
22
+
23
+ `dev --next` writes `.ultra11y/next/overlay.jsx`. You add one line:
24
+
25
+ ```tsx
26
+ // app/layout.tsx
27
+ import { Ultra11yOverlay } from "../.ultra11y/next/overlay";
28
+
29
+ <body>{children}<Ultra11yOverlay /></body>
30
+ ```
31
+
32
+ A component you import, not a bundler hack — deliberately. Custom webpack configuration does
33
+ not exist under Turbopack (the Next default), and entry-point patching breaks across Next
34
+ versions; a client component does not. It **renders nothing outside development**: the whole
35
+ body short-circuits on `NODE_ENV`, so shipping it is inert.
36
+
37
+ The same overlay serves any framework — point a `<script src="http://127.0.0.1:4111/overlay.js">`
38
+ at it in dev and set `window.__ULTRA11Y_ENDPOINT__` if you moved the port.
39
+
40
+ ## The dashboard
41
+
42
+ `http://127.0.0.1:4111` renders the **per-page criterion grid** (`references/pages.md`) for
43
+ every page you have visited: one row per criterion, one column per page, in the standard you
44
+ chose (`--standard rgaa`). It is a self-contained page — no external stylesheet, no script, no
45
+ network — and it accumulates as you browse. Visit five pages of your app and you have the
46
+ beginnings of a real échantillon, snapshotted and re-auditable offline.
47
+
48
+ ## Two things worth knowing
49
+
50
+ **The overlay removes itself from the page before collecting.** It is DOM, and the collector
51
+ serializes DOM: left in place it would be captured, audited, and would report non-conformities
52
+ about *itself* — and, worse, its element would shift every document-order index by one and
53
+ break the join between the styles digest and the DOM. So the host is detached for the duration
54
+ of the collection and re-attached immediately after. It also lives in a shadow root, so the
55
+ app's CSS cannot reach it and its own cannot leak.
56
+
57
+ **The server binds to loopback only.** It writes files and returns audit results, so it is
58
+ never reachable from the network — `127.0.0.1`, never `0.0.0.0`. CORS is permissive because
59
+ the app runs on a different port; the loopback bind is what actually contains it. This is a
60
+ development tool, not something to expose.
61
+
62
+ ## What it does NOT do
63
+
64
+ It runs the same static engine as `audit`, plus the rendered tier when the page carries
65
+ signals. It does not adjudicate the judgment criteria — alt relevance, link purpose, reading
66
+ order remain yours (`verify --manual`) — and a clean panel means "the engine found nothing
67
+ here", never "this page is accessible".
@@ -0,0 +1,112 @@
1
+ # Run the audit during your E2E tests (Playwright / Cypress)
2
+
3
+ `scan` launches its **own** browser after the fact. That is a second run of your app, with
4
+ none of the state your tests just built: no login, no filled form, no opened modal. The E2E
5
+ integration inverts it — the audit runs **inside the run you already have**, on the page as
6
+ your test left it.
7
+
8
+ ```
9
+ node scripts/ultra11y.mjs render --e2e # detects Playwright / Cypress, writes the fixtures
10
+ node scripts/ultra11y.mjs render --e2e --runner cypress # or force one
11
+ ```
12
+
13
+ Fixtures land in `.ultra11y/e2e/`. They are **generated files**, not a published library: no
14
+ install, and they target the engine build that produced them (`ULTRA11Y=<path>` overrides).
15
+
16
+ ## Playwright
17
+
18
+ ```js
19
+ import { test, checkA11y } from "../.ultra11y/e2e/playwright.mjs";
20
+
21
+ test("home page is accessible", async ({ page, ultra11y }) => {
22
+ await page.goto("/");
23
+ await ultra11y({ as: "accueil" }); // via the fixture
24
+ });
25
+
26
+ // …or with no fixture, on any page object:
27
+ await checkA11y(page, { as: "contact", failOn: "major" });
28
+ ```
29
+
30
+ ## Cypress
31
+
32
+ Cypress test code runs in the **browser**, which cannot write to disk, so the collected page
33
+ round-trips through a Node task. Two files, two wiring points:
34
+
35
+ ```js
36
+ // cypress.config.js
37
+ import ultra11y from "./.ultra11y/e2e/cypress-plugin.mjs";
38
+ export default defineConfig({ e2e: { setupNodeEvents(on, config) { ultra11y(on); return config; } } });
39
+
40
+ // cypress/support/e2e.js (the supportFile)
41
+ import "../../.ultra11y/e2e/cypress-commands.mjs";
42
+ ```
43
+
44
+ ```js
45
+ cy.visit("/");
46
+ cy.ultra11y({ as: "accueil" });
47
+ ```
48
+
49
+ ## Options
50
+
51
+ | Option | Effect |
52
+ |---|---|
53
+ | `as` | the page **id** (directory name). Defaults to the slugified URL path |
54
+ | `name` | human page name for the report. Defaults to the document title |
55
+ | `failOn` | `"blocking"` (default), `"major"`, `"minor"`, or `false` to record without ever failing |
56
+ | `auth` | mark the page as sitting behind authentication |
57
+ | `sources` | source files that rendered it — findings are then reported against **your code** |
58
+ | `notes` | reproduction notes carried into the auditor ticket |
59
+
60
+ `failOn: false` is the useful one for adoption: record every page first, look at the real
61
+ backlog, then turn the gate on.
62
+
63
+ ## What it does, exactly
64
+
65
+ Two steps per checked page, and the fixture owns neither:
66
+
67
+ 1. the page is collected **in the browser** with the engine's own collector — DOM,
68
+ computed-style digest, bounding boxes;
69
+ 2. the payload is piped to `ultra11y snapshot write`, which persists
70
+ `.ultra11y/pages/<id>/` **and** audits it, returning the `AuditResult`.
71
+
72
+ So the fixture knows nothing about the snapshot format, the provenance comment or the audit —
73
+ it is a pipe. There is no second implementation to drift out of sync, and any other producer
74
+ can use the same command.
75
+
76
+ ## One difference between the two runners
77
+
78
+ The Playwright fixture also captures a **viewport screenshot** and sends it with the payload,
79
+ which is what feeds the pixel tier (contrast over a gradient or a background image — the case
80
+ computed styles cannot express). Pass `screenshot: false` to skip it.
81
+
82
+ The Cypress command does **not**: `cy.screenshot()` writes to a path Cypress chooses and
83
+ names, so wiring it back into the payload reliably is more machinery than it is worth. A
84
+ Cypress-collected page therefore gets every rule except `rendered-contrast-pixel`, and that
85
+ criterion simply stays undecided for it rather than being guessed. If you need the pixel tier,
86
+ scan those pages separately (`scan <url>`), or capture them with Playwright.
87
+
88
+ Everything else — the DOM, the computed styles, the boxes and the stylesheets — is identical
89
+ between the two.
90
+
91
+ ## Why the artefact matters more than the assertion
92
+
93
+ A failing assertion tells one developer, once. The **snapshot** is the durable part:
94
+
95
+ - It records the page as the browser actually built it, so the same page can be re-audited
96
+ **offline, with no browser** — which is how CI decides rendering criteria without booting
97
+ the app, and how the report speaks page by page.
98
+ - Because it is a full document (a component capture is a fragment), the page-scoped rules run
99
+ on it: **RGAA 8.3** (`lang`), **8.5/8.6** (`title`), 12.6 (`main`). Those are not decidable
100
+ from a component render, nor from source once a framework injects the document shell.
101
+ - Every finding carries `page` **and** `origin.sourceFile`, so a defect found at render time
102
+ is reported against the code that caused it.
103
+
104
+ Commit `.ultra11y/pages/` to gate on it (`audit` ingests the directory automatically). Add
105
+ `sources` to your options and the failure message names your component instead of `dom.html`.
106
+
107
+ ## Scoping
108
+
109
+ `snapshot write` audits **only the page it was handed** — never the whole pages tree — so a
110
+ test checking the contact page is never failed by the home page's backlog. Findings are
111
+ filtered to non-advisory ones before the gate: a non-normative recommendation never fails a
112
+ test.
@@ -88,3 +88,28 @@ Then `node scripts/ultra11y.mjs verify --apply VERIFY.todo.json` is green again
88
88
  > For a country standard's own test grid, `criteria --standard rgaa <id>` shows the RGAA tests
89
89
  > behind a WCAG SC. The pre-completion checklist stops you concluding too early. The rule that
90
90
  > never bends: no residual criterion reaches `C` without a recorded, gated justification.
91
+
92
+ ## Under a country standard, the worklist speaks that standard
93
+
94
+ `verify --manual --standard rgaa` keys the worklist by **RGAA criteria**, not WCAG success
95
+ criteria — which is the granularity that matters, since 99 of RGAA's 106 criteria can only
96
+ ever derive `manual`. Each item carries, inline:
97
+
98
+ - the criterion's **numbered tests, in full** (`11.2.1` … `11.2.6`) — what actually has to be
99
+ ruled on;
100
+ - its **technical note** and **particular cases**;
101
+ - its **implementation guidance** (before/after), previously reserved for criteria that
102
+ already had a finding;
103
+ - the **definitions** of the terms its own tests cite, from the standard's glossary.
104
+
105
+ **`normativeRef` must cite one of the item's OWN tests.** This is not pedantry: an RGAA test
106
+ id has the same `N.N.N` shape as a WCAG success criterion, so a laxer check accepted the WCAG
107
+ id an agent naturally reaches for and read it as an unrelated test — citing `1.4.3` (Contrast
108
+ Minimum) validated as RGAA test 1.4.3, which is about CAPTCHA images. The worklist now lists
109
+ the acceptable references per item, and anything else is rejected.
110
+
111
+ Verdicts fold into `packAdjudication`, not onto the WCAG criteria: a pack decision must not
112
+ rewrite the core verdict, and since WCAG 1.1.1 alone fans out to 19 RGAA criteria, folding by
113
+ success criterion would let those criteria overwrite one another.
114
+
115
+ Look a defined term up on its own with `criteria --standard rgaa --glossary <term>`.