ultra11y 5.38.0 → 5.38.2

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 (54) 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 +6 -6
  6. package/dist/playwright.mjs +24 -4
  7. package/package.json +1 -1
  8. package/scripts/build-pack-rgaa.mjs +16 -9
  9. package/scripts/build-standards.mjs +2 -2
  10. package/scripts/sync-version.mjs +5 -0
  11. package/scripts/ultra11y.mjs +870 -335
  12. package/scripts/vendor/wcag-2.2-fr.json +1 -1
  13. package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
  14. package/scripts/vendor/wcag-2.2-techniques.json +1 -1
  15. package/scripts/vendor/wcag-2.2-text.json +1 -1
  16. package/scripts/verify-skill-bundle.mjs +6 -0
  17. package/skills/review-a11y/SKILL.md +1 -1
  18. package/skills/review-a11y/scripts/ultra11y.mjs +870 -335
  19. package/skills/ultra11y/SKILL.md +4 -4
  20. package/skills/ultra11y/references/ci.md +1 -1
  21. package/skills/ultra11y/references/criteria.md +2 -2
  22. package/skills/ultra11y/references/judgment.md +2 -2
  23. package/skills/ultra11y/references/mcp.md +1 -1
  24. package/skills/ultra11y/references/pages.md +2 -2
  25. package/skills/ultra11y/references/rgaa-automation.md +7 -7
  26. package/skills/ultra11y/references/runbook.md +27 -17
  27. package/skills/ultra11y/scripts/ultra11y.mjs +870 -335
  28. package/skills/ultra11y/templates/gitlab-ci.yml +25 -7
  29. package/src/adjudicate.ts +148 -20
  30. package/src/audit.ts +22 -1
  31. package/src/check.ts +52 -13
  32. package/src/cli.ts +119 -71
  33. package/src/data/standards/rgaa.json +39 -29
  34. package/src/data/wcag-universe.json +1 -1
  35. package/src/data/wcag.json +2 -1
  36. package/src/grounding.ts +13 -1
  37. package/src/ledger.ts +111 -7
  38. package/src/llm.ts +18 -1
  39. package/src/name.ts +68 -8
  40. package/src/parse/html.ts +16 -0
  41. package/src/parse/jsx-bridge.ts +11 -2
  42. package/src/prd.ts +2 -1
  43. package/src/probes.ts +24 -4
  44. package/src/refute.ts +13 -2
  45. package/src/rules/forms.ts +24 -5
  46. package/src/rules/links.ts +108 -52
  47. package/src/rules/mandatory.ts +9 -1
  48. package/src/rules/presentation.ts +15 -1
  49. package/src/rules/scripts-aria.ts +1 -1
  50. package/src/scan.ts +68 -6
  51. package/src/standards/derive.ts +27 -9
  52. package/src/standards/document.ts +12 -5
  53. package/src/types.ts +1 -1
  54. package/src/verify.ts +114 -23
@@ -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.38.0",
13
+ "version": "5.38.2",
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.38.0",
4
+ "version": "5.38.2",
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.38.0",
4
+ "version": "5.38.2",
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.38.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.38.2";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
package/README.md CHANGED
@@ -25,7 +25,7 @@ Full details, thresholds and off-switches under [Install](#install).</sub>
25
25
 
26
26
  > Audit HTML/CSS/JSX against **WCAG 2.2 AA** accessibility and produce a dated compliance report — or author/review accessible markup without regressions. A [skills.sh](https://skills.sh) agent skill: a deterministic, zero-dependency static engine **plus** the agent's judgment, with `check`/`verify` gates against hallucinated non-conformities. **The central deliverable is the auditor conformance block** — theme, criterion + official wording, test(s), WCAG mapping + level, finding, expected state, verification, `file:line` occurrences — rendered identically by the `report` (compliance doc), the `prd` backlog and the GitHub issues, in the active standard's vocabulary and **in your language** (`--lang auto` follows the conversation/repo). **WCAG is the worldwide core; country standards (RGAA, …) are pluggable in-repo packs.**
27
27
 
28
- ultra11y is built around an honest **division of labour**. Automated tools only catch a fraction of accessibility problems, so the engine does the *mechanical* work — 91 machine-detectable static checks tied to the WCAG 2.2 success criteria — and is explicit about everything it can't decide statically. What it can't, the **AI agent adjudicates** (statically, from the evidence, gated by `verify`/`check`) — not a deferral to a human:
28
+ ultra11y is built around an honest **division of labour**. Automated tools only catch a fraction of accessibility problems, so the engine does the *mechanical* work — 93 machine-detectable static checks tied to the WCAG 2.2 success criteria — and is explicit about everything it can't decide statically. What it can't, the **AI agent adjudicates** (statically, from the evidence, gated by `verify`/`check`) — not a deferral to a human:
29
29
 
30
30
  - **Automatable (engine):** missing `alt`/`lang`/`title`, unlabeled fields, empty links/buttons, icon-only controls, iframes without title, tables without headers, heading-level skips, empty/dangling headings & labels, duplicate ids, invalid/broken ARIA, positive `tabindex`, autoplay/timed-refresh/`blink`/`marquee` media…
31
31
  - **Agent judgment (gated):** alt-text relevance, link purpose in context, reading/tab order, caption accuracy — the agent rules on these via `verify --manual` → `--apply`, each verdict carrying a justification (or a groundable NC), never a silent "conforming".
@@ -316,12 +316,12 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
316
316
 
317
317
  ### What RGAA automation actually covers
318
318
 
319
- The engine's 91 rules are not 91 RGAA criteria: several rules can evidence the same official
319
+ The engine's 93 rules are not 93 RGAA criteria: several rules can evidence the same official
320
320
  test, and a rule can detect a useful symptom without exhausting the criterion's alternatives,
321
321
  exceptions or relevance checks. The generated contract classifies all **258 official RGAA
322
322
  tests**:
323
323
 
324
- - **26 `static` tests across 19 criteria:** `1.1`, `2.1`, `5.7`, `5.8`, `6.2`, `7.1`,
324
+ - **27 `static` tests across 19 criteria:** `1.1`, `2.1`, `5.7`, `5.8`, `6.2`, `7.1`,
325
325
  `8.2`, `8.3`, `8.4`, `8.5`, `8.10`, `9.3`, `10.1`, `10.12`, `11.1`, `11.5`,
326
326
  `11.6`, `11.8`, `11.9`.
327
327
  - **3 `rendered` tests across 3 criteria:** `1.1`, `8.1`, `10.7`.
@@ -331,12 +331,12 @@ tests**:
331
331
  41 receive candidate evidence, with overlap. One additional criterion receives only an
332
332
  advisory signal. A candidate is forwarded to adjudication and never changes a verdict by
333
333
  itself.
334
- - Only **8.5 and 10.1** may earn `C` from a complete silent measurement. For the other
334
+ - Only **8.3, 8.5 and 10.1** may earn `C` from a complete silent measurement. For the other
335
335
  mechanically detectable tests, silence can mean “no subject found” or “an alternative or
336
336
  exception still needs judging”, so it cannot prove conformity.
337
- - The remaining **229 `judgment` tests across 92 criteria** all enter the AI worklist,
337
+ - The remaining **228 `judgment` tests across 92 criteria** all enter the AI worklist,
338
338
  including an apparently absent subject whose `NA` still needs confirmation. Overall,
339
- **104 of 106 criteria require adjudication to earn `C`**.
339
+ **103 of 106 criteria require adjudication to earn `C`**.
340
340
 
341
341
  The number can grow, but the useful target is **more observed, exact failures**, not a larger
342
342
  marketing count. The best next candidates are complete rendered probes for zoom/reflow and
@@ -136,10 +136,30 @@ var COLLECT_SNAPSHOT = `(() => {
136
136
  var PRELUDE = `
137
137
  const __sel = (e) => {
138
138
  if (!e || !e.tagName) return '\u2014';
139
- const t = e.tagName.toLowerCase();
140
- if (e.id) return t + '#' + e.id;
141
- const c = typeof e.className === 'string' ? e.className.trim().split(/\\s+/)[0] : '';
142
- return c ? t + '.' + c : t;
139
+ const esc = (v) => (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(v) : String(v).replace(/[^a-zA-Z0-9_-]/g, '\\\\$&');
140
+ const unique = (s) => { try { return document.querySelectorAll(s).length === 1; } catch { return false; } };
141
+ const short = (n) => {
142
+ const t = n.tagName.toLowerCase();
143
+ if (n.id) return t + '#' + esc(n.id);
144
+ const c = typeof n.className === 'string' ? n.className.trim().split(/\\s+/)[0] : '';
145
+ return c ? t + '.' + esc(c) : t;
146
+ };
147
+ const first = short(e);
148
+ if (unique(first)) return first;
149
+ const parts = [];
150
+ for (let n = e; n && n.tagName; n = n.parentElement) {
151
+ let part = short(n);
152
+ if (n.id && unique(part)) { parts.unshift(part); return parts.join(' > '); }
153
+ const parent = n.parentElement;
154
+ if (parent) {
155
+ const same = Array.from(parent.children).filter((x) => x.tagName === n.tagName);
156
+ if (same.length > 1) part += ':nth-of-type(' + (same.indexOf(n) + 1) + ')';
157
+ }
158
+ parts.unshift(part);
159
+ const path = parts.join(' > ');
160
+ if (unique(path)) return path;
161
+ }
162
+ return parts.join(' > ') || first;
143
163
  };
144
164
  const __vis = (e) => {
145
165
  const r = e.getBoundingClientRect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.38.0",
3
+ "version": "5.38.2",
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",
@@ -221,6 +221,7 @@ const RULE_TO_CRITERIA = {
221
221
  "aria-hidden-focusable": ["7.1"], "redundant-aria": ["7.1"], "nested-interactive": ["7.1"],
222
222
  "cross-prop-drilled-name-lost": ["7.1"],
223
223
  "presentational-children-focusable": ["7.1"], "menuitem-empty-name": ["7.1"],
224
+ "button-empty-name": ["7.1"], "label-in-name-mismatch": ["7.1"],
224
225
  "clickable-noninteractive": ["7.3"], "live-region-conflict": ["7.5"], "status-message-not-assertive": ["7.5"],
225
226
  // Dynamic tier (scan --local): the live-region probe projects onto WCAG 4.1.3 → RGAA 7.5
226
227
  // (status messages) — the WCAG-faithful home. Ara ALSO classifies the source finding under
@@ -233,6 +234,7 @@ const RULE_TO_CRITERIA = {
233
234
  "axe:aria-required-attr": ["7.1"], "axe:aria-required-children": ["7.1"], "axe:aria-required-parent": ["7.1"],
234
235
  "axe:aria-valid-attr": ["7.1"], "axe:aria-valid-attr-value": ["7.1"], "axe:nested-interactive": ["7.1"],
235
236
  "axe:aria-hidden-focus": ["7.1"], "axe:presentation-role-conflict": ["7.1"],
237
+ "axe:button-name": ["7.1"], "axe:input-button-name": ["7.1"],
236
238
  // Theme 8 — document (8.2 valid code, 8.3 default lang, 8.4 lang relevant, 8.5 title, 8.7/8.8 lang changes)
237
239
  "duplicate-id": ["8.2"], "axe:duplicate-id": ["8.2"], "axe:duplicate-id-aria": ["8.2"], "axe:duplicate-id-active": ["8.2"],
238
240
  "html-lang-missing": ["8.3"], "axe:html-has-lang": ["8.3"],
@@ -258,11 +260,11 @@ const RULE_TO_CRITERIA = {
258
260
  "control-label-missing": ["11.1"], "label-for-dangling": ["11.1"], "placeholder-as-label": ["11.1"],
259
261
  "form-field-multiple-labels": ["11.1"], "select-has-option": ["11.1"], "control-name-title-only": ["11.1"],
260
262
  "radio-checkbox-group-ungrouped": ["11.5"], "date-fields-ungrouped": ["11.5"],
261
- "field-purpose-incomplete": ["11.1", "11.13"], "fieldset-legend-missing": ["11.6"], "button-empty-name": ["11.9"],
262
- "autocomplete-token-invalid": ["11.13"], "label-in-name-mismatch": ["11.9"],
263
+ "field-purpose-incomplete": ["11.1", "11.13"], "fieldset-legend-missing": ["11.6"], "form-button-empty-name": ["11.9"],
264
+ "autocomplete-token-invalid": ["11.13"], "form-label-in-name-mismatch": ["11.9"],
263
265
  "error-not-associated": ["11.10"], "aria-invalid-no-description": ["11.10"],
264
266
  "axe:label": ["11.1"], "axe:form-field-multiple-labels": ["11.1"], "axe:select-name": ["11.1"], "axe:label-title-only": ["11.1"],
265
- "axe:autocomplete-valid": ["11.13"], "axe:fieldset": ["11.6"], "axe:input-button-name": ["11.9"], "axe:button-name": ["11.9"],
267
+ "axe:autocomplete-valid": ["11.13"], "axe:fieldset": ["11.6"],
266
268
  // Theme 12 — navigation (12.7 skip link, 12.8 tab order, 12.9 keyboard trap)
267
269
  "skip-link-target-missing": ["12.7"], "axe:skip-link": ["12.7"], "axe:bypass": ["12.7"],
268
270
  "positive-tabindex": ["12.8"], "axe:tabindex": ["12.8"],
@@ -316,15 +318,20 @@ const DECISIVE_RULE_TESTS = {
316
318
  "11.1|control-label-missing": ["1"],
317
319
  "11.6|axe:fieldset": ["1"],
318
320
  "11.6|fieldset-legend-missing": ["1"],
319
- "11.9|axe:button-name": ["1"],
320
- "11.9|axe:input-button-name": ["1"],
321
- "11.9|button-empty-name": ["1"],
322
- "11.9|label-in-name-mismatch": ["2"],
321
+ "11.9|form-button-empty-name": ["1"],
322
+ "11.9|form-label-in-name-mismatch": ["2"],
323
323
  "11.5|radio-checkbox-group-ungrouped": ["1"],
324
324
  "5.7|headers-attr-dangling": ["4"],
325
325
  "5.7|table-scope-invalid": ["2", "3"],
326
326
  "5.8|layout-table-data-markup": ["1"],
327
- "7.1|menuitem-empty-name": ["3"],
327
+ // An absent accessible name fails the prerequisite name/role/value test (7.1.1).
328
+ // Test 7.1.3 asks whether an already-accessible name/role is pertinent; it cannot be the
329
+ // attachment for a component that has no name at all.
330
+ "7.1|menuitem-empty-name": ["1"],
331
+ "7.1|button-empty-name": ["1"],
332
+ "7.1|axe:button-name": ["1"],
333
+ "7.1|axe:input-button-name": ["1"],
334
+ "7.1|label-in-name-mismatch": ["3"],
328
335
  };
329
336
 
330
337
  // Candidate evidence is test-scoped too. The fallback remains all tests only when one rule id
@@ -388,7 +395,7 @@ const CANDIDATE_RULE_TESTS = {
388
395
  "13.8|axe:marquee": ["1"],
389
396
  };
390
397
 
391
- const COMPLETE_BY_SILENCE = new Set(["8.5", "10.1"]);
398
+ const COMPLETE_BY_SILENCE = new Set(["8.3", "8.5", "10.1"]);
392
399
  const renderedTier = (ruleId) =>
393
400
  ruleId.startsWith("rendered-") || ruleId.startsWith("dyn-") || ruleId.startsWith("axe:") ? "rendered" : "static";
394
401
  const mergeTestTier = (current, next) => (current === "static" || next === "static" ? "static" : next);
@@ -664,7 +664,7 @@ const RULE_SC_COVERAGE = {
664
664
  "2.4.3": ["positive-tabindex"],
665
665
  "2.4.7": ["rendered-focus-not-visible"],
666
666
  "2.4.4": ["link-empty-name", "icon-only-control-unnamed"],
667
- "2.5.3": ["label-in-name-mismatch"],
667
+ "2.5.3": ["label-in-name-mismatch", "form-label-in-name-mismatch"],
668
668
  "3.2.2": ["on-input-context-change"],
669
669
  "3.3.8": ["credential-entry-blocked"],
670
670
  "3.1.1": ["html-lang-missing", "document-language-missing", "html-lang-xml-lang-mismatch", "lang-invalid"],
@@ -675,7 +675,7 @@ const RULE_SC_COVERAGE = {
675
675
  "iframe-title-missing", "invalid-aria-role", "aria-ref-missing-id", "redundant-aria",
676
676
  "clickable-noninteractive", "aria-required-children", "aria-hidden-focusable", "nested-interactive", "invalid-aria-attr", "invalid-aria-value", "aria-required-attr", "aria-prohibited-attr",
677
677
  "duplicate-id", "duplicate-attribute", "control-label-missing", "placeholder-as-label", "form-field-multiple-labels",
678
- "select-has-option", "button-empty-name", "icon-only-control-unnamed", "control-name-title-only",
678
+ "select-has-option", "button-empty-name", "form-button-empty-name", "icon-only-control-unnamed", "control-name-title-only",
679
679
  "field-purpose-incomplete", "disabled-context-content", "presentational-children-focusable",
680
680
  "decorative-marked-exposed", "menuitem-empty-name",
681
681
  ],
@@ -41,6 +41,11 @@ edit("src/types.ts", (s) => s.replace(/(export const VERSION = ")[^"]+(";)/, `$1
41
41
  edit("skills/ultra11y/SKILL.md", setVersionField);
42
42
  edit("skills/review-a11y/SKILL.md", setVersionField);
43
43
 
44
+ // GitLab installs the published package rather than consuming the repository checkout. Keep
45
+ // its default exact pin on the release that owns the template, so a later `latest` cannot
46
+ // silently change an old pipeline's audit semantics.
47
+ edit("skills/ultra11y/templates/gitlab-ci.yml", (s) => s.replace(/(ULTRA11Y_VERSION:\s*')[^']+(')/, `$1${version}$2`));
48
+
44
49
  // The plugin manifests — what carries the automatic-review hook. A stale version here is
45
50
  // what makes an installed plugin report the wrong engine, so every harness's manifest is
46
51
  // rewritten (verify-skill-bundle.mjs fails the build if they drift). Codex resolves