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.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.opencode/plugins/ultra11y.js +1 -1
  5. package/README.md +40 -4
  6. package/package.json +3 -1
  7. package/scripts/build-guidance-wcag.mjs +335 -0
  8. package/scripts/build-standards.mjs +290 -1
  9. package/scripts/ultra11y.mjs +3805 -274
  10. package/scripts/vendor/wcag-2.2-fr.json +944 -0
  11. package/scripts/vendor/wcag-2.2-techniques.json +840 -0
  12. package/scripts/vendor/wcag-2.2-text.json +749 -0
  13. package/skills/review-a11y/SKILL.md +1 -1
  14. package/skills/review-a11y/scripts/ultra11y.mjs +3805 -274
  15. package/skills/ultra11y/SKILL.md +13 -4
  16. package/skills/ultra11y/references/guidance.md +61 -7
  17. package/skills/ultra11y/references/harnesses.md +10 -0
  18. package/skills/ultra11y/references/mcp.md +215 -0
  19. package/skills/ultra11y/references/packs.md +8 -0
  20. package/skills/ultra11y/references/standards.md +18 -0
  21. package/skills/ultra11y/scripts/ultra11y.mjs +3805 -274
  22. package/src/adjudicate.ts +9 -15
  23. package/src/adjudication-data.ts +38 -0
  24. package/src/check.ts +51 -1
  25. package/src/cli.ts +6 -2
  26. package/src/config.ts +27 -4
  27. package/src/criteria-view.ts +463 -0
  28. package/src/criteria.ts +14 -11
  29. package/src/data/guidance/wcag.json +1126 -0
  30. package/src/data/wcag.json +1112 -55
  31. package/src/guidance/index.ts +14 -0
  32. package/src/guidance/resolve.ts +71 -0
  33. package/src/mcp/handlers.ts +160 -15
  34. package/src/mcp/prompts.ts +13 -2
  35. package/src/mcp/server.ts +18 -4
  36. package/src/mcp/standards-resources.ts +174 -0
  37. package/src/mcp/tools.ts +140 -6
  38. package/src/method-view.ts +227 -0
  39. package/src/prd.ts +5 -16
  40. package/src/standards/coverage.ts +237 -0
  41. package/src/standards/index.ts +1 -0
  42. package/src/standards/pack.ts +19 -0
  43. package/src/standards/registry.ts +115 -9
  44. package/src/types.ts +24 -1
  45. 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.3.0",
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.3.0",
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.3.0",
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.3.0";
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
- Eleven read tools. `ultra11y_audit` is the entry point:
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 skill's own documentation
262
+ ### Resources — the documentation, and the standards themselves
241
263
 
242
- `SKILL.md` and all 32 `references/*.md` are served under `skill://`, read off
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.0",
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
+ "&nbsp;": " ",
77
+ "&amp;": "&",
78
+ "&lt;": "<",
79
+ "&gt;": ">",
80
+ "&quot;": '"',
81
+ "&apos;": "'",
82
+ "&rsquo;": "’",
83
+ "&lsquo;": "‘",
84
+ "&ldquo;": "“",
85
+ "&rdquo;": "”",
86
+ "&mdash;": "—",
87
+ "&ndash;": "–",
88
+ "&hellip;": "…",
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
+ });