ultra11y 5.31.1 → 5.32.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 +16 -5
- package/dist/playwright.mjs +65 -10
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +482 -133
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +482 -133
- package/skills/ultra11y/SKILL.md +24 -7
- package/skills/ultra11y/references/dynamic.md +3 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +482 -133
- package/src/adjudicate.ts +14 -6
- package/src/annotate.ts +15 -7
- package/src/audit.ts +9 -4
- package/src/auditor.ts +10 -9
- package/src/axe-map.ts +9 -0
- package/src/cli.ts +182 -53
- package/src/coverage.ts +5 -3
- package/src/fix.ts +35 -5
- package/src/messages.ts +5 -2
- package/src/output.ts +55 -14
- package/src/prd.ts +47 -15
- package/src/probes.ts +114 -9
- package/src/report.ts +27 -12
- package/src/sarif.ts +35 -17
- package/src/scan-local.ts +23 -10
- package/src/scan.ts +3 -0
- package/src/standards/document.ts +202 -0
- package/src/standards/pack.ts +17 -0
- package/src/types.ts +68 -1
- package/src/verdict-rules.ts +9 -1
- package/src/verify.ts +127 -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.32.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.32.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.32.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.32.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ node scripts/ultra11y.mjs --help
|
|
|
138
138
|
## Commands
|
|
139
139
|
|
|
140
140
|
```
|
|
141
|
-
ultra11y audit <globs… | -> [--out <dir>] [--include <glob>] [--exclude <glob>] [--ext <list>] [--jsx] [--graph] [--json] [--lang auto|en|fr] [--no-default-excludes]
|
|
141
|
+
ultra11y audit <globs… | -> [--standard <pack>] [--out <dir>] [--include <glob>] [--exclude <glob>] [--ext <list>] [--jsx] [--graph] [--json] [--lang auto|en|fr] [--no-default-excludes]
|
|
142
142
|
ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
|
|
143
143
|
ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] # audit rendered-DOM captures alongside source
|
|
144
144
|
ultra11y audit [--format sarif|github] # CI: code-scanning SARIF, or inline annotations + job summary
|
|
@@ -156,6 +156,7 @@ ultra11y criteria [<sc>] [--list] [--standard <pack> [--theme <N>]] [--generate]
|
|
|
156
156
|
ultra11y judge --in <audit.json> [--standard <pack>] [--max <n>] [--model <id>] [--apply] # adjudicate with a model (ANTHROPIC_API_KEY)
|
|
157
157
|
ultra11y check --report <md> [--standard <pack>] [--quiet] [--json]
|
|
158
158
|
ultra11y verify --report <md> [--standard <pack>] [--semantic] [--apply <verdicts.json>] [--max-verify <n>] [--json]
|
|
159
|
+
ultra11y verify --report <md> [--conformities <ledger|adjudication.json> | --no-conformities] # also put the claimed CONFORMITIES on trial
|
|
159
160
|
ultra11y fix <globs… | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--only <ids>] [--jsx] [--json]
|
|
160
161
|
ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
|
|
161
162
|
ultra11y pack check <pack.json> [--guidance <g.json>] | scaffold # gate an (AI-)authored standards pack
|
|
@@ -290,15 +291,25 @@ Two things worth knowing:
|
|
|
290
291
|
|
|
291
292
|
## Standards packs (RGAA France first; add your country)
|
|
292
293
|
|
|
293
|
-
WCAG 2.2 AA is the
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
WCAG 2.2 AA is the ENGINE's canonical key, because its rules are tied to success criteria and
|
|
295
|
+
a pack criterion is defined as a projection of them. Each country standard ships as an in-repo
|
|
296
|
+
**standards pack** — a small JSON carrying that mapping — and `--standard` re-keys everything
|
|
297
|
+
a reader sees, starting with `audit` itself:
|
|
296
298
|
|
|
297
299
|
```sh
|
|
298
|
-
node scripts/ultra11y.mjs
|
|
300
|
+
node scripts/ultra11y.mjs audit src --standard rgaa # RGAA-titled summary by thématique, findings tagged [8.4]
|
|
301
|
+
node scripts/ultra11y.mjs audit src --standard rgaa --out audits --json # a pack-keyed document: 106 criteria, 13 themes
|
|
302
|
+
node scripts/ultra11y.mjs report --in audits/audit-latest.json --standard rgaa # → audits/rgaa-YYYY-MM-DD.md
|
|
299
303
|
node scripts/ultra11y.mjs criteria --standard rgaa 8.3 # a pack criterion (+ its WCAG SCs)
|
|
300
304
|
```
|
|
301
305
|
|
|
306
|
+
`.ultra11yrc.json { "standard": "rgaa" }` makes it the default for every command. Under a pack,
|
|
307
|
+
no deliverable carries a WCAG cross-reference — report, PRD, per-page sheets, CI annotations
|
|
308
|
+
and SARIF name that standard's criteria and nothing else; the WCAG core travels inside the
|
|
309
|
+
document's `core` field, where the pipeline reads it and no rendering does. The one place the
|
|
310
|
+
mapping is still printed is `criteria --standard rgaa <id>`, which is a reference lookup and
|
|
311
|
+
where a reader goes looking for it.
|
|
312
|
+
|
|
302
313
|
**RGAA 4.1.2** (France) ships as the flagship pack. Section 508 (US), EN 301 549 (EU) and
|
|
303
314
|
others are welcome — adding a country is a single PR (pack JSON + one registration line +
|
|
304
315
|
a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/references/standards.md`.
|
package/dist/playwright.mjs
CHANGED
|
@@ -203,7 +203,10 @@ var TEXT_SPACING_PROBE = `(() => { ${PRELUDE}
|
|
|
203
203
|
const clipped = (s.overflowX === 'hidden' || s.overflowY === 'hidden' || s.overflow === 'hidden') && (e.scrollHeight > e.clientHeight + 2 || e.scrollWidth > e.clientWidth + 2);
|
|
204
204
|
const ellipsis = s.textOverflow === 'ellipsis' && e.scrollWidth > e.clientWidth + 2;
|
|
205
205
|
if (clipped || ellipsis) {
|
|
206
|
-
|
|
206
|
+
// No criterion id in the text: every rendering already names the criterion this finding
|
|
207
|
+
// belongs to, and a hard-coded \xAB 1.4.12 \xBB is a WCAG number appearing inside a deliverable
|
|
208
|
+
// that may be keyed on another standard entirely.
|
|
209
|
+
hits.push({ selector: __sel(e), html: __html(e), detail: 'Texte tronqu\xE9/masqu\xE9 sous l\\'espacement de texte impos\xE9 \u2014 perte de contenu.' });
|
|
207
210
|
}
|
|
208
211
|
if (hits.length >= 20) break;
|
|
209
212
|
}
|
|
@@ -259,6 +262,45 @@ var FOCUS_CHECK_PROBE = `(() => {
|
|
|
259
262
|
const now = [s.outlineStyle, s.outlineWidth, s.outlineColor, s.boxShadow, s.borderColor, s.borderTopWidth, s.borderBottomWidth, s.backgroundColor, s.color, s.textDecorationLine].join('|');
|
|
260
263
|
return { key: key, changed: now !== rec.rest, selector: rec.sel, html: rec.html };
|
|
261
264
|
})()`;
|
|
265
|
+
var FOCUS_OBSCURED_PROBE = `(() => { ${PRELUDE}
|
|
266
|
+
const e = document.activeElement;
|
|
267
|
+
if (!e || e === document.body || e === document.documentElement) return null;
|
|
268
|
+
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
269
|
+
if (!key) return null;
|
|
270
|
+
const r = e.getBoundingClientRect();
|
|
271
|
+
if (r.width < 1 || r.height < 1) return null; // nothing to obscure
|
|
272
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
273
|
+
// Sample a 5\xD75 grid inset by a pixel, keeping only points inside the viewport. A component
|
|
274
|
+
// scrolled off-screen leaves no sampleable point and is NOT reported: out of view is not
|
|
275
|
+
// obscured, and the criterion is about content laid over it.
|
|
276
|
+
const xs = [0.02, 0.25, 0.5, 0.75, 0.98], pts = [];
|
|
277
|
+
for (const fx of xs) for (const fy of xs) {
|
|
278
|
+
const x = r.left + r.width * fx, y = r.top + r.height * fy;
|
|
279
|
+
if (x >= 0 && y >= 0 && x < vw && y < vh) pts.push([x, y]);
|
|
280
|
+
}
|
|
281
|
+
if (!pts.length) return null;
|
|
282
|
+
// The topmost element over a point, for each sampled point. The focused element counts as
|
|
283
|
+
// visible when it \u2014 or anything inside it \u2014 is on top: an icon inside a button is the button
|
|
284
|
+
// being visible, and reading that as occlusion would fail every well-built page.
|
|
285
|
+
let occluder = null;
|
|
286
|
+
for (const [x, y] of pts) {
|
|
287
|
+
const top = document.elementsFromPoint(x, y)[0];
|
|
288
|
+
if (!top) continue;
|
|
289
|
+
if (top === e || e.contains(top)) return null; // some part of it is on top \u2192 pass
|
|
290
|
+
if (!occluder) occluder = top;
|
|
291
|
+
}
|
|
292
|
+
if (!occluder) return null;
|
|
293
|
+
// AUTHOR-CREATED OVERLAY, or nothing. Walk up from the occluder looking for the fixed/sticky
|
|
294
|
+
// ancestor that puts it over the page; without one this is ordinary layout, not obscuring.
|
|
295
|
+
let overlay = null;
|
|
296
|
+
for (let n = occluder; n && n !== document.documentElement; n = n.parentElement) {
|
|
297
|
+
const pos = getComputedStyle(n).position;
|
|
298
|
+
if (pos === 'fixed' || pos === 'sticky') { overlay = n; break; }
|
|
299
|
+
}
|
|
300
|
+
if (!overlay) return null;
|
|
301
|
+
if (overlay.contains(e)) return null; // it is the component's own container
|
|
302
|
+
return { key: key, selector: __sel(e), html: __html(e), overlay: __sel(overlay) };
|
|
303
|
+
})()`;
|
|
262
304
|
var HOVER_SETUP_PROBE = `(() => { ${PRELUDE}
|
|
263
305
|
const out = [];
|
|
264
306
|
let n = 0;
|
|
@@ -282,10 +324,11 @@ function hoverVisibleExpr(id, wantHidden = false) {
|
|
|
282
324
|
const j = JSON.stringify(id);
|
|
283
325
|
return `(() => { const t = document.getElementById(${j}); if (!t) return ${wantHidden ? "true" : "false"}; const s = getComputedStyle(t); const shown = s.display !== 'none' && s.visibility !== 'hidden' && t.getBoundingClientRect().height > 0; return ${wantHidden ? "!shown" : "shown"}; })()`;
|
|
284
326
|
}
|
|
285
|
-
async function
|
|
327
|
+
async function probeFocusRing(page, scope = "", limits = PROBE_DEFAULTS, deadline) {
|
|
286
328
|
const count = await page.evaluate(focusSetupExpr(scope, limits.maxFocusables));
|
|
287
|
-
if (!count) return [];
|
|
329
|
+
if (!count) return { visible: [], obscured: [] };
|
|
288
330
|
const hits = [];
|
|
331
|
+
const obscured = [];
|
|
289
332
|
const seen = /* @__PURE__ */ new Set();
|
|
290
333
|
const limit = tabPressBudget(count, limits);
|
|
291
334
|
let prevKey = null;
|
|
@@ -302,12 +345,22 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
|
|
|
302
345
|
hits.push({
|
|
303
346
|
selector: r.selector,
|
|
304
347
|
html: r.html,
|
|
305
|
-
detail: "Le focus clavier ne produit aucun changement visible (outline/box-shadow/bordure/fond) \u2014 focus non visible
|
|
348
|
+
detail: "Le focus clavier ne produit aucun changement visible (outline/box-shadow/bordure/fond) \u2014 focus non visible."
|
|
306
349
|
});
|
|
307
350
|
}
|
|
308
|
-
if (
|
|
351
|
+
if (obscured.length < 20 && !deadline?.out()) {
|
|
352
|
+
const o = await page.evaluate(FOCUS_OBSCURED_PROBE);
|
|
353
|
+
if (o) {
|
|
354
|
+
obscured.push({
|
|
355
|
+
selector: o.selector,
|
|
356
|
+
html: o.html,
|
|
357
|
+
detail: `Le composant qui re\xE7oit le focus clavier est enti\xE8rement masqu\xE9 par un contenu ajout\xE9 par l'auteur (${o.overlay}) \u2014 il est impossible de voir o\xF9 l'on se trouve au clavier.`
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (hits.length >= 20 && obscured.length >= 20) break;
|
|
309
362
|
}
|
|
310
|
-
return hits;
|
|
363
|
+
return { visible: hits, obscured };
|
|
311
364
|
}
|
|
312
365
|
var NATIVE_SEGMENT_STOPS = {
|
|
313
366
|
date: 5,
|
|
@@ -486,11 +539,13 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
486
539
|
}
|
|
487
540
|
}
|
|
488
541
|
}
|
|
489
|
-
if (want("2.4.7") && canType) {
|
|
490
|
-
const r = await bounded("2.4.7", () =>
|
|
542
|
+
if ((want("2.4.7") || want("2.4.11")) && canType) {
|
|
543
|
+
const r = await bounded("2.4.7", () => probeFocusRing(page, "", limits, deadline));
|
|
491
544
|
if (r) {
|
|
492
|
-
out.focusVisible = r;
|
|
493
|
-
out.
|
|
545
|
+
out.focusVisible = r.visible;
|
|
546
|
+
out.focusObscured = r.obscured;
|
|
547
|
+
if (want("2.4.7")) out.probed.push("2.4.7");
|
|
548
|
+
if (want("2.4.11")) out.probed.push("2.4.11");
|
|
494
549
|
}
|
|
495
550
|
}
|
|
496
551
|
if (want("2.1.2") && canType) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.32.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",
|