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.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/docker/Dockerfile +7 -0
- package/docker/package.json +9 -0
- package/docker/runner.mjs +23 -0
- package/package.json +73 -0
- package/scripts/bench.mjs +151 -0
- package/scripts/build-act-corpus.mjs +143 -0
- package/scripts/build-adjudication.mjs +503 -0
- package/scripts/build-pack-rgaa.mjs +397 -0
- package/scripts/build-standards.mjs +548 -0
- package/scripts/copy-bundle.mjs +39 -0
- package/scripts/import-pack.mjs +107 -0
- package/scripts/sync-engine.mjs +84 -0
- package/scripts/sync-version.mjs +43 -0
- package/scripts/ultra11y.mjs +52739 -0
- package/scripts/vendor/act-testcases.json +12297 -0
- package/scripts/vendor/rgaa/SOURCE.json +8 -0
- package/scripts/vendor/rgaa/criteres.json +4144 -0
- package/scripts/vendor/rgaa/glossaire.json +480 -0
- package/scripts/vendor/wcag-2.2-fr.json +82 -0
- package/scripts/vendor/wcag-2.2-sc-universe.json +950 -0
- package/scripts/vendor/wcag-2.2-sc.json +574 -0
- package/scripts/verify-rgaa-source.mjs +126 -0
- package/scripts/verify-skill-bundle.mjs +91 -0
- package/skills/review-a11y/SKILL.md +123 -0
- package/skills/review-a11y/scripts/ultra11y.mjs +52739 -0
- package/skills/ultra11y/SKILL.md +292 -0
- package/skills/ultra11y/references/act.md +163 -0
- package/skills/ultra11y/references/adjudication.md +599 -0
- package/skills/ultra11y/references/audit.md +100 -0
- package/skills/ultra11y/references/authoring.md +37 -0
- package/skills/ultra11y/references/automation.md +113 -0
- package/skills/ultra11y/references/correction.md +41 -0
- package/skills/ultra11y/references/criteria.md +129 -0
- package/skills/ultra11y/references/cross-file.md +82 -0
- package/skills/ultra11y/references/dynamic.md +152 -0
- package/skills/ultra11y/references/false-positives.md +81 -0
- package/skills/ultra11y/references/fix.md +46 -0
- package/skills/ultra11y/references/focus-and-logic.md +64 -0
- package/skills/ultra11y/references/forbidden-patterns.md +61 -0
- package/skills/ultra11y/references/forms-and-errors.md +147 -0
- package/skills/ultra11y/references/guidance.md +55 -0
- package/skills/ultra11y/references/judgment.md +90 -0
- package/skills/ultra11y/references/media-and-motion.md +116 -0
- package/skills/ultra11y/references/methodology.md +72 -0
- package/skills/ultra11y/references/naming.md +117 -0
- package/skills/ultra11y/references/orchestration.md +44 -0
- package/skills/ultra11y/references/packs.md +193 -0
- package/skills/ultra11y/references/prd.md +104 -0
- package/skills/ultra11y/references/rendered.md +110 -0
- package/skills/ultra11y/references/scale.md +49 -0
- package/skills/ultra11y/references/standards.md +97 -0
- package/skills/ultra11y/references/structure.md +138 -0
- package/skills/ultra11y/references/verify.md +88 -0
- package/skills/ultra11y/references/widgets.md +106 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +52739 -0
- package/src/act.ts +441 -0
- package/src/adjudicate.ts +630 -0
- package/src/aria.ts +230 -0
- package/src/audit.ts +396 -0
- package/src/auditor.ts +399 -0
- package/src/autofill.ts +102 -0
- package/src/axe-map.ts +294 -0
- package/src/baseline.ts +75 -0
- package/src/capture.ts +182 -0
- package/src/check.ts +243 -0
- package/src/cli.ts +1927 -0
- package/src/color.ts +102 -0
- package/src/config.ts +182 -0
- package/src/crawl.ts +75 -0
- package/src/criteria.ts +183 -0
- package/src/data/adjudication.json +770 -0
- package/src/data/guidance/rgaa.json +2395 -0
- package/src/data/standards/rgaa.glossary.json +478 -0
- package/src/data/standards/rgaa.json +3454 -0
- package/src/data/wcag-universe.json +530 -0
- package/src/data/wcag.json +1178 -0
- package/src/discover.ts +203 -0
- package/src/fix/codemods.ts +76 -0
- package/src/fix/diff.ts +64 -0
- package/src/fix/edits.ts +126 -0
- package/src/fix.ts +215 -0
- package/src/gh.ts +214 -0
- package/src/glob.ts +206 -0
- package/src/graph/build.ts +114 -0
- package/src/graph/engine-resolve.ts +0 -0
- package/src/graph/graph.ts +126 -0
- package/src/graph/imports.ts +359 -0
- package/src/graph/resolve.ts +73 -0
- package/src/graph/tsconfig.ts +113 -0
- package/src/grounding.ts +119 -0
- package/src/guidance/index.ts +45 -0
- package/src/guidance/types.ts +36 -0
- package/src/init.ts +113 -0
- package/src/mcp/handlers.ts +388 -0
- package/src/mcp/http.ts +260 -0
- package/src/mcp/prompts.ts +183 -0
- package/src/mcp/protocol.ts +187 -0
- package/src/mcp/resources.ts +136 -0
- package/src/mcp/server.ts +228 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/mcp/tools.ts +347 -0
- package/src/messages.ts +1011 -0
- package/src/name.ts +174 -0
- package/src/orchestrate-templates.ts +231 -0
- package/src/orchestrate.ts +186 -0
- package/src/output.ts +108 -0
- package/src/pack.ts +187 -0
- package/src/parse/html.ts +258 -0
- package/src/parse/jsx-ast.ts +67 -0
- package/src/parse/jsx-bridge.ts +247 -0
- package/src/parse/jsx.ts +19 -0
- package/src/parse/source.ts +75 -0
- package/src/prd.ts +406 -0
- package/src/project-lock.ts +42 -0
- package/src/render.ts +307 -0
- package/src/report.ts +399 -0
- package/src/rules/colors.ts +125 -0
- package/src/rules/cross-registry.ts +264 -0
- package/src/rules/cross-rule.ts +83 -0
- package/src/rules/forms.ts +590 -0
- package/src/rules/frames.ts +33 -0
- package/src/rules/headings.ts +227 -0
- package/src/rules/images.ts +200 -0
- package/src/rules/links.ts +193 -0
- package/src/rules/mandatory.ts +173 -0
- package/src/rules/multimedia.ts +77 -0
- package/src/rules/navigation.ts +190 -0
- package/src/rules/presentation.ts +75 -0
- package/src/rules/registry.ts +57 -0
- package/src/rules/rule.ts +122 -0
- package/src/rules/scripts-aria.ts +636 -0
- package/src/rules/tables.ts +264 -0
- package/src/rules/timing.ts +55 -0
- package/src/sample.ts +176 -0
- package/src/scan-local.ts +866 -0
- package/src/scan.ts +538 -0
- package/src/standards/derive.ts +195 -0
- package/src/standards/index.ts +31 -0
- package/src/standards/pack-rules.ts +111 -0
- package/src/standards/pack.ts +81 -0
- package/src/standards/registry.ts +100 -0
- package/src/standards/types.ts +202 -0
- package/src/standards/validate.ts +544 -0
- package/src/standards/vocabulary.ts +75 -0
- package/src/types.ts +355 -0
- package/src/util.ts +32 -0
- package/src/vendor/codeindex-engine.d.mts +1009 -0
- package/src/vendor/codeindex-engine.mjs +13701 -0
- package/src/vendor/engine.meta.json +9 -0
- package/src/verify.ts +275 -0
- package/src/wcag.ts +128 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// DEV-ONLY (not in `bin`). Builds the RGAA 4.1.2 STANDARDS PACK that ships at
|
|
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 does NOT carry the
|
|
5
|
+
// engine's rules or automatability — it is a localized criterion set that maps each
|
|
6
|
+
// of its criteria onto WCAG success criteria (bare SC ids). The WCAG↔rule coverage
|
|
7
|
+
// lives in the core (scripts/build-standards.mjs). The RGAA content is Licence
|
|
8
|
+
// Ouverte / Etalab 2.0 — see NOTICE. The official source is vendored under
|
|
9
|
+
// scripts/vendor/rgaa/ so the build is reproducible offline.
|
|
10
|
+
// node scripts/build-pack-rgaa.mjs # build from the vendored source
|
|
11
|
+
// node scripts/build-pack-rgaa.mjs --offline # alias (the source is always local)
|
|
12
|
+
// node scripts/build-pack-rgaa.mjs --fetch # refresh the vendored source from DINUM, then build
|
|
13
|
+
// node scripts/build-pack-rgaa.mjs --check # rebuild in memory and byte-compare vs the committed
|
|
14
|
+
// # pack; no writes; exit 1 on drift (CI gate)
|
|
15
|
+
import { writeFileSync, readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
+
const OUT = join(root, "src", "data", "standards");
|
|
22
|
+
const VENDOR = join(root, "scripts", "vendor", "rgaa");
|
|
23
|
+
const BIOME = join(root, "node_modules", ".bin", "biome");
|
|
24
|
+
const BASE = "https://raw.githubusercontent.com/DISIC/accessibilite.numerique.gouv.fr/main/RGAA";
|
|
25
|
+
const doFetch = process.argv.includes("--fetch");
|
|
26
|
+
const doCheck = process.argv.includes("--check");
|
|
27
|
+
|
|
28
|
+
// The committed pack JSON is biome-formatted (short arrays collapse onto one line —
|
|
29
|
+
// see biome.json's default `--expand=auto`), not raw `JSON.stringify` output. Route
|
|
30
|
+
// both the write path and the --check comparison through biome so the two can never
|
|
31
|
+
// disagree over whitespace alone; `relPath` (project-relative, e.g.
|
|
32
|
+
// "src/data/standards/rgaa.json") only picks the JSON formatter, no file is touched.
|
|
33
|
+
function biomeFormat(text, relPath) {
|
|
34
|
+
return execFileSync(BIOME, ["format", `--stdin-file-path=${relPath}`], { input: text, encoding: "utf8" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function source(name) {
|
|
38
|
+
if (doFetch) {
|
|
39
|
+
const r = await fetch(`${BASE}/${name}`);
|
|
40
|
+
if (!r.ok) throw new Error(`build-pack-rgaa: ${name} HTTP ${r.status}`);
|
|
41
|
+
const text = await r.text();
|
|
42
|
+
mkdirSync(VENDOR, { recursive: true });
|
|
43
|
+
writeFileSync(join(VENDOR, name), text);
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
return JSON.parse(readFileSync(join(VENDOR, name), "utf8"));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Slug used by the official site for glossary anchors.
|
|
50
|
+
const slug = (s) =>
|
|
51
|
+
s
|
|
52
|
+
.normalize("NFD")
|
|
53
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/['’]/g, " ")
|
|
56
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
57
|
+
.replace(/^-+|-+$/g, "");
|
|
58
|
+
|
|
59
|
+
// [term](#anchor) -> term
|
|
60
|
+
const plain = (s) => s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
|
|
61
|
+
const toArr = (v) => (Array.isArray(v) ? v.map(String) : v == null ? [] : [String(v)]);
|
|
62
|
+
|
|
63
|
+
// crude HTML -> plaintext for glossary bodies
|
|
64
|
+
const deHtml = (s) =>
|
|
65
|
+
s
|
|
66
|
+
.replace(/<\/(p|li|ul|ol|div|tr|h[1-6])>/gi, "\n")
|
|
67
|
+
.replace(/<[^>]+>/g, " ")
|
|
68
|
+
.replace(/ /g, " ")
|
|
69
|
+
.replace(/&/g, "&")
|
|
70
|
+
.replace(/</g, "<")
|
|
71
|
+
.replace(/>/g, ">")
|
|
72
|
+
.replace(/'|’|’/g, "’")
|
|
73
|
+
.replace(/"/g, '"')
|
|
74
|
+
.replace(/[ \t]+/g, " ")
|
|
75
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
76
|
+
.trim();
|
|
77
|
+
|
|
78
|
+
// "1.1.1 Non-text Content (A)" -> "1.1.1" (bare WCAG SC id; the WCAG core owns titles/levels)
|
|
79
|
+
const bareSc = (w) => String(w).trim().split(/\s+/)[0];
|
|
80
|
+
|
|
81
|
+
// ---- Applicability (R1 fix): which ENGINE RULE ids can make each RGAA criterion NC ----
|
|
82
|
+
// A single WCAG SC maps to many RGAA criteria (1.1.1 → 19 criteria: informative image,
|
|
83
|
+
// CAPTCHA, detailed description, layout tables, downloadable documents…). Without this,
|
|
84
|
+
// one `img-alt-missing` failure fanned out to ALL of them. This CURATED table names, per
|
|
85
|
+
// engine rule, the RGAA criterion(s) whose element/context that rule actually evidences;
|
|
86
|
+
// the inverse (criterion → ruleIds) is written to each criterion's `appliesTo`. Static
|
|
87
|
+
// engine rule ids are validated below (must exist + share a WCAG SC with the criterion);
|
|
88
|
+
// axe:*/dyn-* ids (dynamic tier) are namespaced and tolerated as-is.
|
|
89
|
+
//
|
|
90
|
+
// The WCAG SC(s) each STATIC rule can emit — used only to self-validate the table below
|
|
91
|
+
// (a listed (rule, criterion) pair must share an SC, else the entry would be inert).
|
|
92
|
+
const RULE_SC = {
|
|
93
|
+
"aria-hidden-focusable": ["4.1.2"], "aria-invalid-no-description": ["3.3.1"], "aria-ref-missing-id": ["4.1.2"],
|
|
94
|
+
"aria-required-children": ["4.1.2"], "autoplay-media": ["1.4.2", "2.2.2"], "blink-marquee": ["2.2.2"],
|
|
95
|
+
"button-empty-name": ["4.1.2"], "canvas-fallback-missing": ["1.1.1"], "chart-no-accessible-name": ["1.1.1"],
|
|
96
|
+
"clickable-noninteractive": ["4.1.2", "2.1.1"], "contrast-literal": ["1.4.3"], "control-label-missing": ["4.1.2"],
|
|
97
|
+
"css-generated-content-informative": ["1.3.1"], "date-fields-ungrouped": ["3.3.2"], "disabled-context-content": ["4.1.2"],
|
|
98
|
+
"control-name-title-only": ["4.1.2"], "cross-icon-only-unnamed": ["4.1.2"],
|
|
99
|
+
"cross-prop-drilled-name-lost": ["4.1.2"], "data-table-no-headers": ["1.3.1"],
|
|
100
|
+
"decorative-alt-misuse": ["1.1.1"], "duplicate-id": ["4.1.2"], "empty-heading": ["1.3.1"], "error-not-associated": ["3.3.1"],
|
|
101
|
+
"field-purpose-incomplete": ["1.3.5", "4.1.2"], "fieldset-legend-missing": ["1.3.1"], "form-field-multiple-labels": ["4.1.2"],
|
|
102
|
+
"h1-missing": ["1.3.1"], "h1-multiple": ["1.3.1"], "heading-order-skip": ["1.3.1"], "html-lang-missing": ["3.1.1"],
|
|
103
|
+
"icon-only-control-unnamed": ["2.4.4", "4.1.2"], "iframe-title-missing": ["4.1.2"], "img-alt-missing": ["1.1.1"],
|
|
104
|
+
"inline-lang-change-missing": ["3.1.2"], "input-image-alt-missing": ["1.1.1"], "invalid-aria-role": ["4.1.2"],
|
|
105
|
+
"label-for-dangling": ["1.3.1"], "lang-invalid": ["3.1.1", "3.1.2"], "layout-table-data-markup": ["1.3.1"],
|
|
106
|
+
"link-empty-name": ["2.4.4"], "list-structure": ["1.3.1"], "live-region-conflict": ["4.1.3"], "media-no-track": ["1.2.2"],
|
|
107
|
+
"meta-refresh-redirect": ["2.2.1"], "meta-viewport-zoom-block": ["1.4.4"], "missing-main-landmark": ["1.3.1"],
|
|
108
|
+
"multiple-main-landmark": ["1.3.1"], "nav-landmark-missing": ["1.3.1"], "nav-landmark-unnamed": ["1.3.1"],
|
|
109
|
+
"nested-interactive": ["4.1.2"], "object-embed-no-name": ["1.1.1"], "radio-checkbox-group-ungrouped": ["1.3.1", "3.3.2"],
|
|
110
|
+
"placeholder-as-label": ["4.1.2"], "positive-tabindex": ["2.4.3"], "redundant-aria": ["4.1.2"], "select-has-option": ["4.1.2"],
|
|
111
|
+
"skip-link-target-missing": ["2.4.1"], "sortable-header-no-aria-sort": ["1.3.1"], "status-message-not-assertive": ["4.1.3"],
|
|
112
|
+
"table-caption-missing": ["1.3.1"], "table-empty-data-cell": ["1.3.1"], "title-missing-empty": ["2.4.2"],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ruleId → RGAA criterion ids it evidences. Static rules + their axe:/dyn- equivalents so
|
|
116
|
+
// the dynamic-tier merge keeps the same mapping (an axe:color-contrast NC still lands on
|
|
117
|
+
// RGAA 3.2/10.5, not fanned out).
|
|
118
|
+
const RULE_TO_CRITERIA = {
|
|
119
|
+
// Theme 1 — images (1.1 informative alt, 1.2 decorative)
|
|
120
|
+
"img-alt-missing": ["1.1"], "input-image-alt-missing": ["1.1"], "object-embed-no-name": ["1.1"],
|
|
121
|
+
"chart-no-accessible-name": ["1.1"], "canvas-fallback-missing": ["1.1"], "axe:image-alt": ["1.1"],
|
|
122
|
+
"axe:input-image-alt": ["1.1"], "axe:area-alt": ["1.1"], "axe:role-img-alt": ["1.1"], "axe:svg-img-alt": ["1.1"],
|
|
123
|
+
"axe:object-alt": ["1.1"], "decorative-alt-misuse": ["1.2"], "axe:image-redundant-alt": ["1.2"],
|
|
124
|
+
// Theme 2 — frames (2.1 frame title)
|
|
125
|
+
"iframe-title-missing": ["2.1"], "axe:frame-title": ["2.1"], "axe:frame-title-unique": ["2.1"],
|
|
126
|
+
// Theme 3 / 10.5 — colour contrast (3.2 text/bg, 10.5 CSS declarations)
|
|
127
|
+
"contrast-literal": ["3.2", "10.5"], "axe:color-contrast": ["3.2", "10.5"], "axe:color-contrast-enhanced": ["3.2", "10.5"],
|
|
128
|
+
// Theme 4 — multimedia (4.3 captions)
|
|
129
|
+
"media-no-track": ["4.3"], "axe:audio-caption": ["4.3"], "axe:video-caption": ["4.3"],
|
|
130
|
+
// Theme 5 — tables (5.4 title, 5.6/5.7 headers, 5.8 layout-table markup)
|
|
131
|
+
"table-caption-missing": ["5.4"], "data-table-no-headers": ["5.6", "5.7"], "sortable-header-no-aria-sort": ["5.7"],
|
|
132
|
+
"table-empty-data-cell": ["5.7"],
|
|
133
|
+
"layout-table-data-markup": ["5.8"], "axe:td-headers-attr": ["5.7"], "axe:th-has-data-cells": ["5.6"],
|
|
134
|
+
"axe:scope-attr-valid": ["5.7"], "axe:td-has-header": ["5.6"], "axe:empty-table-header": ["5.6"], "axe:table-fake-caption": ["5.8"],
|
|
135
|
+
// Theme 6 — links (6.2 link label)
|
|
136
|
+
"link-empty-name": ["6.2"], "icon-only-control-unnamed": ["6.2", "11.9"], "cross-icon-only-unnamed": ["11.9"],
|
|
137
|
+
"axe:link-name": ["6.2"],
|
|
138
|
+
// Theme 7 — scripts/ARIA (7.1 AT-compat, 7.3 keyboard, 7.5 status messages)
|
|
139
|
+
"invalid-aria-role": ["7.1"], "aria-ref-missing-id": ["7.1"], "aria-required-children": ["7.1"],
|
|
140
|
+
"aria-hidden-focusable": ["7.1"], "redundant-aria": ["7.1"], "nested-interactive": ["7.1"],
|
|
141
|
+
"cross-prop-drilled-name-lost": ["7.1"],
|
|
142
|
+
"clickable-noninteractive": ["7.3"], "live-region-conflict": ["7.5"], "status-message-not-assertive": ["7.5"],
|
|
143
|
+
// Dynamic tier (scan --local): the live-region probe projects onto WCAG 4.1.3 → RGAA 7.5
|
|
144
|
+
// (status messages) — the WCAG-faithful home. Ara ALSO classifies the source finding under
|
|
145
|
+
// RGAA 7.4 (change of context, WCAG 3.2.1/3.2.2); that deviation from the 4.1.3 crosswalk
|
|
146
|
+
// ships as an opt-in `secondaryMappings` entry below (dyn-live-region → 7.4, DISABLED by
|
|
147
|
+
// default), never hardcoded here, so the out-of-box projection stays WCAG-faithful (7.5).
|
|
148
|
+
"dyn-live-region": ["7.5"],
|
|
149
|
+
"disabled-context-content": ["7.1", "10.8"],
|
|
150
|
+
"axe:aria-allowed-attr": ["7.1"], "axe:aria-allowed-role": ["7.1"], "axe:aria-roles": ["7.1"],
|
|
151
|
+
"axe:aria-required-attr": ["7.1"], "axe:aria-required-children": ["7.1"], "axe:aria-required-parent": ["7.1"],
|
|
152
|
+
"axe:aria-valid-attr": ["7.1"], "axe:aria-valid-attr-value": ["7.1"], "axe:nested-interactive": ["7.1"],
|
|
153
|
+
"axe:aria-hidden-focus": ["7.1"], "axe:presentation-role-conflict": ["7.1"],
|
|
154
|
+
// Theme 8 — document (8.2 valid code, 8.3 default lang, 8.4 lang relevant, 8.5 title, 8.7/8.8 lang changes)
|
|
155
|
+
"duplicate-id": ["8.2"], "axe:duplicate-id": ["8.2"], "axe:duplicate-id-aria": ["8.2"], "axe:duplicate-id-active": ["8.2"],
|
|
156
|
+
"html-lang-missing": ["8.3"], "axe:html-has-lang": ["8.3"], "axe:html-xml-lang-mismatch": ["8.3"],
|
|
157
|
+
"lang-invalid": ["8.4", "8.8"], "axe:html-lang-valid": ["8.4"], "axe:valid-lang": ["8.8"],
|
|
158
|
+
"title-missing-empty": ["8.5"], "axe:document-title": ["8.5"], "inline-lang-change-missing": ["8.7"],
|
|
159
|
+
// Theme 9 — structure (9.1 headings, 9.2 doc structure, 9.3 lists)
|
|
160
|
+
"h1-missing": ["9.1"], "h1-multiple": ["9.1"], "heading-order-skip": ["9.1"], "empty-heading": ["9.1"],
|
|
161
|
+
"axe:heading-order": ["9.1"], "axe:empty-heading": ["9.1"], "axe:page-has-heading-one": ["9.1"],
|
|
162
|
+
"missing-main-landmark": ["9.2", "12.6"], "multiple-main-landmark": ["9.2", "12.6"], "axe:landmark-one-main": ["12.6"],
|
|
163
|
+
"nav-landmark-missing": ["9.2", "12.6"], "nav-landmark-unnamed": ["12.6"],
|
|
164
|
+
"list-structure": ["9.3"], "axe:list": ["9.3"], "axe:listitem": ["9.3"], "axe:definition-list": ["9.3"], "axe:dlitem": ["9.3"],
|
|
165
|
+
// Theme 10 — presentation (10.2 CSS-off content, 10.4 zoom, 10.7 focus, 10.11 reflow, 10.12 text-spacing)
|
|
166
|
+
"css-generated-content-informative": ["10.2"],
|
|
167
|
+
"meta-viewport-zoom-block": ["10.4"], "axe:meta-viewport": ["10.4"], "axe:meta-viewport-large": ["10.4"],
|
|
168
|
+
"dyn-reflow": ["10.11"], "dyn-reflow-zoom": ["10.4"], "dyn-focus-visible": ["10.7"], "dyn-text-spacing": ["10.12"], "dyn-hover": ["10.13"],
|
|
169
|
+
// Stateful input-overflow probes — a filled input clipped under each stress, same RGAA
|
|
170
|
+
// theme as the corresponding reflow/zoom/text-spacing residual probe above.
|
|
171
|
+
"dyn-input-overflow-reflow": ["10.11"], "dyn-input-overflow-zoom": ["10.4"], "dyn-input-overflow-spacing": ["10.12"],
|
|
172
|
+
// Theme 11 — forms (11.1 field label, 11.5 field grouping, 11.6 fieldset legend, 11.9 button label, 11.10 input control, 11.13 autocomplete)
|
|
173
|
+
"control-label-missing": ["11.1"], "label-for-dangling": ["11.1"], "placeholder-as-label": ["11.1"],
|
|
174
|
+
"form-field-multiple-labels": ["11.1"], "select-has-option": ["11.1"], "control-name-title-only": ["11.1"],
|
|
175
|
+
"radio-checkbox-group-ungrouped": ["11.5"], "date-fields-ungrouped": ["11.5"],
|
|
176
|
+
"field-purpose-incomplete": ["11.1", "11.13"], "fieldset-legend-missing": ["11.6"], "button-empty-name": ["11.9"],
|
|
177
|
+
"error-not-associated": ["11.10"], "aria-invalid-no-description": ["11.10"],
|
|
178
|
+
"axe:label": ["11.1"], "axe:form-field-multiple-labels": ["11.1"], "axe:select-name": ["11.1"], "axe:label-title-only": ["11.1"],
|
|
179
|
+
"axe:autocomplete-valid": ["11.13"], "axe:fieldset": ["11.6"], "axe:input-button-name": ["11.9"], "axe:button-name": ["11.9"],
|
|
180
|
+
// Theme 12 — navigation (12.7 skip link, 12.8 tab order)
|
|
181
|
+
"skip-link-target-missing": ["12.7"], "axe:skip-link": ["12.7"], "axe:bypass": ["12.7"],
|
|
182
|
+
"positive-tabindex": ["12.8"], "axe:tabindex": ["12.8"],
|
|
183
|
+
// Theme 13 — consultation (13.1 time limits, 13.8 moving/blinking)
|
|
184
|
+
"meta-refresh-redirect": ["13.1"], "blink-marquee": ["13.8"], "autoplay-media": ["4.10", "13.8"],
|
|
185
|
+
"axe:no-autoplay-audio": ["4.10"], "axe:blink": ["13.8"], "axe:marquee": ["13.8"],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
async function main() {
|
|
189
|
+
const criteres = await source("criteres.json");
|
|
190
|
+
const glossaire = await source("glossaire.json");
|
|
191
|
+
|
|
192
|
+
const glossary = {};
|
|
193
|
+
for (const e of glossaire.glossary) glossary[slug(e.title)] = { title: e.title, body: deHtml(e.body) };
|
|
194
|
+
|
|
195
|
+
const themes = [];
|
|
196
|
+
const criteria = [];
|
|
197
|
+
for (const topic of criteres.topics) {
|
|
198
|
+
let count = 0;
|
|
199
|
+
for (const { criterium } of topic.criteria) {
|
|
200
|
+
const id = `${topic.number}.${criterium.number}`;
|
|
201
|
+
const wcag = new Set();
|
|
202
|
+
const techniques = new Set();
|
|
203
|
+
for (const ref of criterium.references || []) {
|
|
204
|
+
for (const w of ref.wcag || []) wcag.add(bareSc(w));
|
|
205
|
+
for (const t of ref.techniques || []) techniques.add(String(t));
|
|
206
|
+
}
|
|
207
|
+
criteria.push({
|
|
208
|
+
id,
|
|
209
|
+
theme: topic.number,
|
|
210
|
+
title: { fr: criterium.title },
|
|
211
|
+
titlePlain: { fr: plain(criterium.title) },
|
|
212
|
+
tests: criterium.tests || {},
|
|
213
|
+
techniques: [...techniques],
|
|
214
|
+
...(criterium.technicalNote ? { technicalNote: toArr(criterium.technicalNote) } : {}),
|
|
215
|
+
...(criterium.particularCases ? { particularCases: toArr(criterium.particularCases) } : {}),
|
|
216
|
+
wcag: [...wcag],
|
|
217
|
+
});
|
|
218
|
+
count++;
|
|
219
|
+
}
|
|
220
|
+
themes.push({ number: topic.number, name: { fr: topic.topic }, count });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- Applicability: invert RULE_TO_CRITERIA and attach an explicit `appliesTo` to EVERY
|
|
224
|
+
// criterion (empty when no engine rule can evidence it, so an SC-sibling can never leak a
|
|
225
|
+
// foreign finding). Self-validate that each STATIC rule shares a WCAG SC with the criteria
|
|
226
|
+
// it's listed under (an inert entry is almost always a curation mistake). ----
|
|
227
|
+
const wcagById = new Map(criteria.map((c) => [c.id, new Set(c.wcag)]));
|
|
228
|
+
const critRules = {};
|
|
229
|
+
for (const [ruleId, critIds] of Object.entries(RULE_TO_CRITERIA)) {
|
|
230
|
+
const scs = RULE_SC[ruleId]; // undefined for axe:/dyn- (namespaced, not statically validated)
|
|
231
|
+
for (const cid of critIds) {
|
|
232
|
+
if (!wcagById.has(cid)) throw new Error(`RULE_TO_CRITERIA: rule "${ruleId}" → unknown RGAA criterion "${cid}"`);
|
|
233
|
+
if (scs && !scs.some((sc) => wcagById.get(cid).has(sc))) {
|
|
234
|
+
throw new Error(`RULE_TO_CRITERIA: "${ruleId}" (SC ${scs.join(",")}) under RGAA ${cid} (WCAG ${[...wcagById.get(cid)].join(",")}) — no shared SC, entry is inert`);
|
|
235
|
+
}
|
|
236
|
+
(critRules[cid] ||= new Set()).add(ruleId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const c of criteria) c.appliesTo = { ruleIds: [...(critRules[c.id] ?? [])].sort() };
|
|
240
|
+
|
|
241
|
+
// ---- Declarative pack RULE (usage proof): an RGAA-only ADVISORY recommendation, run by
|
|
242
|
+
// the bounded interpreter (src/standards/pack-rules.ts) AFTER the core engine rules. It
|
|
243
|
+
// flags a download link (`a[href$=".pdf"]` & co) whose VISIBLE TEXT states neither the
|
|
244
|
+
// file format nor its weight — the DSFR/RGAA auditor recommendation under criterion 6.1
|
|
245
|
+
// (link explicitness, WCAG 2.4.4). ADVISORY: it surfaces as a recommendation in the RGAA
|
|
246
|
+
// projection and NEVER makes 6.1 non-conformant. This proves a pack ships its OWN
|
|
247
|
+
// detection without forking the engine (see skills/ultra11y/references/packs.md).
|
|
248
|
+
const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|epub|mp3|mp4|avi|mov";
|
|
249
|
+
const rules = [
|
|
250
|
+
{
|
|
251
|
+
id: "download-link-format",
|
|
252
|
+
criterion: "6.1",
|
|
253
|
+
wcag: ["2.4.4"],
|
|
254
|
+
severity: "mineur",
|
|
255
|
+
advisory: true,
|
|
256
|
+
match: {
|
|
257
|
+
tag: "a",
|
|
258
|
+
attrs: [{ name: "href", op: "matches", value: `\\.(${DOWNLOAD_EXT})(\\?|#|$)` }],
|
|
259
|
+
text: { op: "lacks", value: `(${DOWNLOAD_EXT}|\\d+\\s*(ko|mo|go|kb|mb|gb|octets?|bytes?))` },
|
|
260
|
+
},
|
|
261
|
+
message: {
|
|
262
|
+
en: "Download link whose visible text states neither the file format nor its size.",
|
|
263
|
+
fr: "Lien de téléchargement dont l’intitulé ne précise ni le format ni le poids du fichier.",
|
|
264
|
+
},
|
|
265
|
+
remediation: {
|
|
266
|
+
en: "State the file format and size in the link text, e.g. “Annual report (PDF, 2 MB)”.",
|
|
267
|
+
fr: "Indiquez le format et le poids du fichier dans l’intitulé du lien, par exemple « Rapport annuel (PDF, 2 Mo) ».",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
// Wire each rule's namespaced id (`pack:rgaa:<id>`) into the criterion it reports under,
|
|
272
|
+
// so derivePackResults routes its finding onto that criterion through the same
|
|
273
|
+
// appliesTo/ruleMatches machinery as engine findings (6.1 evidences no engine rule).
|
|
274
|
+
for (const rule of rules) {
|
|
275
|
+
const c = criteria.find((x) => x.id === rule.criterion);
|
|
276
|
+
if (!c) throw new Error(`build-pack-rgaa: rule "${rule.id}" reports under unknown criterion "${rule.criterion}"`);
|
|
277
|
+
c.appliesTo = { ruleIds: [...new Set([...c.appliesTo.ruleIds, `pack:rgaa:${rule.id}`])].sort() };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const pack = {
|
|
281
|
+
key: "rgaa",
|
|
282
|
+
name: "RGAA",
|
|
283
|
+
fullName: "Référentiel général d’amélioration de l’accessibilité",
|
|
284
|
+
org: "DINUM",
|
|
285
|
+
country: "FR",
|
|
286
|
+
baseVersion: "4.1.2",
|
|
287
|
+
wcagVersion: String(criteres.wcag.version),
|
|
288
|
+
locales: ["fr"],
|
|
289
|
+
defaultLocale: "fr",
|
|
290
|
+
license: "Licence Ouverte / Etalab 2.0",
|
|
291
|
+
source: "https://github.com/DISIC/accessibilite.numerique.gouv.fr",
|
|
292
|
+
attribution: "RGAA 4.1.2 © DINUM (Direction interministérielle du numérique) — Licence Ouverte / Etalab 2.0",
|
|
293
|
+
idPattern: "^\\d+\\.\\d+$",
|
|
294
|
+
// Auditor-display vocabulary (FR): the nouns an RGAA auditor reads. Rendered by the
|
|
295
|
+
// `prd` auditor block + GitHub issues; see src/standards/vocabulary.ts.
|
|
296
|
+
vocabulary: {
|
|
297
|
+
theme: { fr: "Thématique" },
|
|
298
|
+
criterion: { fr: "Critère" },
|
|
299
|
+
test: { fr: "Test" },
|
|
300
|
+
conformant: { fr: "Conforme (C)" },
|
|
301
|
+
nonConformant: { fr: "Non conforme (NC)" },
|
|
302
|
+
notApplicable: { fr: "Non applicable (NA)" },
|
|
303
|
+
auditorHeading: { fr: "Critère d’accessibilité" },
|
|
304
|
+
},
|
|
305
|
+
// Normative page-sample methodology (RGAA): the REQUIRED page KINDS a real audit must
|
|
306
|
+
// cover — its representative sample. Standard-agnostic sample MECHANICS live in the core
|
|
307
|
+
// (src/sample.ts + Ultra11yConfig.sample); this carries only RGAA's own required-kinds
|
|
308
|
+
// list. Drives the advisory `sample check` / `scan --sample` lint (fuzzy match on a
|
|
309
|
+
// page's name/notes/url — short keywords match whole words only), never a hard gate.
|
|
310
|
+
// Keywords are accent-insensitive, fr + en. Ambiguous single words are deliberately NOT
|
|
311
|
+
// keywords ("plan" alone credited "Plan de formation" to plan-du-site; "support"
|
|
312
|
+
// credited "Support RH" to aide) — the multi-word canonical phrases carry those kinds.
|
|
313
|
+
sampleMethodology: {
|
|
314
|
+
requiredKinds: [
|
|
315
|
+
{ id: "accueil", label: { fr: "Page d’accueil" }, keywords: ["accueil", "home", "index", "racine", "homepage"] },
|
|
316
|
+
{ id: "contact", label: { fr: "Contact" }, keywords: ["contact", "nous contacter", "nous ecrire", "coordonnees"] },
|
|
317
|
+
{ id: "mentions-legales", label: { fr: "Mentions légales" }, keywords: ["mentions legales", "mentions", "legal notice", "legal"] },
|
|
318
|
+
{
|
|
319
|
+
id: "declaration-accessibilite",
|
|
320
|
+
label: { fr: "Déclaration d’accessibilité" },
|
|
321
|
+
keywords: ["declaration d accessibilite", "declaration accessibilite", "accessibilite", "accessibility statement", "accessibility"],
|
|
322
|
+
},
|
|
323
|
+
{ id: "plan-du-site", label: { fr: "Plan du site" }, keywords: ["plan du site", "sitemap", "site map"] },
|
|
324
|
+
{ id: "aide", label: { fr: "Aide" }, keywords: ["aide", "help", "faq", "assistance"] },
|
|
325
|
+
{
|
|
326
|
+
id: "authentification",
|
|
327
|
+
label: { fr: "Authentification" },
|
|
328
|
+
keywords: ["authentification", "authentication", "connexion", "identification", "login", "log in", "sign in", "se connecter", "auth"],
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: "pages-representatives",
|
|
332
|
+
label: { fr: "Pages représentatives" },
|
|
333
|
+
keywords: ["representative", "representatif", "representatives", "gabarit", "template", "modele", "formulaire", "recherche", "resultats"],
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: "elements-transverses",
|
|
337
|
+
label: { fr: "Éléments transverses" },
|
|
338
|
+
keywords: ["transverse", "transversaux", "en-tete", "entete", "header", "navigation", "menu", "pied de page", "footer"],
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
// Opt-in SECONDARY crosswalk mapping (Task 13): the live-region probe keys on WCAG 4.1.3,
|
|
343
|
+
// whose WCAG-faithful RGAA home is 7.5 (status messages). Ara additionally classifies the
|
|
344
|
+
// same finding under 7.4 (change of context). That is a DELIBERATE deviation from the SC
|
|
345
|
+
// crosswalk, so it ships DISABLED — the default projection stays WCAG-faithful (7.5 only).
|
|
346
|
+
// Enable per-project via `.ultra11yrc.json`:
|
|
347
|
+
// { "secondaryMappings": [{ "standard": "rgaa", "ruleId": "dyn-live-region", "criterion": "7.4" }] }
|
|
348
|
+
// (see src/standards/types.ts SecondaryMapping + src/config.ts). EXACT-ruleId match means
|
|
349
|
+
// the other 4.1.3 rules (status-message-not-assertive, live-region-conflict) never cross over.
|
|
350
|
+
secondaryMappings: [
|
|
351
|
+
{
|
|
352
|
+
ruleId: "dyn-live-region",
|
|
353
|
+
criterion: "7.4",
|
|
354
|
+
note: {
|
|
355
|
+
fr: "Relève aussi de 7.4 (changement de contexte) selon le classement Ara ; projection WCAG-fidèle = 7.5.",
|
|
356
|
+
en: "Also classified under 7.4 (change of context) per Ara; the WCAG-faithful projection is 7.5.",
|
|
357
|
+
},
|
|
358
|
+
enabled: false,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
rules,
|
|
362
|
+
themes,
|
|
363
|
+
criteria,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const packText = biomeFormat(JSON.stringify(pack, null, 2) + "\n", "src/data/standards/rgaa.json");
|
|
367
|
+
const glossaryText = biomeFormat(JSON.stringify(glossary, null, 2) + "\n", "src/data/standards/rgaa.glossary.json");
|
|
368
|
+
|
|
369
|
+
if (doCheck) {
|
|
370
|
+
const packPath = join(OUT, "rgaa.json");
|
|
371
|
+
const glossaryPath = join(OUT, "rgaa.glossary.json");
|
|
372
|
+
const drift = [];
|
|
373
|
+
if (!existsSync(packPath) || readFileSync(packPath, "utf8") !== packText) drift.push("src/data/standards/rgaa.json");
|
|
374
|
+
if (!existsSync(glossaryPath) || readFileSync(glossaryPath, "utf8") !== glossaryText)
|
|
375
|
+
drift.push("src/data/standards/rgaa.glossary.json");
|
|
376
|
+
if (drift.length > 0) {
|
|
377
|
+
console.error(`build-pack-rgaa --check: OUT OF DATE vs vendored source — re-run \`pnpm run build:pack:rgaa\`: ${drift.join(", ")}`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
console.log("build-pack-rgaa --check: src/data/standards/rgaa.json and rgaa.glossary.json match the vendored source.");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
mkdirSync(OUT, { recursive: true });
|
|
385
|
+
writeFileSync(join(OUT, "rgaa.json"), packText);
|
|
386
|
+
writeFileSync(join(OUT, "rgaa.glossary.json"), glossaryText);
|
|
387
|
+
|
|
388
|
+
const noWcag = criteria.filter((c) => c.wcag.length === 0).map((c) => c.id);
|
|
389
|
+
console.log(`build-pack-rgaa: ${themes.length} themes, ${criteria.length} criteria → src/data/standards/rgaa.json`);
|
|
390
|
+
console.log(`build-pack-rgaa: glossary ${Object.keys(glossary).length} entries → src/data/standards/rgaa.glossary.json`);
|
|
391
|
+
console.log(`build-pack-rgaa: criteria with no WCAG mapping (pack-local): ${noWcag.length ? noWcag.join(", ") : "none"} (${noWcag.length})`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
main().catch((e) => {
|
|
395
|
+
console.error(e);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
});
|