ultra11y 5.35.3 → 5.36.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/README.md +5 -3
- package/package.json +1 -1
- package/scripts/build-pack-rgaa.mjs +230 -27
- package/scripts/ultra11y.mjs +2142 -92
- package/scripts/vendor/wcag-2.2-fr.json +1 -1
- package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
- package/scripts/vendor/wcag-2.2-techniques.json +1 -1
- package/scripts/vendor/wcag-2.2-text.json +1 -1
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +2142 -92
- package/skills/ultra11y/SKILL.md +6 -4
- package/skills/ultra11y/references/ci.md +14 -9
- package/skills/ultra11y/references/judgment.md +8 -4
- package/skills/ultra11y/references/mcp.md +3 -3
- package/skills/ultra11y/references/pages.md +4 -6
- package/skills/ultra11y/references/rgaa-automation.md +381 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +2142 -92
- package/src/adjudicate.ts +67 -8
- package/src/annotate.ts +38 -9
- package/src/data/standards/rgaa.json +1774 -16
- package/src/data/wcag-universe.json +1 -1
- package/src/html-report.ts +83 -3
- package/src/method-view.ts +7 -0
- package/src/prd.ts +2 -2
- package/src/report.ts +120 -10
- package/src/standards/coverage.ts +32 -1
- package/src/standards/derive.ts +24 -12
- package/src/standards/document.ts +2 -1
- package/src/standards/types.ts +16 -0
- package/src/standards/validate.ts +81 -0
- package/src/types.ts +15 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
|
|
13
|
-
"version": "5.
|
|
13
|
+
"version": "5.36.0",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.36.0",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.36.0",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "5.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.36.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ from auditing out of memory. `cwd` is optional on all five; it selects whose pac
|
|
|
227
227
|
`ultra11y_method` is the one to call first. It partitions the standard into evidence tiers —
|
|
228
228
|
`source`, `cross-file`, `rendered-page`, `browser`, `judgment`, `out-of-scope` — from the
|
|
229
229
|
engine's own per-criterion rule applicability, never from guessing at the wording of a test.
|
|
230
|
-
For RGAA it reports that **
|
|
230
|
+
For RGAA it reports that **56 of 106 criteria declare that no engine rule can evidence them**.
|
|
231
231
|
That is the standard saying they are yours, and a plan that hides it reads as coverage that
|
|
232
232
|
does not exist.
|
|
233
233
|
|
|
@@ -349,8 +349,10 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
|
|
|
349
349
|
the per-page grid accumulating. The overlay detaches itself before collecting — otherwise it
|
|
350
350
|
would audit itself and shift every element index — and renders nothing outside development.
|
|
351
351
|
See `references/devtools.md`.
|
|
352
|
-
- **A country standard is adjudicated at its OWN granularity** —
|
|
353
|
-
|
|
352
|
+
- **A country standard is adjudicated at its OWN granularity** — 97 of RGAA's 106 criteria
|
|
353
|
+
carry at least one judgment test and can still need adjudication to earn C; decisive rules
|
|
354
|
+
can nevertheless prove their bounded failures.
|
|
355
|
+
`verify --manual
|
|
354
356
|
--standard rgaa` now keys the worklist by **RGAA criteria**, each carrying its numbered tests
|
|
355
357
|
in full, its technical note, its particular cases, its guidance and the glossary definitions
|
|
356
358
|
its tests cite (119 entries that previously had no reader). A `normativeRef` must cite one of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.36.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",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// DEV-ONLY (not in `bin`). Builds the RGAA 4.1.2 STANDARDS PACK that ships at
|
|
3
3
|
// src/data/standards/rgaa.json (+ rgaa.glossary.json). RGAA is the first of many
|
|
4
|
-
// pluggable, in-repo country packs (see CONTRIBUTING.md): a pack
|
|
5
|
-
//
|
|
6
|
-
// of its criteria onto WCAG success criteria (bare SC ids). The WCAG↔rule coverage
|
|
4
|
+
// pluggable, in-repo country packs (see CONTRIBUTING.md): a pack carries a test-level
|
|
5
|
+
// automation contract, not executable engine code — it is a localized criterion set that
|
|
6
|
+
// maps each of its criteria onto WCAG success criteria (bare SC ids). The WCAG↔rule coverage
|
|
7
7
|
// lives in the core (scripts/build-standards.mjs). The RGAA content is Licence
|
|
8
8
|
// Ouverte / Etalab 2.0 — see NOTICE. The official source is vendored under
|
|
9
9
|
// scripts/vendor/rgaa/ (criteres.json, glossaire.json, methodologies.json) so the
|
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from "node:url";
|
|
|
20
20
|
|
|
21
21
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
22
|
const OUT = join(root, "src", "data", "standards");
|
|
23
|
+
const AUTOMATION_REF = join(root, "skills", "ultra11y", "references", "rgaa-automation.md");
|
|
23
24
|
const VENDOR = join(root, "scripts", "vendor", "rgaa");
|
|
24
25
|
const BIOME = join(root, "node_modules", ".bin", "biome");
|
|
25
26
|
const BASE = "https://raw.githubusercontent.com/DISIC/accessibilite.numerique.gouv.fr/main/RGAA";
|
|
@@ -181,11 +182,12 @@ const RULE_TO_CRITERIA = {
|
|
|
181
182
|
"axe:object-alt": ["1.1"], "decorative-alt-misuse": ["1.2"], "axe:image-redundant-alt": ["1.2"],
|
|
182
183
|
// Theme 2 — frames (2.1 frame title)
|
|
183
184
|
"iframe-title-missing": ["2.1"], "axe:frame-title": ["2.1"], "axe:frame-title-unique": ["2.1"],
|
|
184
|
-
// Theme 3
|
|
185
|
-
|
|
185
|
+
// Theme 3 — colour contrast. RGAA 10.5 asks about paired CSS declarations, not the
|
|
186
|
+
// resulting ratio, so contrast findings must not be presented as evidence for 10.5.
|
|
187
|
+
"contrast-literal": ["3.2"], "axe:color-contrast": ["3.2"], "axe:color-contrast-enhanced": ["3.2"],
|
|
186
188
|
// Rendered tier: contrast measured on the real page (computed styles) and on the
|
|
187
189
|
// screenshot, for the gradient/background-image case the CSSOM cannot express.
|
|
188
|
-
"rendered-contrast": ["3.2"
|
|
190
|
+
"rendered-contrast": ["3.2"], "rendered-contrast-pixel": ["3.2"],
|
|
189
191
|
// 10.6 — a link in running text identified by colour alone. Deliberately NOT also
|
|
190
192
|
// mapped to 3.1 (the general "information by colour alone"): 10.6 is RGAA's specific
|
|
191
193
|
// criterion for links, and claiming both would count one defect twice.
|
|
@@ -202,10 +204,9 @@ const RULE_TO_CRITERIA = {
|
|
|
202
204
|
// Theme 4 — multimedia (4.3 captions)
|
|
203
205
|
"media-no-track": ["4.3"], "axe:audio-caption": ["4.3"], "axe:video-caption": ["4.3"],
|
|
204
206
|
// Theme 5 — tables (5.4 title, 5.6/5.7 headers, 5.8 layout-table markup)
|
|
205
|
-
"table-caption-missing": ["5.4"], "data-table-no-headers": ["5.6", "5.7"],
|
|
206
|
-
"table-empty-data-cell": ["5.7"],
|
|
207
|
+
"table-caption-missing": ["5.4"], "data-table-no-headers": ["5.6", "5.7"],
|
|
207
208
|
"layout-table-data-markup": ["5.8"], "axe:td-headers-attr": ["5.7"], "axe:th-has-data-cells": ["5.6"],
|
|
208
|
-
"axe:scope-attr-valid": ["5.7"], "axe:td-has-header": ["5.6"], "axe:
|
|
209
|
+
"axe:scope-attr-valid": ["5.7"], "axe:td-has-header": ["5.6"], "axe:table-fake-caption": ["5.8"],
|
|
209
210
|
// Theme 6 — links (6.2 link label)
|
|
210
211
|
"link-empty-name": ["6.2"], "icon-only-control-unnamed": ["6.2", "11.9"], "cross-icon-only-unnamed": ["11.9"],
|
|
211
212
|
"axe:link-name": ["6.2"],
|
|
@@ -227,8 +228,8 @@ const RULE_TO_CRITERIA = {
|
|
|
227
228
|
"axe:aria-hidden-focus": ["7.1"], "axe:presentation-role-conflict": ["7.1"],
|
|
228
229
|
// Theme 8 — document (8.2 valid code, 8.3 default lang, 8.4 lang relevant, 8.5 title, 8.7/8.8 lang changes)
|
|
229
230
|
"duplicate-id": ["8.2"], "axe:duplicate-id": ["8.2"], "axe:duplicate-id-aria": ["8.2"], "axe:duplicate-id-active": ["8.2"],
|
|
230
|
-
"html-lang-missing": ["8.3"], "axe:html-has-lang": ["8.3"],
|
|
231
|
-
"lang-invalid": ["8.4", "8.8"], "axe:html-lang-valid": ["8.4"], "axe:valid-lang": ["8.8"],
|
|
231
|
+
"html-lang-missing": ["8.3"], "axe:html-has-lang": ["8.3"],
|
|
232
|
+
"lang-invalid": ["8.4", "8.8"], "axe:html-lang-valid": ["8.4"], "axe:html-xml-lang-mismatch": ["8.4"], "axe:valid-lang": ["8.8"],
|
|
232
233
|
"title-missing-empty": ["8.5"], "axe:document-title": ["8.5"], "inline-lang-change-missing": ["8.7"],
|
|
233
234
|
// Theme 9 — structure (9.1 headings, 9.2 doc structure, 9.3 lists)
|
|
234
235
|
"h1-missing": ["9.1"], "h1-multiple": ["9.1"], "heading-order-skip": ["9.1"], "empty-heading": ["9.1"],
|
|
@@ -262,10 +263,110 @@ const RULE_TO_CRITERIA = {
|
|
|
262
263
|
// has none for keyboard traps, which is why the probe was written in the first place.
|
|
263
264
|
"dyn-keyboard-trap": ["12.9"],
|
|
264
265
|
// Theme 13 — consultation (13.1 time limits, 13.8 moving/blinking)
|
|
265
|
-
"meta-refresh-redirect": ["13.1"], "blink-marquee": ["13.8"], "autoplay-media": ["4.10"
|
|
266
|
+
"meta-refresh-redirect": ["13.1"], "blink-marquee": ["13.8"], "autoplay-media": ["4.10"],
|
|
266
267
|
"axe:no-autoplay-audio": ["4.10"], "axe:blink": ["13.8"], "axe:marquee": ["13.8"],
|
|
267
268
|
};
|
|
268
269
|
|
|
270
|
+
// A rule firing is NOT automatically a complete RGAA failure. Most engine rules are useful
|
|
271
|
+
// evidence for a narrower precondition while the official test still asks about relevance,
|
|
272
|
+
// an allowed alternative, or a particular case. Those rules are routed to adjudication as
|
|
273
|
+
// `candidate` below. This deliberately small table contains only failures whose observation
|
|
274
|
+
// itself exhausts the cited numbered test.
|
|
275
|
+
const DECISIVE_RULE_TESTS = {
|
|
276
|
+
"1.1|axe:area-alt": ["2"],
|
|
277
|
+
"1.1|axe:input-image-alt": ["3"],
|
|
278
|
+
"1.1|input-image-alt-missing": ["3"],
|
|
279
|
+
"2.1|axe:frame-title": ["1"],
|
|
280
|
+
"2.1|iframe-title-missing": ["1"],
|
|
281
|
+
"6.2|link-empty-name": ["1"],
|
|
282
|
+
"6.2|axe:link-name": ["1"],
|
|
283
|
+
"8.2|duplicate-id": ["1"],
|
|
284
|
+
"8.2|axe:duplicate-id": ["1"],
|
|
285
|
+
"8.2|axe:duplicate-id-aria": ["1"],
|
|
286
|
+
"8.2|axe:duplicate-id-active": ["1"],
|
|
287
|
+
"8.4|axe:html-lang-valid": ["1"],
|
|
288
|
+
"8.4|lang-invalid": ["1"],
|
|
289
|
+
"8.5|title-missing-empty": ["1"],
|
|
290
|
+
"8.5|axe:document-title": ["1"],
|
|
291
|
+
"9.3|list-structure": ["1", "2", "3"],
|
|
292
|
+
"9.3|axe:list": ["1", "2"],
|
|
293
|
+
"9.3|axe:listitem": ["1", "2"],
|
|
294
|
+
"9.3|axe:definition-list": ["3"],
|
|
295
|
+
"9.3|axe:dlitem": ["3"],
|
|
296
|
+
"10.7|dyn-focus-visible": ["1"],
|
|
297
|
+
"11.1|axe:label": ["1"],
|
|
298
|
+
"11.1|axe:select-name": ["1"],
|
|
299
|
+
"11.1|control-label-missing": ["1"],
|
|
300
|
+
"11.6|axe:fieldset": ["1"],
|
|
301
|
+
"11.6|fieldset-legend-missing": ["1"],
|
|
302
|
+
"11.9|axe:button-name": ["1"],
|
|
303
|
+
"11.9|axe:input-button-name": ["1"],
|
|
304
|
+
"11.9|button-empty-name": ["1"],
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Candidate evidence is test-scoped too. The fallback remains all tests only when one rule id
|
|
308
|
+
// genuinely cannot distinguish the mechanism it observed; the entries below prevent a signal
|
|
309
|
+
// for one numbered test from being presented to the adjudicator as evidence for its siblings.
|
|
310
|
+
const CANDIDATE_RULE_TESTS = {
|
|
311
|
+
"1.1|axe:image-alt": ["1"],
|
|
312
|
+
"1.1|axe:role-img-alt": ["1"],
|
|
313
|
+
"1.1|img-alt-missing": ["1"],
|
|
314
|
+
"1.1|axe:svg-img-alt": ["5"],
|
|
315
|
+
"1.1|axe:object-alt": ["6"],
|
|
316
|
+
"1.1|object-embed-no-name": ["6", "7"],
|
|
317
|
+
"1.1|canvas-fallback-missing": ["8"],
|
|
318
|
+
"1.1|chart-no-accessible-name": ["1", "5", "8"],
|
|
319
|
+
"4.3|axe:audio-caption": ["1"],
|
|
320
|
+
"4.3|axe:video-caption": ["1"],
|
|
321
|
+
"4.3|media-no-track": ["1"],
|
|
322
|
+
"5.6|axe:td-has-header": ["1", "2"],
|
|
323
|
+
"5.6|axe:th-has-data-cells": ["1", "2", "3"],
|
|
324
|
+
"5.6|data-table-no-headers": ["1", "2", "3"],
|
|
325
|
+
"5.7|axe:scope-attr-valid": ["2", "3"],
|
|
326
|
+
"5.7|axe:td-headers-attr": ["4"],
|
|
327
|
+
"7.3|clickable-noninteractive": ["1"],
|
|
328
|
+
"8.4|axe:html-xml-lang-mismatch": ["1"],
|
|
329
|
+
"9.1|axe:empty-heading": ["2"],
|
|
330
|
+
"9.1|empty-heading": ["2"],
|
|
331
|
+
"9.1|axe:heading-order": ["1"],
|
|
332
|
+
"9.1|heading-order-skip": ["1"],
|
|
333
|
+
"9.1|axe:page-has-heading-one": ["1", "3"],
|
|
334
|
+
"9.1|h1-missing": ["1", "3"],
|
|
335
|
+
"9.1|h1-multiple": ["1"],
|
|
336
|
+
"10.4|axe:meta-viewport": ["2"],
|
|
337
|
+
"10.4|axe:meta-viewport-large": ["2"],
|
|
338
|
+
"10.4|meta-viewport-zoom-block": ["2"],
|
|
339
|
+
"10.4|dyn-input-overflow-zoom": ["1"],
|
|
340
|
+
"10.4|dyn-reflow-zoom": ["1"],
|
|
341
|
+
"10.11|dyn-input-overflow-reflow": ["1"],
|
|
342
|
+
"10.11|dyn-reflow": ["1"],
|
|
343
|
+
"11.1|axe:form-field-multiple-labels": ["2"],
|
|
344
|
+
"11.1|form-field-multiple-labels": ["2"],
|
|
345
|
+
"11.1|label-for-dangling": ["2"],
|
|
346
|
+
"11.1|axe:label-title-only": ["1", "3"],
|
|
347
|
+
"11.1|control-name-title-only": ["1", "3"],
|
|
348
|
+
"11.1|placeholder-as-label": ["1", "3"],
|
|
349
|
+
"11.1|field-purpose-incomplete": ["1", "3"],
|
|
350
|
+
"11.1|select-has-option": ["1"],
|
|
351
|
+
"11.9|cross-icon-only-unnamed": ["1"],
|
|
352
|
+
"11.9|icon-only-control-unnamed": ["1"],
|
|
353
|
+
"11.10|aria-invalid-no-description": ["3", "4", "6", "7"],
|
|
354
|
+
"11.10|error-not-associated": ["3", "4", "6", "7"],
|
|
355
|
+
"12.7|axe:bypass": ["1"],
|
|
356
|
+
"12.7|axe:skip-link": ["2"],
|
|
357
|
+
"12.7|skip-link-target-missing": ["2"],
|
|
358
|
+
"12.8|axe:tabindex": ["1"],
|
|
359
|
+
"12.8|positive-tabindex": ["1"],
|
|
360
|
+
"13.1|meta-refresh-redirect": ["2"],
|
|
361
|
+
"13.8|axe:blink": ["2"],
|
|
362
|
+
"13.8|axe:marquee": ["1"],
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const COMPLETE_BY_SILENCE = new Set(["8.5", "10.1"]);
|
|
366
|
+
const renderedTier = (ruleId) =>
|
|
367
|
+
ruleId.startsWith("rendered-") || ruleId.startsWith("dyn-") || ruleId.startsWith("axe:") ? "rendered" : "static";
|
|
368
|
+
const mergeTestTier = (current, next) => (current === "static" || next === "static" ? "static" : next);
|
|
369
|
+
|
|
269
370
|
async function main() {
|
|
270
371
|
const criteres = await source("criteres.json");
|
|
271
372
|
const glossaire = await source("glossaire.json");
|
|
@@ -389,8 +490,8 @@ const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|ep
|
|
|
389
490
|
// RGAA 8.10.2 — the FIRST of its two sub-conditions: "La valeur de l'attribut dir est
|
|
390
491
|
// conforme (rtl ou ltr)". Mechanical. The second ("la valeur est pertinente") and 8.10.1
|
|
391
492
|
// (is a reading-direction change signalled at all?) need language detection and stay
|
|
392
|
-
// manual.
|
|
393
|
-
//
|
|
493
|
+
// manual. RGAA 4.1.2 closes the accepted set to `rtl` and `ltr`: HTML's `auto` value is
|
|
494
|
+
// valid markup but does not satisfy this test, so it is intentionally reported here.
|
|
394
495
|
{
|
|
395
496
|
id: "dir-value-invalid",
|
|
396
497
|
criterion: "8.10",
|
|
@@ -399,16 +500,16 @@ const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|ep
|
|
|
399
500
|
match: {
|
|
400
501
|
attrs: [
|
|
401
502
|
{ name: "dir", op: "present" },
|
|
402
|
-
{ name: "dir", op: "matches", value: "^(?!(rtl|ltr
|
|
503
|
+
{ name: "dir", op: "matches", value: "^(?!(rtl|ltr)$).+$" },
|
|
403
504
|
],
|
|
404
505
|
},
|
|
405
506
|
message: {
|
|
406
|
-
en: "dir attribute with a value
|
|
407
|
-
fr: "Attribut dir dont la valeur n’est ni rtl
|
|
507
|
+
en: "dir attribute with a value other than rtl or ltr — the reading direction change fails RGAA test 8.10.2.",
|
|
508
|
+
fr: "Attribut dir dont la valeur n’est ni rtl ni ltr — le changement de sens de lecture échoue au test RGAA 8.10.2.",
|
|
408
509
|
},
|
|
409
510
|
remediation: {
|
|
410
|
-
en: 'Use dir="rtl" or dir="ltr"
|
|
411
|
-
fr: 'Utilisez dir="rtl" ou dir="ltr"
|
|
511
|
+
en: 'Use dir="rtl" or dir="ltr", matching the reading direction of the text it carries.',
|
|
512
|
+
fr: 'Utilisez dir="rtl" ou dir="ltr", en accord avec le sens de lecture du texte porté.',
|
|
412
513
|
},
|
|
413
514
|
},
|
|
414
515
|
// RGAA 8.1 — « chaque page web est-elle définie par un type de document ? » THE criterion
|
|
@@ -421,14 +522,11 @@ const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|ep
|
|
|
421
522
|
// doctype is not part of `documentElement.outerHTML`. The collector records it beside the
|
|
422
523
|
// DOM (SnapshotMeta.doctype), and this DOCUMENT-level rule reads it there.
|
|
423
524
|
//
|
|
424
|
-
// WHAT IT DECIDES, AND WHAT IT DOES NOT.
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
// here as the recorded empty string, i.e. as the absence this rule reports. Inventing a
|
|
430
|
-
// separate "invalid" verdict would mean guessing at legacy but legitimate declarations,
|
|
431
|
-
// and manufacturing a non-conformity is the one thing this tier must not do.
|
|
525
|
+
// WHAT IT DECIDES, AND WHAT IT DOES NOT. The signal exhausts test 8.1.1 only: no parsed
|
|
526
|
+
// doctype means the required declaration is absent. It does not retain enough source
|
|
527
|
+
// syntax to validate every accepted declaration (8.1.2) or independently prove its source
|
|
528
|
+
// position (8.1.3). Those two tests remain with the adjudicator, and 8.1 cannot earn C from
|
|
529
|
+
// this rule's silence. Inventing either conclusion would manufacture conformity.
|
|
432
530
|
//
|
|
433
531
|
// NORMATIVE, and signal-gated: it fires only where a capture recorded the field, so a
|
|
434
532
|
// source file and a pre-field capture are both silence rather than a failure.
|
|
@@ -479,6 +577,66 @@ const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|ep
|
|
|
479
577
|
presentational.appliesTo = { ruleIds: [...new Set([...presentational.appliesTo.ruleIds, ...PRESENTATIONAL_RULE_IDS])].sort() };
|
|
480
578
|
const overrides = Object.fromEntries(PRESENTATIONAL_RULE_IDS.map((id) => [id, { advisory: false, severity: "majeur" }]));
|
|
481
579
|
|
|
580
|
+
// ---- Test-level automation matrix -------------------------------------------------
|
|
581
|
+
// Start fail-closed: every one of DINUM's 258 tests is a judgment until a rule is
|
|
582
|
+
// explicitly tied to it below. Every applicable rule is also classified. Unlisted rules
|
|
583
|
+
// become candidates, never normative failures by accident.
|
|
584
|
+
const packRuleById = new Map(rules.map((rule) => [`pack:rgaa:${rule.id}`, rule]));
|
|
585
|
+
const packDecisive = {
|
|
586
|
+
"8.1|pack:rgaa:doctype-missing": ["1"],
|
|
587
|
+
"8.10|pack:rgaa:dir-value-invalid": ["2"],
|
|
588
|
+
"11.8|pack:rgaa:optgroup-without-label": ["2"],
|
|
589
|
+
"10.1|presentational-element": ["1"],
|
|
590
|
+
"10.1|presentational-attribute": ["2"],
|
|
591
|
+
"10.1|presentational-spacing": ["3"],
|
|
592
|
+
};
|
|
593
|
+
for (const [key, tests] of Object.entries(CANDIDATE_RULE_TESTS)) {
|
|
594
|
+
const split = key.indexOf("|");
|
|
595
|
+
const criterionId = key.slice(0, split);
|
|
596
|
+
const ruleId = key.slice(split + 1);
|
|
597
|
+
const criterion = criteria.find((entry) => entry.id === criterionId);
|
|
598
|
+
if (!criterion) throw new Error(`CANDIDATE_RULE_TESTS: unknown criterion "${criterionId}"`);
|
|
599
|
+
if (!criterion.appliesTo.ruleIds.includes(ruleId))
|
|
600
|
+
throw new Error(`CANDIDATE_RULE_TESTS: "${ruleId}" is not applicable to RGAA ${criterionId}`);
|
|
601
|
+
for (const test of tests) if (!(test in criterion.tests)) throw new Error(`CANDIDATE_RULE_TESTS: unknown test ${criterionId}.${test}`);
|
|
602
|
+
}
|
|
603
|
+
for (const c of criteria) {
|
|
604
|
+
const testKeys = Object.keys(c.tests ?? {});
|
|
605
|
+
const testTiers = Object.fromEntries(testKeys.map((key) => [key, "judgment"]));
|
|
606
|
+
const ruleContracts = c.appliesTo.ruleIds.map((id) => {
|
|
607
|
+
const decisiveTests = DECISIVE_RULE_TESTS[`${c.id}|${id}`] ?? packDecisive[`${c.id}|${id}`];
|
|
608
|
+
const advisory = packRuleById.get(id)?.advisory === true;
|
|
609
|
+
// Fail closed: a rule is decisive only when the exact RGAA numbered test is curated
|
|
610
|
+
// above. The old fallback made every unlisted rule decisive and then spread it over
|
|
611
|
+
// every test of the criterion — precisely the false-NC path this contract exists to
|
|
612
|
+
// prevent.
|
|
613
|
+
const effect = advisory ? "advisory" : decisiveTests ? "decisive-nc" : "candidate";
|
|
614
|
+
const touched = decisiveTests ?? CANDIDATE_RULE_TESTS[`${c.id}|${id}`] ?? testKeys;
|
|
615
|
+
if (effect === "decisive-nc") {
|
|
616
|
+
const tier = id === "pack:rgaa:doctype-missing" ? "rendered" : renderedTier(id);
|
|
617
|
+
for (const test of touched) testTiers[test] = mergeTestTier(testTiers[test], tier);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
id,
|
|
621
|
+
tests: touched,
|
|
622
|
+
effect,
|
|
623
|
+
rationale:
|
|
624
|
+
effect === "decisive-nc"
|
|
625
|
+
? "The observed failure exhausts the cited RGAA test."
|
|
626
|
+
: effect === "candidate"
|
|
627
|
+
? "Useful engine evidence, but the RGAA test still has an alternative, applicability condition, relevance judgment, or particular case to adjudicate."
|
|
628
|
+
: "Non-normative recommendation; it cannot affect the criterion verdict.",
|
|
629
|
+
};
|
|
630
|
+
});
|
|
631
|
+
c.automation = {
|
|
632
|
+
tests: testTiers,
|
|
633
|
+
rules: ruleContracts,
|
|
634
|
+
...(COMPLETE_BY_SILENCE.has(c.id) ? { completeBySilence: true } : {}),
|
|
635
|
+
};
|
|
636
|
+
// The matrix supersedes the five historical exceptions: any criterion not explicitly
|
|
637
|
+
// complete by silence must not inherit C from a broader WCAG projection.
|
|
638
|
+
}
|
|
639
|
+
|
|
482
640
|
const pack = {
|
|
483
641
|
key: "rgaa",
|
|
484
642
|
name: "RGAA",
|
|
@@ -571,6 +729,48 @@ const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|ep
|
|
|
571
729
|
|
|
572
730
|
const packText = biomeFormat(JSON.stringify(pack, null, 2) + "\n", "src/data/standards/rgaa.json");
|
|
573
731
|
const glossaryText = biomeFormat(JSON.stringify(glossary, null, 2) + "\n", "src/data/standards/rgaa.glossary.json");
|
|
732
|
+
const automationLines = [
|
|
733
|
+
"<!-- GENERATED by `pnpm run build:pack:rgaa` from the vendored RGAA 4.1.2 data and the curated rule contracts. -->",
|
|
734
|
+
"",
|
|
735
|
+
"# RGAA 4.1.2 automation matrix",
|
|
736
|
+
"",
|
|
737
|
+
"Every one of the 258 official tests is classified. `static` and `rendered` mean that an explicitly mapped decisive rule can prove a failure; `judgment` remains for adjudication. A candidate signal is evidence only and never changes the verdict by itself. Conformity by silence is allowed only where explicitly shown.",
|
|
738
|
+
"",
|
|
739
|
+
"## Summary by criterion",
|
|
740
|
+
"",
|
|
741
|
+
"The 106 criterion rows below are the one-by-one routing review. “AI + signals” means deterministic evidence is forwarded without changing the verdict; “deterministic NC + AI residual” means a precise failure can be decided mechanically while every remaining condition still goes to adjudication.",
|
|
742
|
+
"",
|
|
743
|
+
"| Criterion | Tests (static / rendered / judgment) | Rules (decisive / candidate / advisory) | C by silence | Reviewed routing |",
|
|
744
|
+
"|---|---:|---:|:---:|---|",
|
|
745
|
+
];
|
|
746
|
+
for (const c of criteria) {
|
|
747
|
+
const tiers = Object.values(c.automation.tests);
|
|
748
|
+
const effects = c.automation.rules.map((rule) => rule.effect);
|
|
749
|
+
const count = (values, value) => values.filter((entry) => entry === value).length;
|
|
750
|
+
automationLines.push(
|
|
751
|
+
`| ${c.id} | ${count(tiers, "static")} / ${count(tiers, "rendered")} / ${count(tiers, "judgment")} | ${count(effects, "decisive-nc")} / ${count(effects, "candidate")} / ${count(effects, "advisory")} | ${c.automation.completeBySilence ? "yes" : "no"} | ${
|
|
752
|
+
c.automation.completeBySilence
|
|
753
|
+
? "deterministic"
|
|
754
|
+
: effects.includes("decisive-nc")
|
|
755
|
+
? "deterministic NC + AI residual"
|
|
756
|
+
: effects.includes("candidate")
|
|
757
|
+
? "AI + signals"
|
|
758
|
+
: "AI"
|
|
759
|
+
} |`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
automationLines.push("", "## Test-by-test contract", "");
|
|
763
|
+
automationLines.push("| Test | Owner | Decisive rules | Candidate signals |", "|---|---|---|---|");
|
|
764
|
+
for (const c of criteria) {
|
|
765
|
+
for (const [test, tier] of Object.entries(c.automation.tests)) {
|
|
766
|
+
const contracts = c.automation.rules.filter((rule) => rule.tests.includes(test));
|
|
767
|
+
const decisive = contracts.filter((rule) => rule.effect === "decisive-nc").map((rule) => `\`${rule.id}\``);
|
|
768
|
+
const candidates = contracts.filter((rule) => rule.effect === "candidate").map((rule) => `\`${rule.id}\``);
|
|
769
|
+
automationLines.push(`| ${c.id}.${test} | ${tier} | ${decisive.join(", ") || "—"} | ${candidates.join(", ") || "—"} |`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
automationLines.push("");
|
|
773
|
+
const automationText = automationLines.join("\n");
|
|
574
774
|
|
|
575
775
|
if (doCheck) {
|
|
576
776
|
const packPath = join(OUT, "rgaa.json");
|
|
@@ -579,6 +779,8 @@ const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|ep
|
|
|
579
779
|
if (!existsSync(packPath) || readFileSync(packPath, "utf8") !== packText) drift.push("src/data/standards/rgaa.json");
|
|
580
780
|
if (!existsSync(glossaryPath) || readFileSync(glossaryPath, "utf8") !== glossaryText)
|
|
581
781
|
drift.push("src/data/standards/rgaa.glossary.json");
|
|
782
|
+
if (!existsSync(AUTOMATION_REF) || readFileSync(AUTOMATION_REF, "utf8") !== automationText)
|
|
783
|
+
drift.push("skills/ultra11y/references/rgaa-automation.md");
|
|
582
784
|
if (drift.length > 0) {
|
|
583
785
|
console.error(`build-pack-rgaa --check: OUT OF DATE vs vendored source — re-run \`pnpm run build:pack:rgaa\`: ${drift.join(", ")}`);
|
|
584
786
|
process.exit(1);
|
|
@@ -590,6 +792,7 @@ const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|ep
|
|
|
590
792
|
mkdirSync(OUT, { recursive: true });
|
|
591
793
|
writeFileSync(join(OUT, "rgaa.json"), packText);
|
|
592
794
|
writeFileSync(join(OUT, "rgaa.glossary.json"), glossaryText);
|
|
795
|
+
writeFileSync(AUTOMATION_REF, automationText);
|
|
593
796
|
|
|
594
797
|
// A normative sentence that stringified to "[object Object]" is a DELETED sub-condition an
|
|
595
798
|
// auditor would have had to apply. Fail the build rather than ship placeholder text.
|