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.
@@ -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.31.1",
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.31.1",
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.31.1",
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.31.1";
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 engine's canonical key. Each country standard ships as an in-repo
294
- **standards pack** — a small JSON that maps the national standard's criteria onto WCAG
295
- success criteria — so the same audit re-keys to any standard:
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 report --in audits/audit-latest.json --standard rgaa # → audits/rgaa-YYYY-MM-DD.md
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`.
@@ -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
- hits.push({ selector: __sel(e), html: __html(e), detail: 'Texte tronqu\xE9/masqu\xE9 sous l\\'espacement de texte WCAG 1.4.12 \u2014 perte de contenu.' });
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 probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, deadline) {
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 (2.4.7)."
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 (hits.length >= 20) break;
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", () => probeFocusVisible(page, "", limits, deadline));
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.probed.push("2.4.7");
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.31.1",
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",