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
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Audit HTML/CSS/JSX against **WCAG 2.2 AA** accessibility and produce a dated compliance report — or author/review accessible markup without regressions. A [skills.sh](https://skills.sh) agent skill: a deterministic, zero-dependency static engine **plus** the agent's judgment, with `check`/`verify` gates against hallucinated non-conformities. **The central deliverable is the auditor conformance block** — theme, criterion + official wording, test(s), WCAG mapping + level, finding, expected state, verification, `file:line` occurrences — rendered identically by the `report` (compliance doc), the `prd` backlog and the GitHub issues, in the active standard's vocabulary and **in your language** (`--lang auto` follows the conversation/repo). **WCAG is the worldwide core; country standards (RGAA, …) are pluggable in-repo packs.**
4
4
 
5
- ultra11y is built around an honest **division of labour**. Automated tools only catch a fraction of accessibility problems, so the engine does the *mechanical* work — 72 machine-detectable static checks tied to the WCAG 2.2 success criteria — and is explicit about everything it can't decide statically. What it can't, the **AI agent adjudicates** (statically, from the evidence, gated by `verify`/`check`) — not a deferral to a human:
5
+ ultra11y is built around an honest **division of labour**. Automated tools only catch a fraction of accessibility problems, so the engine does the *mechanical* work — 78 machine-detectable static checks tied to the WCAG 2.2 success criteria — and is explicit about everything it can't decide statically. What it can't, the **AI agent adjudicates** (statically, from the evidence, gated by `verify`/`check`) — not a deferral to a human:
6
6
 
7
7
  - **Automatable (engine):** missing `alt`/`lang`/`title`, unlabeled fields, empty links/buttons, icon-only controls, iframes without title, tables without headers, heading-level skips, empty/dangling headings & labels, duplicate ids, invalid/broken ARIA, positive `tabindex`, autoplay/timed-refresh/`blink`/`marquee` media…
8
8
  - **Agent judgment (gated):** alt-text relevance, link purpose in context, reading/tab order, caption accuracy — the agent rules on these via `verify --manual` → `--apply`, each verdict carrying a justification (or a groundable NC), never a silent "conforming".
@@ -50,9 +50,14 @@ node scripts/ultra11y.mjs --help
50
50
  ultra11y audit <globs… | -> [--out <dir>] [--include <glob>] [--exclude <glob>] [--ext <list>] [--jsx] [--graph] [--json] [--lang auto|en|fr] [--no-default-excludes]
51
51
  ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
52
52
  ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] # audit rendered-DOM captures alongside source
53
- ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--lang auto|en|fr]
53
+ ultra11y audit [--format sarif|github] # CI: code-scanning SARIF, or inline annotations + job summary
54
+ ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
54
55
  ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--standard <pack>] [--gh-issues | --gh-single] [--lang auto|en|fr]
55
- ultra11y render [<dir>] [--scaffold | --setup | --coverage | --storybook] [--captures <dir>] [--out <file>] [--json]
56
+ ultra11y render [<dir>] [--scaffold | --setup | --e2e | --coverage | --storybook] [--runner playwright|cypress] [--captures <dir>] [--out <file>] [--json]
57
+ ultra11y snapshot write [--root <dir>] [--fail-on blocking|major|minor] [--json] # payload on stdin → .ultra11y/pages/<id>/ + audit
58
+ ultra11y snapshot list [--root <dir>] [--json]
59
+ ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] # the per-page criterion grid
60
+ ultra11y dev [--port <n>] [--standard <pack>] | dev --next # live overlay while you build + dashboard
56
61
  ultra11y criteria [<sc>] [--list] [--standard <pack> [--theme <N>]] [--generate] [--json] [--lang auto|en|fr]
57
62
  ultra11y check --report <md> [--standard <pack>] [--quiet] [--json]
58
63
  ultra11y verify --report <md> [--standard <pack>] [--semantic] [--apply <verdicts.json>] [--max-verify <n>] [--json]
@@ -61,92 +66,10 @@ ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
61
66
  ultra11y pack check <pack.json> [--guidance <g.json>] | scaffold # gate an (AI-)authored standards pack
62
67
  ultra11y scan <url|file…> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--out <dir>] [--json]
63
68
  ultra11y scan --sitemap <url> | --crawl <url> [--depth <n>] [--max <n>] [--runtime …] [--merge <audit.json>] [--json]
64
- ultra11y mcp [--transport stdio|http] [--cwd <dir>] [--allow-write] [--port <n>] [--bind <addr>] [--allow-remote]
65
69
 
66
70
  # global: --pack <paths> (load external standards pack(s) at runtime) · --override
67
71
  ```
68
72
 
69
- ## Use it as an MCP server
70
-
71
- The skill shells out to the CLI and parses its output. An MCP server skips both:
72
- your agent calls ultra11y as typed tools, with JSON schemas in and structured
73
- results out. Same engine, same standards data, no wrapper.
74
-
75
- ```bash
76
- # stdio — the default, and what Claude Code / Claude Desktop / Cursor expect
77
- claude mcp add ultra11y -- node /abs/path/to/scripts/ultra11y.mjs mcp
78
-
79
- # or over HTTP, on loopback
80
- node scripts/ultra11y.mjs mcp --transport http --port 7341
81
- claude mcp add --transport http ultra11y http://127.0.0.1:7341/mcp
82
- ```
83
-
84
- ```jsonc
85
- // Claude Desktop takes stdio servers only — a remote URL here will not work.
86
- { "mcpServers": { "ultra11y": { "command": "node", "args": ["/abs/path/to/scripts/ultra11y.mjs", "mcp"] } } }
87
- // Cursor, HTTP:
88
- { "mcpServers": { "ultra11y": { "url": "http://127.0.0.1:7341/mcp" } } }
89
- ```
90
-
91
- It serves all three MCP primitives, because a skill is three things: the engine
92
- (**tools**), the method (**prompts**), and the documentation the method refers
93
- to (**resources**). A client given only the tools runs the audit, sees no
94
- errors, and reports the page as accessible — a false conformance claim, which is
95
- the one output an accessibility tool must never produce.
96
-
97
- ### Tools
98
-
99
- Ten read tools. `ultra11y_audit` is the entry point:
100
-
101
- | Tool | What it does |
102
- |------|--------------|
103
- | `ultra11y_audit` | The static pass — findings keyed by success criterion |
104
- | `ultra11y_adjudicate` | The criteria the engine **cannot** decide, with their evidence |
105
- | `ultra11y_criteria` | The offline standards reference — look a criterion up, don't recall it |
106
- | `ultra11y_report` | The dated conformance report, WCAG or a country pack |
107
- | `ultra11y_prd` | Non-conformities → remediation units with effort |
108
- | `ultra11y_check` | The gate: nothing asserted conformant that was never tested |
109
- | `ultra11y_verify` | Claim↔evidence worklist |
110
- | `ultra11y_pack_check` | Validate a country standards pack against what it really ships |
111
- | `ultra11y_sample_check` | Lint the normative page sample |
112
- | `ultra11y_read` | A file, or a line range, from the project |
113
-
114
- `--allow-write` additionally exposes `ultra11y_fix` (safe codemods, dry-run by
115
- default) and `ultra11y_init` (hook/CI/baseline) — the tools that change **your**
116
- project. `ultra11y_scan` is declared but declines over MCP: it drives a headless
117
- browser and may pull a Docker image, which is not a subprocess lifecycle a
118
- long-lived server should own. It tells you to run it from the CLI and re-audit.
119
-
120
- Pass `--cwd <dir>` at startup to dedicate the server to one project — `cwd` then
121
- becomes optional on every tool.
122
-
123
- ### Prompts — the workflow, not just the tools
124
-
125
- | Prompt | Arguments | What it drives |
126
- |--------|-----------|----------------|
127
- | `audit_wcag` | `cwd`, `globs?`, `standard?` | audit → adjudicate → rule on each → report → check, with the untested criteria named |
128
- | `adjudicate_criteria` | `cwd`, `standard?` | What alt-text relevance, link purpose, heading structure and reading order actually ask |
129
- | `review_diff_a11y` | `cwd`, `since?` | Audit exactly the diff, refute the false positives, state the residual risks |
130
-
131
- Each carries the coverage arithmetic: of the 55 WCAG 2.2 AA criteria the static
132
- engine decides a handful, a browser decides fourteen, and **thirty-eight are
133
- yours**.
134
-
135
- ### Resources — the skill's own documentation
136
-
137
- `SKILL.md` and all 28 `references/*.md` are served under `skill://`, read off
138
- disk at request time — so a documentation fix reaches every client without a
139
- rebuild.
140
-
141
- Two things worth knowing:
142
-
143
- - **A criterion nobody tested is untested, never conformant.** Every tool
144
- description and every prompt says so, because the failure mode here is not a
145
- wrong answer — it is a confident silence.
146
- - **The HTTP transport binds `127.0.0.1` and refuses anything else** unless you
147
- pass `--allow-remote`. This server reads local files; an exposed port is a
148
- read-anything primitive for whoever finds it.
149
-
150
73
  ## Standards packs (RGAA France first; add your country)
151
74
 
152
75
  WCAG 2.2 AA is the engine's canonical key. Each country standard ships as an in-repo
@@ -172,6 +95,65 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
172
95
  auto-codemods, fill-in `TODO` placeholders for the agent to complete, and judgment-only
173
96
  proposals. `--dry-run` is the default; `--write` applies but only after a re-audit proves
174
97
  no new non-conformity, and never on lossy JSX/TSX. See `references/fix.md`.
98
+ - **During your E2E run** — `render --e2e` writes Playwright/Cypress fixtures that audit a
99
+ targeted page **inside the test run you already have**, so the page is checked in the state
100
+ your test built (logged in, form filled, modal open) — state a separate `scan` run does not
101
+ have. Each checked page is persisted as a **snapshot** (`.ultra11y/pages/<id>/`: the whole
102
+ rendered document + computed styles + boxes), which is the durable half: the same page
103
+ re-audits **offline, with no browser**, so CI and the per-page report work from it. Because
104
+ a snapshot is a full document, the page-scoped rules finally run — RGAA **8.3** (`lang`),
105
+ **8.5/8.6** (`title`) — and every finding carries both its page and the source file that
106
+ rendered it. See `references/e2e.md` and `references/pages.md`.
107
+ - **While you build** — `dev --next` writes a one-line Next overlay component and `dev` starts
108
+ a loopback side-car: a floating panel lists the current page's non-conformities as you
109
+ browse, each linking to its `file:line` in your editor, and `http://127.0.0.1:4111` shows
110
+ the per-page grid accumulating. The overlay detaches itself before collecting — otherwise it
111
+ would audit itself and shift every element index — and renders nothing outside development.
112
+ See `references/devtools.md`.
113
+ - **A country standard is adjudicated at its OWN granularity** — 99 of RGAA's 106 criteria can
114
+ only ever derive `manual`, so ~93% of an RGAA audit is settled by the agent. `verify --manual
115
+ --standard rgaa` now keys the worklist by **RGAA criteria**, each carrying its numbered tests
116
+ in full, its technical note, its particular cases, its guidance and the glossary definitions
117
+ its tests cite (119 entries that previously had no reader). A `normativeRef` must cite one of
118
+ the item's **own** tests — the laxer check accepted a WCAG id that happened to collide with
119
+ an unrelated RGAA test number. Verdicts fold into a separate `packAdjudication` layer so a
120
+ pack decision never rewrites the WCAG core. See `references/judgment.md`.
121
+ - **Rendered tier (offline)** — six rules read a snapshot's browser-only signals inside the
122
+ ordinary `audit`: **no browser, no Docker, no running server**. `rendered-contrast` measures
123
+ contrast on the *computed* styles (the inline-literal rule could only see colours written in
124
+ the markup); `rendered-contrast-pixel` measures it **on the screenshot** for text over a
125
+ gradient or image, where `background-color` is transparent and every CSSOM-based checker —
126
+ axe-core included — is blind; `rendered-nontext-contrast` decides whether a form control's
127
+ boundary is perceivable (**RGAA 3.3**); and two more read the *stylesheet itself*, which is
128
+ the only place they exist: a focus indicator removed with nothing put back (**10.7**, until
129
+ now reachable only by a live browser probe) and an orientation lock (**13.9**).
130
+ `rendered-link-colour-only` makes **RGAA 10.6** decidable at all.
131
+ Each can say *"I don't know"*: an unresolvable backdrop, a varied region, a cross-origin
132
+ stylesheet, a `box-shadow` that might be the boundary — all leave the criterion undecided
133
+ rather than guessed, and without signals the rules do not fire, so no pre-existing verdict
134
+ changes. `tests/rgaa-coverage.test.ts` ratchets the result: **48 of 106** RGAA criteria now
135
+ map onto an engine rule, up from 43, and the number can only go up.
136
+ - **Page by page** — RGAA is a per-page norm; the engine's verdict is scope-wide. `pages`
137
+ bridges the two: one row per criterion, one column per page, embedded in `report` and
138
+ rebuilt from a committed `audit.json` alone. The per-page status is not recomputed — a
139
+ per-page *view* of the audit runs through the very same projection the report uses, so grid
140
+ and report agree by construction. Two rules hold the line: a finding is attributed to a page
141
+ only when something **says** so (else it is counted as unattributed, never spread across
142
+ pages), and a criterion is conforming *by silence* only on a page whose real rendered DOM
143
+ was audited. See `references/pages.md`.
144
+ - **A shipped GitHub Action** — `uses: maxgfr/ultra11y@main` audits the **code** (PR diff) and,
145
+ optionally, the **pages** (it can start your app, wait for it, then scan real URLs or your
146
+ declared sample). The engine ships inside the action, so there is nothing to install and no
147
+ `setup-node`. The gate runs **last**, after SARIF, annotations, the summary, the sticky PR
148
+ comment and the report — a red job is never a dead end. `ultra11y init --ci` writes a
149
+ workflow using it. See `references/ci.md`.
150
+ - **CI surfaces** — a red job says *that* the build broke, not *where*. `--format sarif` emits
151
+ SARIF 2.1.0 for GitHub code scanning, so each finding lands as an **inline annotation on the
152
+ causing line** of the PR; `--format github` does the same via `::error::` workflow commands
153
+ plus a `$GITHUB_STEP_SUMMARY` table, for plans without code scanning. Alert identity reuses
154
+ the baseline's `findingId`, so an alert survives line drift; a URL-keyed finding gets **no**
155
+ location rather than a guessed one. Run it from `report --standard rgaa` for pack-keyed
156
+ output. See `references/ci.md`.
175
157
  - **Automation** — `init --hook` (default) wires a zero-dependency git pre-commit gate over the
176
158
  **strict staged snapshot**: it audits the exact index blobs, auto-applies the safe fixes and
177
159
  re-stages them, and blocks only on judgment issues. `init --baseline`/`--ci` is the opt-in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "2.22.0",
3
+ "version": "2.23.0",
4
4
  "description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -56,10 +56,11 @@
56
56
  "@semantic-release/changelog": "^6.0.3",
57
57
  "@semantic-release/exec": "^7.1.0",
58
58
  "@semantic-release/git": "^10.0.1",
59
+ "@semantic-release/npm": "^13.1.5",
59
60
  "@types/node": "^26.0.1",
60
61
  "@vitest/coverage-v8": "^4.1.9",
61
62
  "jsdom": "^29.1.1",
62
- "semantic-release": "^25.0.5",
63
+ "semantic-release": "^25.0.8",
63
64
  "tsup": "^8.5.1",
64
65
  "typescript": "^6.0.3",
65
66
  "vitest": "^4.1.9",
@@ -69,5 +70,8 @@
69
70
  "onlyBuiltDependencies": [
70
71
  "esbuild"
71
72
  ]
73
+ },
74
+ "publishConfig": {
75
+ "access": "public"
72
76
  }
73
77
  }
@@ -58,7 +58,23 @@ const slug = (s) =>
58
58
 
59
59
  // [term](#anchor) -> term
60
60
  const plain = (s) => s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
61
- const toArr = (v) => (Array.isArray(v) ? v.map(String) : v == null ? [] : [String(v)]);
61
+ // DINUM nests a bullet list inside a note as `{ ul: [...] }` (17 places across
62
+ // technicalNote / particularCases). A plain `String(v)` on those produced a literal
63
+ // "[object Object]" in the shipped pack, silently deleting the normative sub-conditions an
64
+ // auditor needs — e.g. every exception under RGAA 1.2's images-of-text rule. Flatten the
65
+ // node instead of stringifying it, and refuse anything still unrecognised rather than
66
+ // emitting placeholder text (see the assertion at the end of this file).
67
+ const flattenNode = (v) => {
68
+ if (v == null) return [];
69
+ if (typeof v === "string") return [v];
70
+ if (Array.isArray(v)) return v.flatMap(flattenNode);
71
+ if (typeof v === "object") {
72
+ const list = v.ul ?? v.ol ?? v.li ?? v.p ?? v.text;
73
+ if (list !== undefined) return flattenNode(list);
74
+ }
75
+ return [];
76
+ };
77
+ const toArr = (v) => flattenNode(v);
62
78
 
63
79
  // crude HTML -> plaintext for glossary bodies
64
80
  const deHtml = (s) =>
@@ -94,6 +110,8 @@ const RULE_SC = {
94
110
  "aria-required-children": ["4.1.2"], "autoplay-media": ["1.4.2", "2.2.2"], "blink-marquee": ["2.2.2"],
95
111
  "button-empty-name": ["4.1.2"], "canvas-fallback-missing": ["1.1.1"], "chart-no-accessible-name": ["1.1.1"],
96
112
  "clickable-noninteractive": ["4.1.2", "2.1.1"], "contrast-literal": ["1.4.3"], "control-label-missing": ["4.1.2"],
113
+ "rendered-contrast": ["1.4.3"], "rendered-contrast-pixel": ["1.4.3"], "rendered-link-colour-only": ["1.4.1"],
114
+ "rendered-nontext-contrast": ["1.4.11"], "rendered-focus-not-visible": ["2.4.7"], "rendered-orientation-lock": ["1.3.4"],
97
115
  "css-generated-content-informative": ["1.3.1"], "date-fields-ungrouped": ["3.3.2"], "disabled-context-content": ["4.1.2"],
98
116
  "control-name-title-only": ["4.1.2"], "cross-icon-only-unnamed": ["4.1.2"],
99
117
  "cross-prop-drilled-name-lost": ["4.1.2"], "data-table-no-headers": ["1.3.1"],
@@ -125,6 +143,22 @@ const RULE_TO_CRITERIA = {
125
143
  "iframe-title-missing": ["2.1"], "axe:frame-title": ["2.1"], "axe:frame-title-unique": ["2.1"],
126
144
  // Theme 3 / 10.5 — colour contrast (3.2 text/bg, 10.5 CSS declarations)
127
145
  "contrast-literal": ["3.2", "10.5"], "axe:color-contrast": ["3.2", "10.5"], "axe:color-contrast-enhanced": ["3.2", "10.5"],
146
+ // Rendered tier: contrast measured on the real page (computed styles) and on the
147
+ // screenshot, for the gradient/background-image case the CSSOM cannot express.
148
+ "rendered-contrast": ["3.2", "10.5"], "rendered-contrast-pixel": ["3.2", "10.5"],
149
+ // 10.6 — a link in running text identified by colour alone. Deliberately NOT also
150
+ // mapped to 3.1 (the general "information by colour alone"): 10.6 is RGAA's specific
151
+ // criterion for links, and claiming both would count one defect twice.
152
+ "rendered-link-colour-only": ["10.6"],
153
+ // 3.3 — the boundary of a form control, measured on the computed borders/fill. The
154
+ // only WCAG SC RGAA 3.3 maps to (1.4.11) had no rule at all before.
155
+ "rendered-nontext-contrast": ["3.3"],
156
+ // 10.7 — a focus indicator removed by the stylesheet with nothing put back. The live
157
+ // `scan` probe (dyn-focus-visible) already evidenced this; the CSSOM route decides it
158
+ // OFFLINE, with no browser and no running app.
159
+ "rendered-focus-not-visible": ["10.7"],
160
+ // 13.9 — orientation lock. Its SC (1.3.4) had no rule.
161
+ "rendered-orientation-lock": ["13.9"],
128
162
  // Theme 4 — multimedia (4.3 captions)
129
163
  "media-no-track": ["4.3"], "axe:audio-caption": ["4.3"], "axe:video-caption": ["4.3"],
130
164
  // Theme 5 — tables (5.4 title, 5.6/5.7 headers, 5.8 layout-table markup)
@@ -267,6 +301,51 @@ async function main() {
267
301
  fr: "Indiquez le format et le poids du fichier dans l’intitulé du lien, par exemple « Rapport annuel (PDF, 2 Mo) ».",
268
302
  },
269
303
  },
304
+ // RGAA 11.8.2 — "Dans chaque balise <select>, chaque balise <optgroup> possède-t-elle un
305
+ // attribut label ?" Fully mechanical, and the criterion had no rule at all. Its siblings
306
+ // 11.8.1 ("si nécessaire") and 11.8.3 ("le label est-il pertinent ?") stay the agent's:
307
+ // this rule decides the one test of the three that is decidable from markup, which is
308
+ // exactly what the DSL is for. NORMATIVE — a missing `label` is a plain failure.
309
+ {
310
+ id: "optgroup-without-label",
311
+ criterion: "11.8",
312
+ wcag: ["1.3.1"],
313
+ severity: "majeur",
314
+ match: { tag: "optgroup", attrs: [{ name: "label", op: "absent" }] },
315
+ message: {
316
+ en: "<optgroup> without a label attribute — the group of options is unnamed (RGAA test 11.8.2).",
317
+ fr: "<optgroup> sans attribut label — le regroupement d’options n’est pas nommé (test RGAA 11.8.2).",
318
+ },
319
+ remediation: {
320
+ en: 'Add label="…" naming what the options in this group have in common, e.g. <optgroup label="Europe">.',
321
+ fr: 'Ajoutez label="…" nommant ce que les options du groupe ont en commun, par ex. <optgroup label="Europe">.',
322
+ },
323
+ },
324
+ // RGAA 8.10.2 — the FIRST of its two sub-conditions: "La valeur de l'attribut dir est
325
+ // conforme (rtl ou ltr)". Mechanical. The second ("la valeur est pertinente") and 8.10.1
326
+ // (is a reading-direction change signalled at all?) need language detection and stay
327
+ // manual. Anchored negative lookahead so only a genuinely invalid value matches; `auto`
328
+ // is valid HTML and is accepted.
329
+ {
330
+ id: "dir-value-invalid",
331
+ criterion: "8.10",
332
+ wcag: ["1.3.2"],
333
+ severity: "majeur",
334
+ match: {
335
+ attrs: [
336
+ { name: "dir", op: "present" },
337
+ { name: "dir", op: "matches", value: "^(?!(rtl|ltr|auto)$).+$" },
338
+ ],
339
+ },
340
+ message: {
341
+ en: "dir attribute with a value that is not rtl, ltr or auto — the reading direction change is not declared conformantly (RGAA test 8.10.2).",
342
+ fr: "Attribut dir dont la valeur n’est ni rtl, ni ltr, ni auto — le changement de sens de lecture n’est pas déclaré conformément (test RGAA 8.10.2).",
343
+ },
344
+ remediation: {
345
+ en: 'Use dir="rtl" or dir="ltr" (or dir="auto"), matching the reading direction of the text it carries.',
346
+ fr: 'Utilisez dir="rtl" ou dir="ltr" (ou dir="auto"), en accord avec le sens de lecture du texte porté.',
347
+ },
348
+ },
270
349
  ];
271
350
  // Wire each rule's namespaced id (`pack:rgaa:<id>`) into the criterion it reports under,
272
351
  // so derivePackResults routes its finding onto that criterion through the same
@@ -385,6 +464,21 @@ async function main() {
385
464
  writeFileSync(join(OUT, "rgaa.json"), packText);
386
465
  writeFileSync(join(OUT, "rgaa.glossary.json"), glossaryText);
387
466
 
467
+ // A normative sentence that stringified to "[object Object]" is a DELETED sub-condition an
468
+ // auditor would have had to apply. Fail the build rather than ship placeholder text.
469
+ const stringified = [];
470
+ for (const c of criteria) {
471
+ for (const field of ["technicalNote", "particularCases"]) {
472
+ for (const line of c[field] ?? []) if (String(line).includes("[object Object]")) stringified.push(`${c.id}.${field}`);
473
+ }
474
+ for (const [k, lines] of Object.entries(c.tests ?? {})) {
475
+ for (const line of lines) if (String(line).includes("[object Object]")) stringified.push(`${c.id} test ${k}`);
476
+ }
477
+ }
478
+ if (stringified.length) {
479
+ throw new Error(`build-pack-rgaa: ${stringified.length} normative text(s) stringified to "[object Object]" — extend flattenNode: ${stringified.join(", ")}`);
480
+ }
481
+
388
482
  const noWcag = criteria.filter((c) => c.wcag.length === 0).map((c) => c.id);
389
483
  console.log(`build-pack-rgaa: ${themes.length} themes, ${criteria.length} criteria → src/data/standards/rgaa.json`);
390
484
  console.log(`build-pack-rgaa: glossary ${Object.keys(glossary).length} entries → src/data/standards/rgaa.glossary.json`);
@@ -341,7 +341,10 @@ const RULE_SC_COVERAGE = {
341
341
  ],
342
342
  "1.3.5": ["field-purpose-incomplete", "autocomplete-token-invalid"],
343
343
  "1.4.2": ["autoplay-media"],
344
- "1.4.3": ["contrast-literal"],
344
+ "1.3.4": ["rendered-orientation-lock"],
345
+ "1.4.1": ["rendered-link-colour-only"],
346
+ "1.4.3": ["contrast-literal", "rendered-contrast", "rendered-contrast-pixel"],
347
+ "1.4.11": ["rendered-nontext-contrast"],
345
348
  "1.4.4": ["meta-viewport-zoom-block"],
346
349
  "2.1.1": ["clickable-noninteractive"],
347
350
  "2.2.1": ["meta-refresh-redirect"],
@@ -349,6 +352,7 @@ const RULE_SC_COVERAGE = {
349
352
  "2.4.1": ["skip-link-target-missing"],
350
353
  "2.4.2": ["title-missing-empty"],
351
354
  "2.4.3": ["positive-tabindex"],
355
+ "2.4.7": ["rendered-focus-not-visible"],
352
356
  "2.4.4": ["link-empty-name", "icon-only-control-unnamed"],
353
357
  "2.5.3": ["label-in-name-mismatch"],
354
358
  "3.2.2": ["on-input-context-change"],