ultra11y 2.22.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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/docker/Dockerfile +7 -0
  4. package/docker/package.json +9 -0
  5. package/docker/runner.mjs +23 -0
  6. package/package.json +73 -0
  7. package/scripts/bench.mjs +151 -0
  8. package/scripts/build-act-corpus.mjs +143 -0
  9. package/scripts/build-adjudication.mjs +503 -0
  10. package/scripts/build-pack-rgaa.mjs +397 -0
  11. package/scripts/build-standards.mjs +548 -0
  12. package/scripts/copy-bundle.mjs +39 -0
  13. package/scripts/import-pack.mjs +107 -0
  14. package/scripts/sync-engine.mjs +84 -0
  15. package/scripts/sync-version.mjs +43 -0
  16. package/scripts/ultra11y.mjs +52739 -0
  17. package/scripts/vendor/act-testcases.json +12297 -0
  18. package/scripts/vendor/rgaa/SOURCE.json +8 -0
  19. package/scripts/vendor/rgaa/criteres.json +4144 -0
  20. package/scripts/vendor/rgaa/glossaire.json +480 -0
  21. package/scripts/vendor/wcag-2.2-fr.json +82 -0
  22. package/scripts/vendor/wcag-2.2-sc-universe.json +950 -0
  23. package/scripts/vendor/wcag-2.2-sc.json +574 -0
  24. package/scripts/verify-rgaa-source.mjs +126 -0
  25. package/scripts/verify-skill-bundle.mjs +91 -0
  26. package/skills/review-a11y/SKILL.md +123 -0
  27. package/skills/review-a11y/scripts/ultra11y.mjs +52739 -0
  28. package/skills/ultra11y/SKILL.md +292 -0
  29. package/skills/ultra11y/references/act.md +163 -0
  30. package/skills/ultra11y/references/adjudication.md +599 -0
  31. package/skills/ultra11y/references/audit.md +100 -0
  32. package/skills/ultra11y/references/authoring.md +37 -0
  33. package/skills/ultra11y/references/automation.md +113 -0
  34. package/skills/ultra11y/references/correction.md +41 -0
  35. package/skills/ultra11y/references/criteria.md +129 -0
  36. package/skills/ultra11y/references/cross-file.md +82 -0
  37. package/skills/ultra11y/references/dynamic.md +152 -0
  38. package/skills/ultra11y/references/false-positives.md +81 -0
  39. package/skills/ultra11y/references/fix.md +46 -0
  40. package/skills/ultra11y/references/focus-and-logic.md +64 -0
  41. package/skills/ultra11y/references/forbidden-patterns.md +61 -0
  42. package/skills/ultra11y/references/forms-and-errors.md +147 -0
  43. package/skills/ultra11y/references/guidance.md +55 -0
  44. package/skills/ultra11y/references/judgment.md +90 -0
  45. package/skills/ultra11y/references/media-and-motion.md +116 -0
  46. package/skills/ultra11y/references/methodology.md +72 -0
  47. package/skills/ultra11y/references/naming.md +117 -0
  48. package/skills/ultra11y/references/orchestration.md +44 -0
  49. package/skills/ultra11y/references/packs.md +193 -0
  50. package/skills/ultra11y/references/prd.md +104 -0
  51. package/skills/ultra11y/references/rendered.md +110 -0
  52. package/skills/ultra11y/references/scale.md +49 -0
  53. package/skills/ultra11y/references/standards.md +97 -0
  54. package/skills/ultra11y/references/structure.md +138 -0
  55. package/skills/ultra11y/references/verify.md +88 -0
  56. package/skills/ultra11y/references/widgets.md +106 -0
  57. package/skills/ultra11y/scripts/ultra11y.mjs +52739 -0
  58. package/src/act.ts +441 -0
  59. package/src/adjudicate.ts +630 -0
  60. package/src/aria.ts +230 -0
  61. package/src/audit.ts +396 -0
  62. package/src/auditor.ts +399 -0
  63. package/src/autofill.ts +102 -0
  64. package/src/axe-map.ts +294 -0
  65. package/src/baseline.ts +75 -0
  66. package/src/capture.ts +182 -0
  67. package/src/check.ts +243 -0
  68. package/src/cli.ts +1927 -0
  69. package/src/color.ts +102 -0
  70. package/src/config.ts +182 -0
  71. package/src/crawl.ts +75 -0
  72. package/src/criteria.ts +183 -0
  73. package/src/data/adjudication.json +770 -0
  74. package/src/data/guidance/rgaa.json +2395 -0
  75. package/src/data/standards/rgaa.glossary.json +478 -0
  76. package/src/data/standards/rgaa.json +3454 -0
  77. package/src/data/wcag-universe.json +530 -0
  78. package/src/data/wcag.json +1178 -0
  79. package/src/discover.ts +203 -0
  80. package/src/fix/codemods.ts +76 -0
  81. package/src/fix/diff.ts +64 -0
  82. package/src/fix/edits.ts +126 -0
  83. package/src/fix.ts +215 -0
  84. package/src/gh.ts +214 -0
  85. package/src/glob.ts +206 -0
  86. package/src/graph/build.ts +114 -0
  87. package/src/graph/engine-resolve.ts +0 -0
  88. package/src/graph/graph.ts +126 -0
  89. package/src/graph/imports.ts +359 -0
  90. package/src/graph/resolve.ts +73 -0
  91. package/src/graph/tsconfig.ts +113 -0
  92. package/src/grounding.ts +119 -0
  93. package/src/guidance/index.ts +45 -0
  94. package/src/guidance/types.ts +36 -0
  95. package/src/init.ts +113 -0
  96. package/src/mcp/handlers.ts +388 -0
  97. package/src/mcp/http.ts +260 -0
  98. package/src/mcp/prompts.ts +183 -0
  99. package/src/mcp/protocol.ts +187 -0
  100. package/src/mcp/resources.ts +136 -0
  101. package/src/mcp/server.ts +228 -0
  102. package/src/mcp/stdio.ts +119 -0
  103. package/src/mcp/tools.ts +347 -0
  104. package/src/messages.ts +1011 -0
  105. package/src/name.ts +174 -0
  106. package/src/orchestrate-templates.ts +231 -0
  107. package/src/orchestrate.ts +186 -0
  108. package/src/output.ts +108 -0
  109. package/src/pack.ts +187 -0
  110. package/src/parse/html.ts +258 -0
  111. package/src/parse/jsx-ast.ts +67 -0
  112. package/src/parse/jsx-bridge.ts +247 -0
  113. package/src/parse/jsx.ts +19 -0
  114. package/src/parse/source.ts +75 -0
  115. package/src/prd.ts +406 -0
  116. package/src/project-lock.ts +42 -0
  117. package/src/render.ts +307 -0
  118. package/src/report.ts +399 -0
  119. package/src/rules/colors.ts +125 -0
  120. package/src/rules/cross-registry.ts +264 -0
  121. package/src/rules/cross-rule.ts +83 -0
  122. package/src/rules/forms.ts +590 -0
  123. package/src/rules/frames.ts +33 -0
  124. package/src/rules/headings.ts +227 -0
  125. package/src/rules/images.ts +200 -0
  126. package/src/rules/links.ts +193 -0
  127. package/src/rules/mandatory.ts +173 -0
  128. package/src/rules/multimedia.ts +77 -0
  129. package/src/rules/navigation.ts +190 -0
  130. package/src/rules/presentation.ts +75 -0
  131. package/src/rules/registry.ts +57 -0
  132. package/src/rules/rule.ts +122 -0
  133. package/src/rules/scripts-aria.ts +636 -0
  134. package/src/rules/tables.ts +264 -0
  135. package/src/rules/timing.ts +55 -0
  136. package/src/sample.ts +176 -0
  137. package/src/scan-local.ts +866 -0
  138. package/src/scan.ts +538 -0
  139. package/src/standards/derive.ts +195 -0
  140. package/src/standards/index.ts +31 -0
  141. package/src/standards/pack-rules.ts +111 -0
  142. package/src/standards/pack.ts +81 -0
  143. package/src/standards/registry.ts +100 -0
  144. package/src/standards/types.ts +202 -0
  145. package/src/standards/validate.ts +544 -0
  146. package/src/standards/vocabulary.ts +75 -0
  147. package/src/types.ts +355 -0
  148. package/src/util.ts +32 -0
  149. package/src/vendor/codeindex-engine.d.mts +1009 -0
  150. package/src/vendor/codeindex-engine.mjs +13701 -0
  151. package/src/vendor/engine.meta.json +9 -0
  152. package/src/verify.ts +275 -0
  153. package/src/wcag.ts +128 -0
@@ -0,0 +1,152 @@
1
+ # Dynamic tier (axe-core) — optional
2
+
3
+ The static engine leaves some criteria "to assess" because they need a **render**: computed
4
+ contrast (1.4.3), focus visible (2.4.7), reflow/zoom (1.4.4/1.4.10), text spacing (1.4.12),
5
+ content on hover (1.4.13), target size (2.5.8). The dynamic tier decides them by running
6
+ **axe-core in a real headless browser** (Playwright). Two runtimes, same finding shape:
7
+
8
+ - **`--runtime local`** (recommended): resolves a host/target Playwright + `@axe-core/playwright`
9
+ **at runtime** from `--cwd` (no Docker, no global install). Most projects with Playwright
10
+ e2e tests already have both deps. Adds the residual-criteria **probes** (below).
11
+ - **`--runtime docker`**: runs axe-core in a Docker image auto-built on first use (runner +
12
+ Dockerfile embedded in the bundle). No host deps beyond Docker. Axe + 320px reflow only.
13
+ - **`--runtime auto`** (default): local if Playwright resolves from `--cwd`, else Docker, else
14
+ an actionable error.
15
+
16
+ ## Prerequisites
17
+
18
+ - **local**: a project reachable from `--cwd` with `@playwright/test` + `@axe-core/playwright`
19
+ installed, and a Chromium browser (`npx playwright install chromium`).
20
+ - **docker**: Docker running.
21
+
22
+ The rest of the skill (static audit) needs neither.
23
+
24
+ ## Usage
25
+
26
+ ```
27
+ # auto runtime (local if available, else Docker)
28
+ node scripts/ultra11y.mjs scan https://example.com --json
29
+
30
+ # explicit local runtime, resolving deps + browser from a project (e.g. a monorepo package)
31
+ node scripts/ultra11y.mjs scan http://localhost:3000 --runtime local --cwd packages/app --json
32
+
33
+ # authenticated pages: pass a Playwright storageState JSON (cookies/localStorage)
34
+ node scripts/ultra11y.mjs scan http://localhost:3000/dashboard --runtime local \
35
+ --cwd packages/app --storage-state packages/app/test-results/.auth/user.json --json
36
+
37
+ # several explicit URLs at once (one browser, one context per page)
38
+ node scripts/ultra11y.mjs scan http://localhost:3000/ http://localhost:3000/login --runtime local --cwd packages/app
39
+
40
+ # merge with a static audit: the "to assess" criteria turn C/NC
41
+ node scripts/ultra11y.mjs audit "src/**/*.tsx" --jsx --out audits --json > /dev/null
42
+ node scripts/ultra11y.mjs scan http://localhost:3000 --runtime local --cwd packages/app --merge audits/audit-latest.json --out audits
43
+ node scripts/ultra11y.mjs report --in audits/audit-latest.json --out audits
44
+ ```
45
+
46
+ `--storage-state` is **local-only**. Combining it with an **explicit** `--runtime docker` (or
47
+ the `--docker` alias) is an unsupported combination and errors out (exit 2) — the Docker tier
48
+ cannot use a Playwright storageState, and scanning unauthenticated would silently defeat the
49
+ flag. Under `--runtime auto` that happens to fall back to Docker (no local Playwright
50
+ resolved), it degrades with a warning instead — you didn't ask for Docker specifically.
51
+ Produce the file with Playwright (e.g. an e2e auth setup that logs in and
52
+ `context.storageState({ path })`).
53
+
54
+ ### Cover many pages (crawl)
55
+
56
+ ```
57
+ # every URL listed in a sitemap.xml
58
+ node scripts/ultra11y.mjs scan --sitemap https://example.com/sitemap.xml --json
59
+
60
+ # BFS of served same-origin links, from an entry page
61
+ node scripts/ultra11y.mjs scan --crawl https://example.com --depth 2 --max 50 --json
62
+ ```
63
+
64
+ Each finding keeps the **page** it came from (`--merge` reports that URL as `file`). `--crawl`
65
+ follows links in the **served HTML** (SSR/MPA); for a pure SPA, use `--sitemap` or pass the
66
+ URLs explicitly. The crawl fetch is unauthenticated — for authed pages pass explicit URLs +
67
+ `--storage-state` instead.
68
+
69
+ ## What the dynamic tier adds
70
+
71
+ - **Real contrast (1.4.3)** — axe computes the rendered colours (the main win, both runtimes).
72
+ - **Reflow (1.4.10)** — no horizontal scroll at 320px wide (both runtimes).
73
+ - **Cross-check** — axe re-validates the structural criteria (alt, labels, ARIA, headings…) at
74
+ render; a render finding is **authoritative** and turns the criterion NC.
75
+
76
+ axe findings map to WCAG success criteria via a curated table (`axe-rule → SC`), completed by
77
+ axe's **native WCAG tags** (`wcag<abc>`). On merge (`--merge`), a `manual` criterion the tier
78
+ decides leaves the residual risks and becomes `C`/`NC` (ruleId `axe:<rule>` for axe findings).
79
+
80
+ ### Residual-criteria probes (local runtime only)
81
+
82
+ Beyond axe, the local runtime runs bespoke Playwright probes for the criteria axe alone cannot
83
+ decide. Each raises a **definite NC only when the failure is observed** in the rendered page; a
84
+ clean probe leaves the SC `manual` (never silently conforming). Merged findings get a
85
+ `dyn-<engine>` ruleId.
86
+
87
+ | Probe | SC | How |
88
+ |---|---|---|
89
+ | focus visibility | 2.4.7 | Tab through focusables; flag any whose computed style (outline/box-shadow/border/background) is unchanged when focused |
90
+ | 200% zoom | 1.4.4 | Enlarge text to 200%; flag page-level horizontal scroll or text clipped in an `overflow:hidden` container |
91
+ | text spacing | 1.4.12 | Inject the WCAG 1.4.12 spacing override; flag clipped/truncated text |
92
+ | content on hover | 1.4.13 | For `aria-describedby` triggers whose target is hidden, hover to reveal then check it is dismissible (Escape) |
93
+
94
+ Visually-hidden (`clip`/1px sr-only) elements are excluded from these probes. **Target size
95
+ (2.5.8)** is intentionally left to axe-core's own `target-size` rule, which applies the inline
96
+ and 24px-spacing exceptions correctly (a hand-rolled probe was strictly noisier on real pages).
97
+
98
+ These probes are heuristic (conservative severities: focus + zoom `majeur`, the rest `mineur`)
99
+ and **local-only** — the Docker `RUNNER` is kept byte-identical to `docker/runner.mjs`
100
+ (`docker-sync` test), so mirroring the probes into the Docker path is deferred. Adversarially
101
+ verify probe findings (a `verify` pass) before filing them.
102
+
103
+ ### Stateful interaction probes (local runtime, interactions ON by default)
104
+
105
+ The read-only probes above measure a page **as served**. Some non-conformities only appear
106
+ once the user has *interacted* — a filled field that overflows its cell, a status message
107
+ that never reaches a live region. The local runtime therefore also runs a **stateful** pass
108
+ that drives the page, then restores it. **Safety contract**: only NON-navigating actions are
109
+ performed — fill text inputs (a long representative value, respecting `maxlength`), toggle
110
+ checkbox/radio, click `button[type="button"]`. **Never** a link, a submit button, or a form
111
+ submit; every interaction records `location.href` first and aborts + restores if it changed;
112
+ every loop is bounded; original state is always restored.
113
+
114
+ | Stateful probe | SC | What it adds |
115
+ |---|---|---|
116
+ | fill-inputs → re-measure | 1.4.4 / 1.4.10 / 1.4.12 | fills visible text-like inputs with real content, then re-runs the zoom/reflow/spacing stress probes so an overflow that only occurs *when the field holds the value the auditor must type* is caught |
117
+ | live-region | 4.1.3 | triggers safe interactions and checks that a resulting status message lands in an `aria-live`/`role="status"`/`role="alert"` region (status-messages) — the extra SC `localTestedScs` reports only when interactions are on |
118
+
119
+ - **`--no-interact`** disables the whole stateful pass (fill + live-region), leaving only the
120
+ read-only probes — use it when even bounded, non-navigating interaction is unwelcome.
121
+ - **Authenticated-scan click policy.** When a `--storage-state` session is loaded, the
122
+ live-region probe does **not** click buttons by default (even a `type="button"` click can
123
+ trigger a server mutation the `location.href` assertion cannot see). Fill/toggle still run.
124
+ **`--interact-clicks`** re-enables the clicks explicitly; unauthenticated scans keep clicks
125
+ on. Defense-in-depth on top: a button whose accessible name matches a
126
+ destructive/submitting verb (delete, remove, send, submit, confirm, pay…) is **never**
127
+ clicked, in either mode.
128
+
129
+ ## Scan a normative page sample (`scan --sample`)
130
+
131
+ A real country-standard audit runs over a declared **page sample** (échantillon), not one
132
+ URL. Declare it in `.ultra11yrc.json` under `sample.pages` (+ `transverse`), then:
133
+
134
+ ```
135
+ node scripts/ultra11y.mjs sample check # lint the sample's coverage vs the standard's required kinds
136
+ node scripts/ultra11y.mjs scan --sample --runtime local --cwd packages/app --merge audits/audit-latest.json --out audits
137
+ ```
138
+
139
+ `scan --sample` iterates every configured sample page (per-page `--storage-state` supported
140
+ for authenticated pages), keeps each finding's originating **page name + auth flag** as
141
+ provenance (surfaced in the auditor ticket's *Pages / URLs impactées* and *Contexte de
142
+ reproduction*), and `--merge`s them into the audit. `sample check` is an **advisory** lint —
143
+ it reports which required page kinds the sample lacks (a malformed `sample` block is a hard
144
+ error, exit 2; a merely-incomplete one is guidance, exit 0). See `references/audit.md`
145
+ (sample concept) and `references/packs.md` (`sampleMethodology`).
146
+
147
+ ## Limits
148
+
149
+ Even with the local probes, **reading order**, **alt relevance** and the other judgment criteria
150
+ are the AI agent's to adjudicate (gated, `verify --manual`), not the dynamic tier's. The probes
151
+ reduce — but do not eliminate — the residual on 2.4.7/1.4.4/1.4.12/1.4.13/2.5.8: confirm a sample
152
+ on screen (optional human oversight). pa11y can be added as a second source if needed.
@@ -0,0 +1,81 @@
1
+ # Known false-positive traps — what to scrutinise before trusting a finding
2
+
3
+ The static engine reads SOURCE, not the rendered DOM. On component-based code that
4
+ creates a few predictable false-positive (FP) classes. The engine now **auto-suppresses**
5
+ most of them; this page tells you (the agent) where it still cannot be sure, so you can
6
+ **confirm or refute** instead of trusting blindly. Run the refutation loop:
7
+ `verify --report <md> [--semantic]` → rule each item against the cited `file:line` →
8
+ `verify --apply <verdicts.json>` (refuted/unsupported entries fail the gate).
9
+
10
+ ## How the engine already protects you
11
+
12
+ - **Components are skipped.** A PascalCase tag (`<Button>`, `<HistoryEntry>`) keeps its
13
+ case in JSX **and** in `.vue/.svelte/.astro` (parsed with case preserved). Single-file
14
+ rules only fire on intrinsic HTML tags, so a component child of `<ul>`, a `<Select>`,
15
+ or a `role` prop on `<UChatMessage>` is no longer mis-flagged.
16
+ - **Injected content is not "empty".** A `<slot>`, `{@render children()}`, `{children}`,
17
+ or a child component means the name/content/options may be supplied at runtime —
18
+ `empty-heading`, `button-empty-name`, `link-empty-name`, `select-has-option`,
19
+ `list-structure`, `data-table-no-headers` and `h1-missing` all bail instead of asserting.
20
+ - **Dynamic bindings/values count as present, never invalid.** `:aria-label`,
21
+ `v-bind:aria-label`, `bind:value`, `:alt`, `:scope` (Vue/Svelte/Alpine) are read as
22
+ "present, value unknown"; a dynamic `role={…}` is skipped (not parsed as a bogus role);
23
+ a `v-bind="…"` / `{...rest}` / `{shorthand}` spread suppresses missing-name/label/alt.
24
+ - **Cross-file references resolve under `--graph`.** An `aria-controls`/`for`/heading
25
+ target defined in another file (including a `const X = "id"; id={X}`) is proven to exist
26
+ and the finding is suppressed; an icon-only component whose definition supplies its own
27
+ `aria-label` is not flagged "unnamed". **Always run component code with `--graph`.**
28
+ - **Conditional branches & interleaved components aren't crossed.** Headings in different
29
+ arms of `{cond ? … : …}` are mutually exclusive, and a child component between two
30
+ headings may inject an intermediate one — so `heading-order-skip` won't compare across them.
31
+ - **Framework shell templates are recognized.** A SPA mount shell (`<div id="app"></div>`)
32
+ or a build placeholder (`%sveltekit.body%`/`%sveltekit.head%`, `{{ }}`, `<%= %>`) means
33
+ the real content/title is injected later — `h1-missing`/`title-missing-empty` don't fire.
34
+ - **Slot-projected required content is not "missing".** A `<slot>`/`{@render children()}`
35
+ passthrough suppresses `fieldset-legend-missing` and `aria-required-children`; `<th>` in a
36
+ `<thead>` carries implicit column scope.
37
+ - **Test/story markup is out of scope by default.** `*.test.*`, `*.spec.*`, `*.stories.*`
38
+ and `__tests__/` are excluded (bad-by-design fixtures). Re-include with
39
+ `--no-default-excludes` or by naming the file.
40
+
41
+ ## Where the AI agent must still adjudicate
42
+
43
+ A finding's `preliminary: true` (in `--json`) and a `scope.sourceTemplate` / `scope.rendered`
44
+ caveat in the report mean **the verdict is provisional** — the AI agent confirms it against the
45
+ rendered output (rendering criteria go to `scan`), recording a verdict either way.
46
+
47
+ | Rule / situation | Why it may still be wrong | How to confirm or refute |
48
+ |---|---|---|
49
+ | Any finding on `.vue/.svelte/.astro` SOURCE | Slots, snippets, `{#if}`, `v-for`, scoped logic and provide/inject are invisible to the template parser | Audit the **rendered** HTML (Storybook/build output) or `scan` a live page; refute the source finding if the rendered DOM is conformant |
50
+ | Any finding on library-rendered JSX (DSFR/MUI…) | The component's produced HTML lives in `node_modules` | `render` → build/SSR → `audit` the output, or `scan` |
51
+ | `control-label-missing` in an SFC | The label may be wired via provide/inject or a sibling `<Label>` component the parser can't link | Read the form component + its field wrapper; refute if a programmatic label exists at render |
52
+ | `heading-order-skip` via `{#if}`/`v-if` in SFCs (no AST) | SFC conditionals are opaque text, so arms can't be separated like in JSX | Reason about each render path; flag only a skip that occurs in a single path |
53
+ | `contrast` (1.4.3), focus, zoom, reflow | Needs computed styles | `scan` (Docker tier) — never decide from source |
54
+ | `aria-*` referencing a runtime-built id (`id={`\`x-${i}\``}`) | The id is dynamic; cross-file resolution only handles `const` string ids | Read the component; refute if the id is generated and wired correctly |
55
+
56
+ ## Normativity false positives — a "problem" with no failing normative test
57
+
58
+ A finding can cite a real element yet still not be a **non-conformity**, because no test of
59
+ the active standard actually fails. These were the professional-auditor + official-Ara
60
+ corrections against ultra11y's own RGAA audit of egapro; each is now handled as a
61
+ **recommendation (non-normative)** or suppressed outright — never a normative NC.
62
+
63
+ | Case | Why it is NOT an NC | Current handling |
64
+ |---|---|---|
65
+ | An **empty `<th>`** (layout/spacer header cell) | No RGAA/WCAG test says every `<th>` must have content; a best-practice-only axe `empty-table-header` violation carries no `wcag*` tag | merges as **advisory**, attaches to no criterion as NC |
66
+ | A **download link** (`…report.pdf`) whose text doesn't name the format/weight | Good practice (DSFR), but no failing normative test under 6.1/2.4.4 | the RGAA `download-link-format` pack rule is **`advisory: true`** → « Recommandation (non normative) », never flips 6.1 |
67
+ | **Two `<h1>`** on a page / a page **starting at `<h2>`** | "one `<h1>` per page" is not a rule in HTML, a11y, or SEO; no heading-order skip occurs | `h1-multiple` / `h1-missing` are **advisory** (no normative NC) |
68
+ | A **lorem-ipsum tooltip** | Content-quality (UX), not accessibility | emits **no finding** |
69
+
70
+ The distinction is the same one you apply in the judgment phase (`references/judgment.md`):
71
+ an **NC needs a `normativeRef`** citing the precise failed test of the active standard; a
72
+ good practice without one is a `recommendations[]` entry, and a pure UX concern is neither.
73
+ When you adjudicate, don't "promote" a recommendation to an NC to look thorough.
74
+
75
+ ## The rule of thumb
76
+
77
+ The engine raises **confirmed candidates**, never silent conformance. When a finding is
78
+ `preliminary`, or sits on SFC/library source, treat it as "verify, don't trust": open the
79
+ `file:line`, check whether the missing thing is injected/rendered elsewhere, and record a
80
+ `supported`/`refuted` verdict via `verify --apply`. A refuted finding is dropped — that is
81
+ the anti-hallucination contract working as intended.
@@ -0,0 +1,46 @@
1
+ # Put the fixes in place — `fix` (hybrid, native-first)
2
+
3
+ `fix` applies what is **mechanically safe**, prepares what needs a word filled in, and
4
+ **proposes** what is judgment — without ever inventing content (anti-hallucination doctrine).
5
+ By default it is a **dry-run** (diff); `--write` applies.
6
+
7
+ ```
8
+ node scripts/ultra11y.mjs fix "src/**/*.html" # dry-run: diff + list
9
+ node scripts/ultra11y.mjs fix "src/**/*.html" --write # apply the safe fixes
10
+ node scripts/ultra11y.mjs fix --changed --write # only the git diff
11
+ node scripts/ultra11y.mjs fix page.html --only positive-tabindex --write
12
+ ```
13
+
14
+ ## Three classes of fix
15
+
16
+ - **auto** — deterministic, no judgment, written as-is:
17
+ `positive-tabindex` (→ `tabindex="0"`), `redundant-aria` (drop the redundant `role`),
18
+ `meta-viewport-zoom-block` (drop blocking `user-scalable=no` / `maximum-scale`).
19
+ - **fill-in (placeholder)** — inserts a **valid** attribute with a `TODO` sentinel that **you**
20
+ replace with a relevant, in-context value: `html-lang-missing` (`lang="TODO"`),
21
+ `iframe-title-missing` (`title="TODO"`), `img-alt-missing` (`alt="TODO"` or
22
+ `aria-label="TODO"`), `control-label-missing` (`aria-label="TODO"`).
23
+ - **judgment (proposal)** — the engine never fixes on its own: alt relevance (1.1.1), link/
24
+ button purpose (2.4.4/4.1.2), contrast, table/fieldset structure… the AI agent writes the
25
+ content and adjudicates the criterion (`verify --manual`, gated).
26
+
27
+ ## Guarantees
28
+
29
+ - **Edit by source range**: codemods re-read the opening tag locally (attribute offsets are not
30
+ provided by the parser) and apply edits **back-to-front**; edits that overlap on one element
31
+ are dropped, never misapplied.
32
+ - **Anti-regression gate**: after `--write`, the file is re-audited; if a fix **introduced** a
33
+ new non-conformity, it is **not** written.
34
+ - **JSX/TSX: safe codemods only.** `.jsx`/`.tsx` parse to a real AST whose offsets index the
35
+ original file, so `fix --write` applies the codemods marked **jsxSafe** (attribute removal /
36
+ insertion of React-valid props) — it never rewrites an attribute name (`tabIndex={5}` is
37
+ never turned into `tabindex="0"`). Only the rare **lossy** fallback (a file the AST parser
38
+ could not parse at all) stays proposal-only.
39
+
40
+ ## Fix order (native-first)
41
+
42
+ native semantics → accessible names / labels → keyboard access → visible focus → meaning not
43
+ carried by colour alone → media. See `references/authoring.md` and
44
+ `references/forbidden-patterns.md`. After fixing, re-audit; the AI agent adjudicates the judgment
45
+ criteria (`verify --manual`, gated) and the rendering criteria go to `scan` — never marked
46
+ conforming without a recorded, gated justification.
@@ -0,0 +1,64 @@
1
+ # Focus & interaction logic — the agent reads the source and adjudicates the interaction logic
2
+
3
+ A static engine sees *structure*. It cannot see what happens when a user **tabs, types, or
4
+ opens a dialog** — focus visibility, focus order, focus traps, what changes on focus/input,
5
+ whether a mouse interaction has a keyboard equivalent. ultra11y therefore marks every such
6
+ criterion **`manual` / residual risk** (never silently "conforming") and hands it to **the AI
7
+ agent**, which adjudicates it via `verify --manual` (gated). This is the interaction-logic half
8
+ of the division of labour, and it is where reading the *actual source* matters most.
9
+
10
+ > **Read the whole component, not just the report.** The audit parses every element in scope
11
+ > (`audit --graph` resolves imports across files), but a focus/logic verdict requires reading
12
+ > the real source of each interactive component — the JSX, the event handlers, the effects,
13
+ > the state — and reasoning about what a keyboard/screen-reader user experiences.
14
+
15
+ ## The criteria you own (surfaced as residual risks)
16
+
17
+ | SC | What to reason about, reading the source |
18
+ |---|---|
19
+ | **2.1.1 Keyboard** | Every action reachable by mouse is reachable by keyboard. Mouse-only handlers (`onMouseEnter`/`onMouseLeave`/`onClick` on a non-interactive element) need a focus/key equivalent (`onFocus`/`onKeyDown`) or a native interactive element. |
20
+ | **2.1.2 No keyboard trap** | Focus can leave every widget (modals, menus, date pickers, embeds). Trace the trap logic and the Escape/return path. |
21
+ | **2.4.3 Focus order** | Tab order follows meaning. Watch DOM order vs visual order, portals/modals, and `tabIndex` (positive is already flagged; reason about roving `tabIndex` in menus/tabs/grids). |
22
+ | **2.4.7 Focus visible** | A visible focus indicator on every focusable element — check CSS (`:focus`/`:focus-visible`, never a bare `outline: none`) at render. |
23
+ | **2.4.11 Focus not obscured** | The focused element isn't hidden behind sticky headers/footers/overlays. |
24
+ | **3.2.1 On focus / 3.2.2 On input** | Focusing or changing a control doesn't trigger an unexpected context change (auto-submit, navigation, surprise modal). Trace `onFocus`/`onChange` handlers. |
25
+ | **2.5.3 Label in name** | The visible label text is contained in the accessible name (so voice control works). |
26
+
27
+ ## Component patterns to trace (where focus logic lives)
28
+
29
+ - **Dialog / modal**: focus moves into it on open, is trapped while open, returns to the
30
+ trigger on close, Escape closes it, `aria-modal`/`role="dialog"` + a label.
31
+ - **Menu / dropdown / combobox / listbox**: roving `tabIndex` (one tab stop), arrow-key
32
+ navigation, Escape, `aria-expanded`/`aria-activedescendant`, focus returned to the trigger.
33
+ - **Tabs / accordion**: arrow keys move between tabs, focus follows selection appropriately.
34
+ - **Toast / live region**: announced without stealing focus.
35
+ - **Custom controls built on `<div>`/`<span>`**: prefer a native element; if not, they need
36
+ `role`, `tabindex="0"`, key handlers (Enter/Space), and state in ARIA — the engine flags
37
+ the clickable non-interactive element (2.1.1), but YOU verify the full keyboard contract.
38
+
39
+ ## Workflow
40
+
41
+ 1. **Audit the full scope** with `audit --graph` so every component/import is captured.
42
+ 2. **Build the worklists**. `verify --manual --in audit.json` lists each residual (manual)
43
+ focus/logic criterion, pre-loaded with harvested evidence (ARIA state, `tabindex`, control
44
+ labels), for the AI agent to adjudicate; `verify --report … --semantic` lists the engine's
45
+ detected candidates (e.g. a clickable non-interactive element flagged under 2.1.1) to confirm
46
+ or refute — both grounded in the W3C Understanding reference (see `references/verify.md` /
47
+ `references/judgment.md`).
48
+ 3. **For each focus/logic item, open the real source** of the relevant components and reason
49
+ about the keyboard/focus/interaction behaviour above. Adjudicate a residual criterion in
50
+ `ADJUDICATE.todo.json`: `C` (justified) / `NC` (with a groundable `file:line` finding: what
51
+ the keyboard user experiences) / `NA` (justified) / `manual` (with a `reason`). Confirm or
52
+ refute an engine candidate in `VERIFY.todo.json`: `supported` / `partial` / `refuted` /
53
+ `unsupported`, with a concrete cited reason. What needs a rendered DOM (focus *visible*, *not
54
+ obscured*) is marked `needs-rendered-dom` and goes through the `scan` dynamic tier
55
+ (`references/dynamic.md`). The local runtime (`scan --runtime local`) now **probes focus
56
+ visibility (2.4.7)** directly — tabbing through focusables and flagging any whose computed
57
+ style does not change on focus — alongside 200% zoom (1.4.4), text spacing (1.4.12) and
58
+ content-on-hover (1.4.13). A probe finding is a real NC; a clean probe still leaves the SC
59
+ `manual` — never marked conforming on a guess.
60
+ 4. **`verify --apply`** to gate FAIL-CLOSED: an unadjudicated, refuted or unsupported focus/logic
61
+ claim — or an agent verdict missing its justification/finding/reason — fails.
62
+
63
+ The rule that never bends: a focus/interaction criterion the AI agent could not decide by reading
64
+ the source (or rendering) is an **explicit residual risk** with a recorded reason, not a silent pass.
@@ -0,0 +1,61 @@
1
+ # Forbidden anti-patterns (accessible HTML/CSS)
2
+
3
+ 15 common traps. Format: ❌ bad → why → ✅ native fix → WCAG success criterion (engine
4
+ rule when statically detected).
5
+
6
+ ### 1. Clickable `<div>`/`<span>`
7
+ ❌ `<div onClick={…}>` → not focusable, not keyboard-operable, no role.
8
+ ✅ native `<button type="button">`. WCAG 2.1.1 / 4.1.2 (`clickable-noninteractive`).
9
+
10
+ ### 2. `<html>` without `lang`
11
+ ❌ default language undeclared → wrong screen-reader pronunciation.
12
+ ✅ `<html lang="en">`. WCAG 3.1.1 (`html-lang-missing`).
13
+
14
+ ### 3. Page without `<title>` (or empty)
15
+ ❌ missing tab/history title. ✅ a relevant `<title>`. WCAG 2.4.2 (`title-missing-empty`).
16
+
17
+ ### 4. Empty link or button
18
+ ❌ `<a href="/"></a>`, `<button></button>` → no accessible name.
19
+ ✅ visible text, or `aria-label`. WCAG 2.4.4 / 4.1.2 (`link-empty-name`, `button-empty-name`).
20
+
21
+ ### 5. Unnamed icon-only control
22
+ ❌ `<button><svg/></button>` with no name. ✅ `aria-label` or visually-hidden text.
23
+ WCAG 2.4.4 / 4.1.2 (`icon-only-control-unnamed`).
24
+
25
+ ### 6. `<label>` not associated with the field
26
+ ❌ a visual label with no `for`/wrapping. ✅ `<label for="id">` or a wrapping label.
27
+ WCAG 4.1.2 (`control-label-missing`).
28
+
29
+ ### 7. `placeholder` used as a label
30
+ ❌ placeholder alone → disappears on input, not a label. ✅ a real `<label>`.
31
+ WCAG 4.1.2 (`placeholder-as-label`).
32
+
33
+ ### 8. `outline: none` / `outline: 0` on a focusable element
34
+ ❌ removes the keyboard focus indicator. ✅ keep/style a visible focus.
35
+ WCAG 2.4.7 (needs-rendering — verify manually).
36
+
37
+ ### 9. Positive `tabindex`
38
+ ❌ `tabindex="1"` breaks the logical DOM order. ✅ `tabindex="0"` or nothing.
39
+ WCAG 2.4.3 (`positive-tabindex`).
40
+
41
+ ### 10. Informative text baked into an image
42
+ ❌ text-as-image with no alternative. ✅ real text styled with CSS. WCAG 1.4.5 (judgment).
43
+
44
+ ### 11. Meaning carried by CSS alone (`::before`/`::after`)
45
+ ❌ information injected by styles only. ✅ text in the HTML. WCAG 1.3.1 (judgment/rendering).
46
+
47
+ ### 12. Skip link hidden with `display:none`
48
+ ❌ missing target or non-focusable skip link. ✅ a real target + visible on focus.
49
+ WCAG 2.4.1 (`skip-link-target-missing` for the target).
50
+
51
+ ### 13. Navigation not structured as a list
52
+ ❌ a run of links with no `<ul>`. ✅ `<nav><ul><li>…`. WCAG 1.3.1 (judgment).
53
+
54
+ ### 14. Data table without headers
55
+ ❌ `<table>` without `<th>`/`scope`/`<caption>`. ✅ headers + `scope` + caption.
56
+ WCAG 1.3.1 (`data-table-no-headers`, `table-caption-missing`).
57
+
58
+ ### 15. Redundant or broken ARIA
59
+ ❌ `<button role="button">`, `aria-labelledby="nonexistent-id"`, an invented role.
60
+ ✅ native semantics; valid ARIA references. WCAG 4.1.2 (`redundant-aria`,
61
+ `aria-ref-missing-id`, `invalid-aria-role`).
@@ -0,0 +1,147 @@
1
+ # Forms, errors and authentication
2
+
3
+ Forms are where accessibility failures cost the most: a broken image is an annoyance, an
4
+ unusable form is a transaction the user cannot complete. This page covers 1.3.5, 3.2.2,
5
+ 3.3.1–3.3.4 and WCAG 2.2's 3.3.7/3.3.8.
6
+
7
+ ## Labels and instructions (3.3.2, 4.1.2)
8
+
9
+ ```html
10
+ <!-- ✓ the association is programmatic, and clicking the text focuses the field -->
11
+ <label for="email">Email address</label>
12
+ <input id="email" type="email" name="email" autocomplete="email"
13
+ aria-describedby="email-hint" required>
14
+ <p id="email-hint">We use it only to send your receipt.</p>
15
+ ```
16
+
17
+ - A `placeholder` is **not** a label: it disappears on input, is often too low-contrast, and
18
+ is announced inconsistently. Keep it for an example (`e.g. name@example.com`), never for
19
+ the label. Engine rule: `placeholder-as-label`.
20
+ - State the format **before** the error, not only in it: `aria-describedby` on the field.
21
+ - `required` on the control is what assistive tech announces. A red asterisk alone is a
22
+ 1.4.1 failure too — pair it with text ("Required fields are marked *", or a real word).
23
+ - Group related controls: radios and checkboxes in `<fieldset><legend>`, a date split across
24
+ three inputs likewise. Engine rules: `fieldset-legend-missing`,
25
+ `radio-checkbox-group-ungrouped`, `date-fields-ungrouped`.
26
+
27
+ ## 1.3.5 Identify Input Purpose — the token list
28
+
29
+ A field collecting information **about the user** must declare its purpose. The token has to
30
+ come from the HTML autofill vocabulary; an invented one reads as compliant and fills nothing
31
+ (engine rule: `autocomplete-token-invalid`).
32
+
33
+ | Group | Tokens |
34
+ |---|---|
35
+ | Identity | `name` `honorific-prefix` `given-name` `additional-name` `family-name` `honorific-suffix` `nickname` `organization-title` `organization` `sex` `bday` `bday-day` `bday-month` `bday-year` `photo` `language` |
36
+ | Credentials | `username` `new-password` `current-password` `one-time-code` |
37
+ | Address | `street-address` `address-line1` `address-line2` `address-line3` `address-level1`…`address-level4` `country` `country-name` `postal-code` |
38
+ | Payment | `cc-name` `cc-given-name` `cc-additional-name` `cc-family-name` `cc-number` `cc-exp` `cc-exp-month` `cc-exp-year` `cc-csc` `cc-type` `transaction-currency` `transaction-amount` |
39
+ | Contact | `tel` `tel-country-code` `tel-national` `tel-area-code` `tel-local` `tel-local-prefix` `tel-local-suffix` `tel-extension` `email` `impp` `url` |
40
+
41
+ Qualifiers may precede the token: `section-*` (to separate two addresses on one form), then
42
+ `shipping`/`billing`, then `home`/`work`/`mobile`/`fax`/`pager` (contact fields only).
43
+
44
+ ```html
45
+ <input autocomplete="shipping postal-code"> <!-- ✓ -->
46
+ <input autocomplete="section-work work email"> <!-- ✓ -->
47
+ <input autocomplete="postcode"> <!-- ✗ not a token -->
48
+ ```
49
+
50
+ **Applicability:** the criterion covers the USER's own data. An order number, a search term
51
+ or a quantity is not in scope — that is a legitimate `NA` on the field.
52
+
53
+ ## 3.3.1 / 3.3.3 — identifying and fixing errors
54
+
55
+ ```html
56
+ <label for="dob">Date of birth</label>
57
+ <input id="dob" name="dob" aria-invalid="true"
58
+ aria-describedby="dob-err" aria-errormessage="dob-err">
59
+ <p id="dob-err">Enter the date as DD/MM/YYYY — for example 04/03/1990.</p>
60
+ ```
61
+
62
+ - The error must be **in text**, not only a red border (1.4.1 + 3.3.1).
63
+ - It must be **associated** with its field. Engine rules: `error-not-associated`,
64
+ `aria-invalid-no-description`.
65
+ - 3.3.3 asks for the **correction** when it is knowable: "Enter DD/MM/YYYY" beats "Invalid
66
+ date". Withhold only where suggesting it would compromise security.
67
+
68
+ **Error summary pattern** — for a form with several errors, put a summary at the top, move
69
+ focus to it on submit, and link each entry to its field:
70
+
71
+ ```html
72
+ <div role="alert" tabindex="-1" id="errors">
73
+ <h2>There are 2 problems with this form</h2>
74
+ <ul>
75
+ <li><a href="#dob">Date of birth: enter the date as DD/MM/YYYY</a></li>
76
+ <li><a href="#email">Email address: enter an address with an @</a></li>
77
+ </ul>
78
+ </div>
79
+ ```
80
+
81
+ Focus the summary (not the first field): the user hears how many problems there are before
82
+ being dropped into one of them.
83
+
84
+ ## 3.2.2 On Input / 3.2.1 On Focus
85
+
86
+ Changing a value, or merely receiving focus, must not by itself change the context.
87
+
88
+ ```html
89
+ <!-- ✗ selecting an option navigates; keyboard users trigger it while browsing the list -->
90
+ <select onchange="location = this.value">…</select>
91
+
92
+ <!-- ✓ the change is explicit, and announced beforehand -->
93
+ <label for="lang">Language</label>
94
+ <select id="lang">…</select>
95
+ <button type="submit">Change language</button>
96
+ ```
97
+
98
+ Engine rule: `on-input-context-change`. Auto-advancing between one-character code fields is
99
+ the same failure in a friendlier costume — allow it, but keep the form usable without it.
100
+
101
+ ## 3.3.4 Error Prevention (legal, financial, data)
102
+
103
+ For submissions that are legally binding, financial, or that modify/delete user data, one of
104
+ these must hold: **reversible**, **checked** (errors detected and a chance to correct), or
105
+ **confirmed** (a review step before the final commit). A one-click irreversible delete fails.
106
+
107
+ ## 3.3.7 Redundant Entry (WCAG 2.2)
108
+
109
+ Information already entered in the same process must not be asked for again — auto-populate
110
+ it, or offer it for selection. Exceptions: re-entry is essential (confirming a password),
111
+ the earlier information is no longer valid, or it would compromise security.
112
+
113
+ ```html
114
+ <!-- ✓ the shipping address is offered rather than retyped -->
115
+ <label><input type="checkbox" name="same" checked> Billing address is the same as shipping</label>
116
+ ```
117
+
118
+ ## 3.3.8 Accessible Authentication (WCAG 2.2)
119
+
120
+ No step of an authentication process may require a **cognitive function test** — memorising,
121
+ transcribing, or solving a puzzle — unless an alternative, a mechanism to help, object
122
+ recognition, or user-provided content is available.
123
+
124
+ ```html
125
+ <!-- ✗ blocking paste turns login into a memory test and breaks password managers -->
126
+ <input type="password" onpaste="return false">
127
+ <!-- ✗ autofill switched off on a credential field -->
128
+ <input type="password" autocomplete="off">
129
+
130
+ <!-- ✓ declare the real purpose; let the manager fill and the user paste -->
131
+ <input type="password" autocomplete="current-password">
132
+ <input type="text" inputmode="numeric" autocomplete="one-time-code">
133
+ ```
134
+
135
+ Engine rule: `credential-entry-blocked`. Also in scope: a transcription CAPTCHA with no
136
+ alternative, and a code the user must retype from another screen with no way to paste.
137
+
138
+ ## What the engine settles, and what stays yours
139
+
140
+ | Decided by the engine | Yours to adjudicate |
141
+ |---|---|
142
+ | Missing/duplicate labels, placeholder-as-label, ungrouped radios and date parts | Whether a label is **descriptive** (2.4.6) and an instruction sufficient (3.3.2) |
143
+ | `autocomplete` token outside the vocabulary | Whether the field collects **user** data at all (1.3.5 applicability) |
144
+ | Error text not associated, `aria-invalid` with no description | Whether the message says **how to fix it** (3.3.3) |
145
+ | Paste/autofill blocked on a credential field | Whether the whole auth flow imposes a cognitive test (3.3.8) |
146
+ | `onchange` that submits or navigates | Whether a context change was **announced beforehand** (3.2.2) |
147
+ | — | Redundant entry across a multi-step flow (3.3.7) |
@@ -0,0 +1,55 @@
1
+ # Implementation guidance — concrete before/after rules attached to criteria
2
+
3
+ A standards pack says *what* a criterion requires; **guidance** says *how to implement
4
+ it*. It is the layer of concrete, how-to-implement rules — the RGAA SocialGouv/etalab
5
+ good/bad code patterns — keyed to a pack criterion and the WCAG success criteria it maps
6
+ onto. It enriches output without ever changing the verdict.
7
+
8
+ ## Where it shows up
9
+
10
+ Guidance is looked up at **render time** by criterion / WCAG SC, so the canonical
11
+ `audit` JSON stays untouched (no schema change, no new finding). It surfaces in:
12
+
13
+ - **`prd`** — each backlog item gains a before/after example and an effort estimate; the
14
+ `--format doc` PRD weaves it into the user stories and acceptance criteria.
15
+ - **`criteria`** lookups for a pack criterion.
16
+
17
+ Because attachment is presentation-only, guidance never marks anything conforming and
18
+ never invents a non-conformity — it is illustration, not detection.
19
+
20
+ ## Dataset shape
21
+
22
+ A guidance dataset is JSON: `{ pack, source, license, attribution, entries[] }`, where
23
+ each entry is `{ id, criterionId, wcag[], title, summary, impact?, examples[], reference }`
24
+ and an example is `{ lang: "html"|"jsx"|"css", bad?, good?, note? }`. The built-in RGAA
25
+ guidance ships at `src/data/guidance/rgaa.json` (its entry ids mirror the SocialGouv rule
26
+ files). Load external guidance with `--pack <dir>` (a `guidance.json` beside `pack.json`)
27
+ or a `.ultra11yrc.json` `guidance` list. `pack check --guidance` validates that every
28
+ entry resolves to a real criterion, maps to recognized WCAG SCs, and that every example
29
+ parses.
30
+
31
+ ## Honesty rule — guidance is NOT a free detector
32
+
33
+ A pattern earns a **WCAG-core detector** only when it is both statically decidable **and**
34
+ maps to a success criterion present in the WCAG 2.2 **AA** core. Everything else lives
35
+ here as guidance (and, where the country standard covers it, in the pack), never as a
36
+ silent "conforming".
37
+
38
+ The headline example: **opening a new window** (`target="_blank"` without warning). Its
39
+ only clean WCAG home is **3.2.5 Change on Request — which is AAA, absent from the AA
40
+ core** (the dataset jumps 3.2.4 → 3.2.6). So ultra11y does **not** ship a WCAG-core
41
+ detector for it — no static rule flags `target="_blank"` under any standard. It ships as
42
+ RGAA guidance (criterion 13.2 → WCAG 3.2.1): look it up with `criteria --standard rgaa 13.2`,
43
+ and it attaches to the relevant `prd`/`report` entry — it is never raised as an audit
44
+ non-conformity, and never force-mapped onto a wrong AA criterion. (`--standard` is a
45
+ `report`/`prd`/`criteria` flag, not an `audit` one.) Judgment- and
46
+ rendering-dependent rules (alt relevance, computed contrast, reading order) stay guidance
47
+ + residual risk for the same reason.
48
+
49
+ ## Attribution
50
+
51
+ RGAA 4.1.2 © DINUM — Licence Ouverte / Etalab 2.0. The before/after implementation
52
+ patterns are adapted from SocialGouv/skills (`rgaa-html-css`) and the official RGAA
53
+ méthode; the dataset stores short derived summaries + minimal examples, not verbatim prose
54
+ (see `NOTICE`). When ingesting another source, record its license and attribution in the
55
+ dataset header and `NOTICE` before redistributing any text.