ultra11y 5.23.0 → 5.25.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.
Files changed (38) 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 +2 -2
  6. package/package.json +1 -1
  7. package/scripts/build-pack-rgaa.mjs +68 -2
  8. package/scripts/ultra11y.mjs +934 -41
  9. package/scripts/vendor/rgaa/SOURCE.json +4 -3
  10. package/scripts/vendor/rgaa/methodologies.json +260 -0
  11. package/scripts/verify-rgaa-source.mjs +23 -6
  12. package/skills/review-a11y/SKILL.md +1 -1
  13. package/skills/review-a11y/scripts/ultra11y.mjs +934 -41
  14. package/skills/ultra11y/SKILL.md +12 -3
  15. package/skills/ultra11y/references/ci.md +1 -1
  16. package/skills/ultra11y/references/criteria.md +1 -1
  17. package/skills/ultra11y/references/judgment.md +28 -2
  18. package/skills/ultra11y/references/mcp.md +1 -1
  19. package/skills/ultra11y/references/orchestration.md +5 -0
  20. package/skills/ultra11y/scripts/ultra11y.mjs +934 -41
  21. package/src/adjudicate-subjects.ts +165 -3
  22. package/src/adjudicate.ts +215 -23
  23. package/src/adjudication-data.ts +31 -0
  24. package/src/cli.ts +51 -2
  25. package/src/criteria-view.ts +18 -8
  26. package/src/criteria.ts +17 -1
  27. package/src/data/standards/rgaa.json +486 -1
  28. package/src/data/wcag.json +4 -1
  29. package/src/llm.ts +1 -1
  30. package/src/messages.ts +42 -0
  31. package/src/orchestrate-templates.ts +5 -4
  32. package/src/rules/presentation.ts +165 -2
  33. package/src/standards/coverage.ts +1 -1
  34. package/src/standards/derive.ts +13 -1
  35. package/src/standards/pack.ts +6 -0
  36. package/src/standards/types.ts +17 -0
  37. package/src/standards/validate.ts +30 -0
  38. package/src/types.ts +1 -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": "5.23.0",
13
+ "version": "5.25.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.23.0",
4
+ "version": "5.25.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.23.0",
4
+ "version": "5.25.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.23.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.25.0";
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 — 78 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 — 81 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".
@@ -226,7 +226,7 @@ from auditing out of memory. `cwd` is optional on all five; it selects whose pac
226
226
  `ultra11y_method` is the one to call first. It partitions the standard into evidence tiers —
227
227
  `source`, `cross-file`, `rendered-page`, `browser`, `judgment`, `out-of-scope` — from the
228
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**.
229
+ For RGAA it reports that **56 of 106 criteria declare that no engine rule can evidence them**.
230
230
  That is the standard saying they are yours, and a plan that hides it reads as coverage that
231
231
  does not exist.
232
232
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.23.0",
3
+ "version": "5.25.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",
@@ -6,7 +6,8 @@
6
6
  // of its criteria onto WCAG success criteria (bare SC ids). The WCAG↔rule coverage
7
7
  // lives in the core (scripts/build-standards.mjs). The RGAA content is Licence
8
8
  // Ouverte / Etalab 2.0 — see NOTICE. The official source is vendored under
9
- // scripts/vendor/rgaa/ so the build is reproducible offline.
9
+ // scripts/vendor/rgaa/ (criteres.json, glossaire.json, methodologies.json) so the
10
+ // build is reproducible offline.
10
11
  // node scripts/build-pack-rgaa.mjs # build from the vendored source
11
12
  // node scripts/build-pack-rgaa.mjs --offline # alias (the source is always local)
12
13
  // node scripts/build-pack-rgaa.mjs --fetch # refresh the vendored source from DINUM, then build
@@ -76,6 +77,22 @@ const flattenNode = (v) => {
76
77
  };
77
78
  const toArr = (v) => flattenNode(v);
78
79
 
80
+ // The official méthodologie de test for one criterion, keyed by the criterion's OWN test
81
+ // numbers so it renders directly under the test it explains: `{ "1": "1. Retrouver…", … }`.
82
+ // DINUM's file is flat and keyed by full test id ("11.2.1"), so this is the re-key.
83
+ //
84
+ // Returns undefined when the criterion has none, and the field is then absent from the pack
85
+ // rather than present-and-empty: `methodology` is optional on PackCriterion, and an empty
86
+ // object would make every consumer test for two shapes instead of one.
87
+ function methodologyOf(methodologies, id, tests) {
88
+ const out = {};
89
+ for (const k of Object.keys(tests || {})) {
90
+ const text = methodologies[`${id}.${k}`];
91
+ if (typeof text === "string" && text.trim()) out[k] = text.trim();
92
+ }
93
+ return Object.keys(out).length ? { methodology: out } : undefined;
94
+ }
95
+
79
96
  // crude HTML -> plaintext for glossary bodies
80
97
  const deHtml = (s) =>
81
98
  s
@@ -245,6 +262,12 @@ const RULE_TO_CRITERIA = {
245
262
  async function main() {
246
263
  const criteres = await source("criteres.json");
247
264
  const glossaire = await source("glossaire.json");
265
+ // THE OFFICIAL TEST METHODOLOGY — the third file DINUM publishes beside the other two,
266
+ // keyed by TEST id ("11.2.1"), and the one thing an adjudicator was never given. The
267
+ // criterion's wording says WHAT is required; this says HOW it is tested, step by step, in
268
+ // the referential's own words. Vendored like the rest so it reaches CI, an offline run and
269
+ // a model prompt without a network call — see `methodology` in src/standards/types.ts.
270
+ const methodologies = await source("methodologies.json");
248
271
 
249
272
  const glossary = {};
250
273
  for (const e of glossaire.glossary) glossary[slug(e.title)] = { title: e.title, body: deHtml(e.body) };
@@ -267,6 +290,7 @@ async function main() {
267
290
  title: { fr: criterium.title },
268
291
  titlePlain: { fr: plain(criterium.title) },
269
292
  tests: criterium.tests || {},
293
+ ...(methodologyOf(methodologies, id, criterium.tests) ?? {}),
270
294
  techniques: [...techniques],
271
295
  ...(criterium.technicalNote ? { technicalNote: toArr(criterium.technicalNote) } : {}),
272
296
  ...(criterium.particularCases ? { particularCases: toArr(criterium.particularCases) } : {}),
@@ -310,7 +334,10 @@ async function main() {
310
334
  // (link explicitness, WCAG 2.4.4). ADVISORY: it surfaces as a recommendation in the RGAA
311
335
  // projection and NEVER makes 6.1 non-conformant. This proves a pack ships its OWN
312
336
  // detection without forking the engine (see skills/ultra11y/references/packs.md).
313
- const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|epub|mp3|mp4|avi|mov";
337
+ // Kept in step with the engine instead of retyped: two hand-maintained copies of a rule-id
338
+ // list are two chances for the pack to claim an instrument the engine does not ship.
339
+ const PRESENTATIONAL_RULE_IDS = ["presentational-element", "presentational-attribute", "presentational-spacing"];
340
+ const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|epub|mp3|mp4|avi|mov";
314
341
  const rules = [
315
342
  {
316
343
  id: "download-link-format",
@@ -423,6 +450,28 @@ async function main() {
423
450
  c.appliesTo = { ruleIds: [...new Set([...c.appliesTo.ruleIds, `pack:rgaa:${rule.id}`])].sort() };
424
451
  }
425
452
 
453
+ // ---- RGAA 10.1 — the engine's presentational-markup rules, and their normativity ----
454
+ //
455
+ // 10.1's three tests are « these must be absent », over lists the RGAA closes itself: the
456
+ // glossary entry « Présentation de l'information » names the forbidden elements and
457
+ // attributes. The engine ships the instruments (src/rules/presentation.ts); the pack says
458
+ // they decide THIS criterion, and re-normativizes them.
459
+ //
460
+ // They ship ADVISORY because `<center>` is obsolete HTML rather than a WCAG failure —
461
+ // nothing about it breaks assistive technology, and reporting a WCAG 1.3.1 non-conformity
462
+ // for it would be wrong. Under the RGAA it is a plain failure of test 10.1.1, so the pack
463
+ // flips it here. This is what `overrides` is for, and it is the first use of it.
464
+ //
465
+ // Before this, 10.1 had NO instrument and inherited `readingOrder` through WCAG 1.3.2 — a
466
+ // subject that answers a different question — so it reached the adjudicator with an empty
467
+ // harvest. Measured on run 32508717451 (Sonnet, RGAA, 3 passes): the model ruled `C` three
468
+ // times, the gate refused all three for citing nothing, and 10.1 was the ONE criterion of
469
+ // the 106 left « à évaluer », on the run and on every page.
470
+ const presentational = criteria.find((x) => x.id === "10.1");
471
+ if (!presentational) throw new Error("build-pack-rgaa: RGAA 10.1 is missing from the referential");
472
+ presentational.appliesTo = { ruleIds: [...new Set([...presentational.appliesTo.ruleIds, ...PRESENTATIONAL_RULE_IDS])].sort() };
473
+ const overrides = Object.fromEntries(PRESENTATIONAL_RULE_IDS.map((id) => [id, { advisory: false, severity: "majeur" }]));
474
+
426
475
  const pack = {
427
476
  key: "rgaa",
428
477
  name: "RGAA",
@@ -437,6 +486,9 @@ async function main() {
437
486
  source: "https://github.com/DISIC/accessibilite.numerique.gouv.fr",
438
487
  attribution: "RGAA 4.1.2 © DINUM (Direction interministérielle du numérique) — Licence Ouverte / Etalab 2.0",
439
488
  idPattern: "^\\d+\\.\\d+$",
489
+ // Where DINUM publishes each criterion. Cited by the adjudication brief so a reader can
490
+ // reach the normative page — the vendored text above stays the authority either way.
491
+ criterionUrl: "https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#{id}",
440
492
  // Auditor-display vocabulary (FR): the nouns an RGAA auditor reads. Rendered by the
441
493
  // `prd` auditor block + GitHub issues; see src/standards/vocabulary.ts.
442
494
  vocabulary: {
@@ -505,6 +557,7 @@ async function main() {
505
557
  },
506
558
  ],
507
559
  rules,
560
+ overrides,
508
561
  themes,
509
562
  criteria,
510
563
  };
@@ -546,9 +599,22 @@ async function main() {
546
599
  throw new Error(`build-pack-rgaa: ${stringified.length} normative text(s) stringified to "[object Object]" — extend flattenNode: ${stringified.join(", ")}`);
547
600
  }
548
601
 
602
+ // THE METHODOLOGY TABLE MUST NOT GO STALE EITHER. A methodology whose test id does not
603
+ // exist in the referential is a curation mistake upstream OR a re-key bug here, and it
604
+ // would ship as dead weight in every bundle. A test with no methodology is legitimate
605
+ // (DINUM does not document every one), so that is counted, not refused.
606
+ const testIds = new Set();
607
+ for (const c of criteria) for (const k of Object.keys(c.tests ?? {})) testIds.add(`${c.id}.${k}`);
608
+ const orphans = Object.keys(methodologies).filter((k) => !testIds.has(k));
609
+ if (orphans.length) {
610
+ throw new Error(`build-pack-rgaa: ${orphans.length} methodology(ies) name a test the referential does not have: ${orphans.join(", ")}`);
611
+ }
612
+ const documented = criteria.reduce((n, c) => n + Object.keys(c.methodology ?? {}).length, 0);
613
+
549
614
  const noWcag = criteria.filter((c) => c.wcag.length === 0).map((c) => c.id);
550
615
  console.log(`build-pack-rgaa: ${themes.length} themes, ${criteria.length} criteria → src/data/standards/rgaa.json`);
551
616
  console.log(`build-pack-rgaa: glossary ${Object.keys(glossary).length} entries → src/data/standards/rgaa.glossary.json`);
617
+ console.log(`build-pack-rgaa: official test methodology on ${documented}/${testIds.size} tests`);
552
618
  console.log(`build-pack-rgaa: criteria with no WCAG mapping (pack-local): ${noWcag.length ? noWcag.join(", ") : "none"} (${noWcag.length})`);
553
619
  }
554
620