ultra11y 4.3.0 → 4.4.1
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 +40 -4
- package/package.json +3 -1
- package/scripts/build-guidance-wcag.mjs +335 -0
- package/scripts/build-standards.mjs +290 -1
- package/scripts/ultra11y.mjs +3805 -274
- package/scripts/vendor/wcag-2.2-fr.json +944 -0
- package/scripts/vendor/wcag-2.2-techniques.json +840 -0
- package/scripts/vendor/wcag-2.2-text.json +749 -0
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +3805 -274
- package/skills/ultra11y/SKILL.md +13 -4
- package/skills/ultra11y/references/guidance.md +61 -7
- package/skills/ultra11y/references/harnesses.md +10 -0
- package/skills/ultra11y/references/mcp.md +215 -0
- package/skills/ultra11y/references/packs.md +8 -0
- package/skills/ultra11y/references/standards.md +18 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +3805 -274
- package/src/adjudicate.ts +9 -15
- package/src/adjudication-data.ts +38 -0
- package/src/check.ts +51 -1
- package/src/cli.ts +6 -2
- package/src/config.ts +27 -4
- package/src/criteria-view.ts +463 -0
- package/src/criteria.ts +14 -11
- package/src/data/guidance/wcag.json +1126 -0
- package/src/data/wcag.json +1112 -55
- package/src/guidance/index.ts +14 -0
- package/src/guidance/resolve.ts +71 -0
- package/src/mcp/handlers.ts +160 -15
- package/src/mcp/prompts.ts +13 -2
- package/src/mcp/server.ts +18 -4
- package/src/mcp/standards-resources.ts +174 -0
- package/src/mcp/tools.ts +140 -6
- package/src/method-view.ts +227 -0
- package/src/prd.ts +5 -16
- package/src/standards/coverage.ts +237 -0
- package/src/standards/index.ts +1 -0
- package/src/standards/pack.ts +19 -0
- package/src/standards/registry.ts +115 -9
- package/src/types.ts +24 -1
- package/src/wcag.ts +21 -1
|
@@ -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": "4.
|
|
13
|
+
"version": "4.4.1",
|
|
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": "4.
|
|
4
|
+
"version": "4.4.1",
|
|
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": "4.
|
|
4
|
+
"version": "4.4.1",
|
|
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 = "4.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "4.4.1";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -196,15 +196,15 @@ the one output an accessibility tool must never produce.
|
|
|
196
196
|
|
|
197
197
|
### Tools
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
Sixteen read tools, in two blocks. **The audit block** tells you what the page does:
|
|
200
200
|
|
|
201
201
|
| Tool | What it does |
|
|
202
202
|
|------|--------------|
|
|
203
203
|
| `ultra11y_audit` | The static pass — findings keyed by success criterion |
|
|
204
204
|
| `ultra11y_adjudicate` | The criteria the engine **cannot** decide, with their evidence |
|
|
205
|
-
| `ultra11y_criteria` | The offline standards reference — look a criterion up, don't recall it |
|
|
206
205
|
| `ultra11y_report` | The dated conformance report, WCAG or a country pack |
|
|
207
206
|
| `ultra11y_prd` | Non-conformities → remediation units with effort |
|
|
207
|
+
| `ultra11y_tickets` | The same backlog as a ticket plan — dry-run, files nothing |
|
|
208
208
|
| `ultra11y_check` | The gate: nothing asserted conformant that was never tested |
|
|
209
209
|
| `ultra11y_verify` | Claim↔evidence worklist |
|
|
210
210
|
| `ultra11y_pack_check` | Validate a country standards pack against what it really ships |
|
|
@@ -212,6 +212,28 @@ Eleven read tools. `ultra11y_audit` is the entry point:
|
|
|
212
212
|
| `ultra11y_pages` | The per-page view: the criterion × page grid, or one dossier per page |
|
|
213
213
|
| `ultra11y_read` | A file, or a line range, from the project |
|
|
214
214
|
|
|
215
|
+
**The reference block** tells you what the standard requires — the half that keeps an agent
|
|
216
|
+
from auditing out of memory. `cwd` is optional on all five; it selects whose packs are visible:
|
|
217
|
+
|
|
218
|
+
| Tool | What it does |
|
|
219
|
+
|------|--------------|
|
|
220
|
+
| `ultra11y_standards` | Which standards exist here, and how much of each any engine can decide |
|
|
221
|
+
| `ultra11y_criteria` | One criterion in full — wording, **numbered tests**, techniques, mapping, defined terms — or the index, or one theme |
|
|
222
|
+
| `ultra11y_glossary` | What a term the standard **defines** means, and which criteria it governs |
|
|
223
|
+
| `ultra11y_guidance` | The before/after implementation pattern, inherited through the WCAG mapping when the pack has none |
|
|
224
|
+
| `ultra11y_method` | The work plan: what the engine settles, what needs a render, what is yours |
|
|
225
|
+
|
|
226
|
+
`ultra11y_method` is the one to call first. It partitions the standard into evidence tiers —
|
|
227
|
+
`source`, `cross-file`, `rendered-page`, `browser`, `judgment`, `out-of-scope` — from the
|
|
228
|
+
engine's own per-criterion rule applicability, never from guessing at the wording of a test.
|
|
229
|
+
For RGAA it reports that **58 of 106 criteria declare that no engine rule can evidence them**.
|
|
230
|
+
That is the standard saying they are yours, and a plan that hides it reads as coverage that
|
|
231
|
+
does not exist.
|
|
232
|
+
|
|
233
|
+
`standard` carries no enum, deliberately: a country pack arrives with a project, so an enum
|
|
234
|
+
pinned when the tool list was built would reject a pack that is perfectly valid for the
|
|
235
|
+
project being asked about. The handler validates against the registry and names what it knows.
|
|
236
|
+
|
|
215
237
|
`--allow-write` additionally exposes `ultra11y_fix` (safe codemods, dry-run by
|
|
216
238
|
default) and `ultra11y_init` (hook/CI/baseline) — the tools that change **your**
|
|
217
239
|
project. `ultra11y_scan` is declared but declines over MCP: it drives a headless
|
|
@@ -237,12 +259,26 @@ Each carries the coverage arithmetic: of the 55 WCAG 2.2 AA criteria the static
|
|
|
237
259
|
engine decides a handful, a browser decides fourteen, and **thirty-eight are
|
|
238
260
|
yours**.
|
|
239
261
|
|
|
240
|
-
### Resources — the
|
|
262
|
+
### Resources — the documentation, and the standards themselves
|
|
241
263
|
|
|
242
|
-
`SKILL.md` and all
|
|
264
|
+
`SKILL.md` and all 37 `references/*.md` are served under `skill://`, read off
|
|
243
265
|
disk at request time — so a documentation fix reaches every client without a
|
|
244
266
|
rebuild.
|
|
245
267
|
|
|
268
|
+
The standards are served too, under `std://`, because in MCP documentation is a
|
|
269
|
+
*resource*, not a tool call:
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
std://rgaa/criteria/8.3 std://rgaa/glossary/lien std://rgaa/method
|
|
273
|
+
std://rgaa/themes/8 std://rgaa/guidance/13.2 std://rgaa/pack.json
|
|
274
|
+
std://wcag/criteria/1.4.3 std://wcag/glossary std://wcag/method
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`resources/list` carries a bounded index per standard; the per-item URIs are
|
|
278
|
+
**templates** (`resources/templates/list`), because enumerating RGAA's 106
|
|
279
|
+
criteria and 119 glossary terms would bloat every client's listing and go stale
|
|
280
|
+
the moment a project's own pack registers.
|
|
281
|
+
|
|
246
282
|
Two things worth knowing:
|
|
247
283
|
|
|
248
284
|
- **A criterion nobody tested is untested, never conformant.** Every tool
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.1",
|
|
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",
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
"build:wcag": "node scripts/build-standards.mjs",
|
|
59
59
|
"build:pack:rgaa": "node scripts/build-pack-rgaa.mjs",
|
|
60
60
|
"check:pack:rgaa": "node scripts/build-pack-rgaa.mjs --check",
|
|
61
|
+
"build:guidance:wcag": "node scripts/build-guidance-wcag.mjs",
|
|
62
|
+
"check:guidance:wcag": "node scripts/build-guidance-wcag.mjs --check",
|
|
61
63
|
"build:criteria": "node scripts/ultra11y.mjs criteria --generate > skills/ultra11y/references/criteria.md",
|
|
62
64
|
"build:act": "UPDATE_ACT_MATRIX=1 vitest run tests/act-matrix-sync.test.ts",
|
|
63
65
|
"build:act:refresh": "node scripts/build-act-corpus.mjs --refresh",
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// DEV-ONLY (not in `bin`). Builds src/data/guidance/wcag.json — the WCAG-keyed
|
|
3
|
+
// implementation guidance — from the W3C's own Techniques and Understanding documents.
|
|
4
|
+
//
|
|
5
|
+
// Nothing here is hand-written. The RGAA guidance is derived from an upstream rule source
|
|
6
|
+
// and rebuilt by a script; this one had been authored by hand, which made it the only
|
|
7
|
+
// dataset in the repo whose content nobody could re-derive or check against a source. Now
|
|
8
|
+
// it is generated the same way everything else is:
|
|
9
|
+
//
|
|
10
|
+
// · WHICH techniques implement a criterion — understanding/understanding.11tydata.js,
|
|
11
|
+
// the W3C's own machine-readable association map (sufficient / advisory / failure).
|
|
12
|
+
// · WHAT the criterion asks — the "In brief" block of understanding/<ver>/<slug>.html
|
|
13
|
+
// (Goal / What to do / Why it's important), written by the WCAG working group.
|
|
14
|
+
// · The GOOD example — the first sufficient technique carrying an HTML or CSS code
|
|
15
|
+
// sample, in the W3C's own listing order.
|
|
16
|
+
// · The BAD example — the first documented FAILURE carrying one.
|
|
17
|
+
//
|
|
18
|
+
// The selection is positional, never editorial: "the first one that has a code sample",
|
|
19
|
+
// in the order W3C lists them. A criterion whose techniques carry no HTML/CSS sample gets
|
|
20
|
+
// an entry with the official summary and no example, rather than an invented one.
|
|
21
|
+
//
|
|
22
|
+
// WCAG 2.2 © W3C, reused under the W3C Document License; the code samples are Code
|
|
23
|
+
// Components under the W3C Software License. See NOTICE.
|
|
24
|
+
//
|
|
25
|
+
// Usage:
|
|
26
|
+
// node scripts/build-guidance-wcag.mjs --refresh # re-fetch (network) the vendored snapshot
|
|
27
|
+
// node scripts/build-guidance-wcag.mjs # emit the dataset from the snapshot
|
|
28
|
+
// node scripts/build-guidance-wcag.mjs --check # verify the committed dataset matches
|
|
29
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
31
|
+
import { dirname, join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
|
|
35
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
36
|
+
const VENDOR = join(root, "scripts", "vendor", "wcag-2.2-techniques.json");
|
|
37
|
+
const SNAPSHOT = join(root, "scripts", "vendor", "wcag-2.2-sc.json");
|
|
38
|
+
const OUT = join(root, "src", "data", "guidance", "wcag.json");
|
|
39
|
+
const BIOME = join(root, "node_modules", ".bin", "biome");
|
|
40
|
+
const RAW = "https://raw.githubusercontent.com/w3c/wcag/main";
|
|
41
|
+
const VER_OF = { "2.0": "20", "2.1": "21", "2.2": "22" };
|
|
42
|
+
|
|
43
|
+
// Technique id → the directory it lives in. The W3C names these by technology, and the
|
|
44
|
+
// prefix is the technology — no directory listing needed, and no id ever special-cased.
|
|
45
|
+
const TECH_DIR = {
|
|
46
|
+
ARIA: "aria",
|
|
47
|
+
C: "css",
|
|
48
|
+
F: "failures",
|
|
49
|
+
FLASH: "flash",
|
|
50
|
+
G: "general",
|
|
51
|
+
H: "html",
|
|
52
|
+
PDF: "pdf",
|
|
53
|
+
SCR: "client-side-script",
|
|
54
|
+
SL: "silverlight",
|
|
55
|
+
SM: "smil",
|
|
56
|
+
SVR: "server-side-script",
|
|
57
|
+
T: "text",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function techDir(id) {
|
|
61
|
+
const prefix = (id.match(/^[A-Z]+/) || [])[0];
|
|
62
|
+
return TECH_DIR[prefix];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function biomeFormat(text, relPath) {
|
|
66
|
+
return execFileSync(BIOME, ["format", `--stdin-file-path=${relPath}`], { input: text, encoding: "utf8" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchText(path) {
|
|
70
|
+
const r = await fetch(`${RAW}/${path}`);
|
|
71
|
+
if (!r.ok) return null; // a technique listed but not present is data, not a crash
|
|
72
|
+
return r.text();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ENTITIES = {
|
|
76
|
+
" ": " ",
|
|
77
|
+
"&": "&",
|
|
78
|
+
"<": "<",
|
|
79
|
+
">": ">",
|
|
80
|
+
""": '"',
|
|
81
|
+
"'": "'",
|
|
82
|
+
"’": "’",
|
|
83
|
+
"‘": "‘",
|
|
84
|
+
"“": "“",
|
|
85
|
+
"”": "”",
|
|
86
|
+
"—": "—",
|
|
87
|
+
"–": "–",
|
|
88
|
+
"…": "…",
|
|
89
|
+
};
|
|
90
|
+
function decodeEntities(s) {
|
|
91
|
+
return s
|
|
92
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
|
|
93
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(Number.parseInt(h, 16)))
|
|
94
|
+
.replace(/&[a-z]+;/gi, (m) => ENTITIES[m] ?? m);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Tags out, entities in, whitespace collapsed — for prose, never for code. */
|
|
98
|
+
function plain(html) {
|
|
99
|
+
return decodeEntities(html.replace(/<[^>]+>/g, " "))
|
|
100
|
+
.replace(/\s+/g, " ")
|
|
101
|
+
.trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The W3C's own association map, read by EVALUATING the module rather than regexing it.
|
|
106
|
+
*
|
|
107
|
+
* It is an ES module exporting a function of 11ty's data cascade; calling it with a stub
|
|
108
|
+
* yields the map exactly as the W3C's own site build sees it. Parsing this shape with a
|
|
109
|
+
* regex would be guesswork, and it is dev-only code from the same repo this project already
|
|
110
|
+
* derives its entire WCAG dataset from.
|
|
111
|
+
*/
|
|
112
|
+
async function fetchAssociations() {
|
|
113
|
+
const src = await fetchText("understanding/understanding.11tydata.js");
|
|
114
|
+
if (!src) throw new Error("build-guidance-wcag: understanding.11tydata.js not found upstream");
|
|
115
|
+
const tmp = join(tmpdir(), `u11y-assoc-${process.pid}.mjs`);
|
|
116
|
+
writeFileSync(tmp, src);
|
|
117
|
+
try {
|
|
118
|
+
const mod = await import(`file://${tmp}`);
|
|
119
|
+
return mod.default({ understandingUrl: "" }).associatedTechniques ?? {};
|
|
120
|
+
} finally {
|
|
121
|
+
rmSync(tmp, { force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Every technique id an entry mentions, flattened, in the W3C's own listing order. */
|
|
126
|
+
function flattenTechniques(node, out = []) {
|
|
127
|
+
if (node === null || node === undefined) return out;
|
|
128
|
+
if (typeof node === "string") {
|
|
129
|
+
// Ids only: the map also carries free prose ("Providing a descriptive label").
|
|
130
|
+
if (/^[A-Z]+\d+$/.test(node)) out.push(node);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(node)) {
|
|
134
|
+
for (const n of node) flattenTechniques(n, out);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
if (typeof node === "object") {
|
|
138
|
+
// { and: [...] } / { title, techniques, groups } — order matters, so walk explicitly.
|
|
139
|
+
for (const key of ["and", "techniques", "groups", "using"]) {
|
|
140
|
+
if (node[key]) flattenTechniques(node[key], out);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The first HTML or CSS code sample in a technique document, with the prose above it. */
|
|
148
|
+
function firstCodeExample(html) {
|
|
149
|
+
const EX = /<section class="example">([\s\S]*?)<\/section>/g;
|
|
150
|
+
let m;
|
|
151
|
+
while ((m = EX.exec(html))) {
|
|
152
|
+
const block = m[1];
|
|
153
|
+
const code = /<pre><code class="language-(html|css)">([\s\S]*?)<\/code><\/pre>/.exec(block);
|
|
154
|
+
if (!code) continue;
|
|
155
|
+
const snippet = decodeEntities(code[2]).trim();
|
|
156
|
+
if (!snippet) continue;
|
|
157
|
+
const before = block.slice(0, code.index);
|
|
158
|
+
return { lang: code[1], code: snippet, note: plain(before) };
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function techniqueTitle(html) {
|
|
164
|
+
const h1 = /<h1>([\s\S]*?)<\/h1>/.exec(html);
|
|
165
|
+
return h1 ? plain(h1[1]) : "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The "In brief" block: the working group's own Goal / What to do / Why. */
|
|
169
|
+
function briefOf(html) {
|
|
170
|
+
const sec = /<section id="brief">([\s\S]*?)<\/section>/.exec(html);
|
|
171
|
+
if (!sec) return null;
|
|
172
|
+
const out = {};
|
|
173
|
+
const DL = /<dt>([\s\S]*?)<\/dt>\s*<dd>([\s\S]*?)<\/dd>/g;
|
|
174
|
+
let m;
|
|
175
|
+
while ((m = DL.exec(sec[1]))) out[plain(m[1]).toLowerCase().replace(/[^a-z]+/g, "")] = plain(m[2]);
|
|
176
|
+
return Object.keys(out).length ? out : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function refresh() {
|
|
180
|
+
if (!existsSync(SNAPSHOT)) {
|
|
181
|
+
console.error(`build-guidance-wcag --refresh: missing ${SNAPSHOT}. Run: node scripts/build-standards.mjs --refresh <w3c/wcag checkout>`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
const snap = JSON.parse(readFileSync(SNAPSHOT, "utf8"));
|
|
185
|
+
const assoc = await fetchAssociations();
|
|
186
|
+
|
|
187
|
+
const cache = new Map();
|
|
188
|
+
const techniqueDoc = async (id) => {
|
|
189
|
+
if (cache.has(id)) return cache.get(id);
|
|
190
|
+
const dir = techDir(id);
|
|
191
|
+
const doc = dir ? await fetchText(`techniques/${dir}/${id}.html`) : null;
|
|
192
|
+
cache.set(id, doc);
|
|
193
|
+
return doc;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const criteria = {};
|
|
197
|
+
for (const c of snap.criteria) {
|
|
198
|
+
const a = assoc[c.slug];
|
|
199
|
+
const understanding = await fetchText(`understanding/${VER_OF[c.addedIn]}/${c.slug}.html`);
|
|
200
|
+
const brief = understanding ? briefOf(understanding) : null;
|
|
201
|
+
|
|
202
|
+
// Selection: the first technique that carries a code sample, taking the technologies in
|
|
203
|
+
// a fixed order rather than purely positionally.
|
|
204
|
+
//
|
|
205
|
+
// Order matters because the W3C lists techniques by conformance strategy, not by how
|
|
206
|
+
// well they READ as an example. For 1.1.1 the first listed with a sample is ARIA6,
|
|
207
|
+
// whose own example is a navigation landmark — true to the technique, useless as an
|
|
208
|
+
// illustration of a text alternative. Preferring HTML picks H37 (`alt` on `img`)
|
|
209
|
+
// instead. This is one uniform rule applied to every criterion, not a per-criterion
|
|
210
|
+
// choice: within a technology the W3C's own order still decides.
|
|
211
|
+
const TECH_ORDER = ["html", "css", "aria", "client-side-script", "general", "text"];
|
|
212
|
+
const pick = async (ids) => {
|
|
213
|
+
const byTech = new Map();
|
|
214
|
+
for (const id of ids) {
|
|
215
|
+
const dir = techDir(id);
|
|
216
|
+
if (!dir) continue;
|
|
217
|
+
if (!byTech.has(dir)) byTech.set(dir, []);
|
|
218
|
+
byTech.get(dir).push(id);
|
|
219
|
+
}
|
|
220
|
+
const ordered = [...TECH_ORDER.flatMap((t) => byTech.get(t) ?? []), ...ids.filter((id) => !TECH_ORDER.includes(techDir(id)))];
|
|
221
|
+
for (const id of ordered) {
|
|
222
|
+
const doc = await techniqueDoc(id);
|
|
223
|
+
if (!doc) continue;
|
|
224
|
+
const ex = firstCodeExample(doc);
|
|
225
|
+
if (ex) return { id, title: techniqueTitle(doc), ...ex };
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const good = a ? await pick(flattenTechniques(a.sufficient)) : null;
|
|
231
|
+
const bad = a ? await pick(flattenTechniques(a.failure)) : null;
|
|
232
|
+
|
|
233
|
+
criteria[c.sc] = {
|
|
234
|
+
slug: c.slug,
|
|
235
|
+
...(brief ? { brief } : {}),
|
|
236
|
+
...(good ? { good } : {}),
|
|
237
|
+
...(bad ? { bad } : {}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const out = {
|
|
242
|
+
source: "https://github.com/w3c/wcag",
|
|
243
|
+
understanding: "https://www.w3.org/WAI/WCAG22/Understanding/",
|
|
244
|
+
license: "W3C Document License (code samples: W3C Software License)",
|
|
245
|
+
fetchedAt: new Date().toISOString().slice(0, 10),
|
|
246
|
+
criteria,
|
|
247
|
+
};
|
|
248
|
+
mkdirSync(dirname(VENDOR), { recursive: true });
|
|
249
|
+
writeFileSync(VENDOR, JSON.stringify(out, null, 2) + "\n");
|
|
250
|
+
const withGood = Object.values(criteria).filter((x) => x.good).length;
|
|
251
|
+
const withBad = Object.values(criteria).filter((x) => x.bad).length;
|
|
252
|
+
const withBrief = Object.values(criteria).filter((x) => x.brief).length;
|
|
253
|
+
console.log(`build-guidance-wcag --refresh: ${Object.keys(criteria).length} SCs — ${withBrief} briefs, ${withGood} good samples, ${withBad} failure samples → ${VENDOR}`);
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The dataset, from the vendored snapshot. Offline and deterministic. */
|
|
258
|
+
function build() {
|
|
259
|
+
if (!existsSync(VENDOR)) {
|
|
260
|
+
console.error(`build-guidance-wcag: missing ${VENDOR}. Run: node scripts/build-guidance-wcag.mjs --refresh`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
const snap = JSON.parse(readFileSync(VENDOR, "utf8"));
|
|
264
|
+
const core = JSON.parse(readFileSync(join(root, "src", "data", "wcag.json"), "utf8"));
|
|
265
|
+
const titleOf = new Map(core.criteria.map((c) => [c.sc, c]));
|
|
266
|
+
|
|
267
|
+
const entries = [];
|
|
268
|
+
for (const [sc, d] of Object.entries(snap.criteria)) {
|
|
269
|
+
const meta = titleOf.get(sc);
|
|
270
|
+
if (!meta) continue; // not in the shipped AA core
|
|
271
|
+
const summary = d.brief
|
|
272
|
+
? [d.brief.whattodo, d.brief.goal, d.brief.whyitsimportant].filter(Boolean).join(" ")
|
|
273
|
+
: undefined;
|
|
274
|
+
// A `bad`/`good` pair shares ONE `lang`, so the two halves may only be paired when they
|
|
275
|
+
// are the same technology. A CSS failure beside an HTML fix, labelled `html`, would be
|
|
276
|
+
// a snippet the reader cannot trust — so they are emitted separately instead.
|
|
277
|
+
const label = (t) => ({ en: `${t.kind}: ${t.title} (${t.id}).` });
|
|
278
|
+
const examples = [];
|
|
279
|
+
if (d.good && d.bad && d.good.lang === d.bad.lang) {
|
|
280
|
+
examples.push({
|
|
281
|
+
lang: d.good.lang,
|
|
282
|
+
bad: d.bad.code,
|
|
283
|
+
good: d.good.code,
|
|
284
|
+
note: { en: `Good: ${d.good.title} (${d.good.id}). Bad: ${d.bad.title} (${d.bad.id}).` },
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
if (d.good) examples.push({ lang: d.good.lang, good: d.good.code, note: label({ kind: "Good", ...d.good }) });
|
|
288
|
+
if (d.bad) examples.push({ lang: d.bad.lang, bad: d.bad.code, note: label({ kind: "Bad", ...d.bad }) });
|
|
289
|
+
}
|
|
290
|
+
entries.push({
|
|
291
|
+
id: `wcag-${d.slug}`,
|
|
292
|
+
criterionId: sc,
|
|
293
|
+
wcag: [sc],
|
|
294
|
+
title: { en: meta.title, ...(meta.titleFr ? { fr: meta.titleFr } : {}) },
|
|
295
|
+
...(summary ? { summary: { en: summary } } : { summary: {} }),
|
|
296
|
+
examples,
|
|
297
|
+
reference: `https://www.w3.org/WAI/WCAG22/Understanding/${d.slug}.html`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
entries.sort((a, b) => a.criterionId.localeCompare(b.criterionId, "en", { numeric: true }));
|
|
301
|
+
|
|
302
|
+
const out = {
|
|
303
|
+
pack: "wcag",
|
|
304
|
+
source: snap.understanding,
|
|
305
|
+
license: snap.license,
|
|
306
|
+
attribution:
|
|
307
|
+
"Derived from the W3C Techniques for WCAG 2.2 and the Understanding documents (https://github.com/w3c/wcag). WCAG 2.2 © W3C. " +
|
|
308
|
+
"Summaries are the working group's own 'In brief' text; code samples are the techniques' own examples. Generated by scripts/build-guidance-wcag.mjs — never hand-edited.",
|
|
309
|
+
entries,
|
|
310
|
+
};
|
|
311
|
+
return biomeFormat(JSON.stringify(out, null, 2) + "\n", "src/data/guidance/wcag.json");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function main() {
|
|
315
|
+
if (process.argv.includes("--refresh")) await refresh();
|
|
316
|
+
const text = build();
|
|
317
|
+
if (process.argv.includes("--check")) {
|
|
318
|
+
const current = existsSync(OUT) ? readFileSync(OUT, "utf8") : "";
|
|
319
|
+
if (current !== text) {
|
|
320
|
+
console.error("build-guidance-wcag --check: src/data/guidance/wcag.json does not match the vendored W3C source. Re-run: node scripts/build-guidance-wcag.mjs");
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
console.log("build-guidance-wcag --check: src/data/guidance/wcag.json matches the vendored source.");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
mkdirSync(dirname(OUT), { recursive: true });
|
|
327
|
+
writeFileSync(OUT, text);
|
|
328
|
+
const n = JSON.parse(text).entries.length;
|
|
329
|
+
console.log(`build-guidance-wcag: ${n} entries → src/data/guidance/wcag.json`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
main().catch((e) => {
|
|
333
|
+
console.error(e);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
});
|