ultra11y 2.22.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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/docker/Dockerfile +7 -0
  4. package/docker/package.json +9 -0
  5. package/docker/runner.mjs +23 -0
  6. package/package.json +73 -0
  7. package/scripts/bench.mjs +151 -0
  8. package/scripts/build-act-corpus.mjs +143 -0
  9. package/scripts/build-adjudication.mjs +503 -0
  10. package/scripts/build-pack-rgaa.mjs +397 -0
  11. package/scripts/build-standards.mjs +548 -0
  12. package/scripts/copy-bundle.mjs +39 -0
  13. package/scripts/import-pack.mjs +107 -0
  14. package/scripts/sync-engine.mjs +84 -0
  15. package/scripts/sync-version.mjs +43 -0
  16. package/scripts/ultra11y.mjs +52739 -0
  17. package/scripts/vendor/act-testcases.json +12297 -0
  18. package/scripts/vendor/rgaa/SOURCE.json +8 -0
  19. package/scripts/vendor/rgaa/criteres.json +4144 -0
  20. package/scripts/vendor/rgaa/glossaire.json +480 -0
  21. package/scripts/vendor/wcag-2.2-fr.json +82 -0
  22. package/scripts/vendor/wcag-2.2-sc-universe.json +950 -0
  23. package/scripts/vendor/wcag-2.2-sc.json +574 -0
  24. package/scripts/verify-rgaa-source.mjs +126 -0
  25. package/scripts/verify-skill-bundle.mjs +91 -0
  26. package/skills/review-a11y/SKILL.md +123 -0
  27. package/skills/review-a11y/scripts/ultra11y.mjs +52739 -0
  28. package/skills/ultra11y/SKILL.md +292 -0
  29. package/skills/ultra11y/references/act.md +163 -0
  30. package/skills/ultra11y/references/adjudication.md +599 -0
  31. package/skills/ultra11y/references/audit.md +100 -0
  32. package/skills/ultra11y/references/authoring.md +37 -0
  33. package/skills/ultra11y/references/automation.md +113 -0
  34. package/skills/ultra11y/references/correction.md +41 -0
  35. package/skills/ultra11y/references/criteria.md +129 -0
  36. package/skills/ultra11y/references/cross-file.md +82 -0
  37. package/skills/ultra11y/references/dynamic.md +152 -0
  38. package/skills/ultra11y/references/false-positives.md +81 -0
  39. package/skills/ultra11y/references/fix.md +46 -0
  40. package/skills/ultra11y/references/focus-and-logic.md +64 -0
  41. package/skills/ultra11y/references/forbidden-patterns.md +61 -0
  42. package/skills/ultra11y/references/forms-and-errors.md +147 -0
  43. package/skills/ultra11y/references/guidance.md +55 -0
  44. package/skills/ultra11y/references/judgment.md +90 -0
  45. package/skills/ultra11y/references/media-and-motion.md +116 -0
  46. package/skills/ultra11y/references/methodology.md +72 -0
  47. package/skills/ultra11y/references/naming.md +117 -0
  48. package/skills/ultra11y/references/orchestration.md +44 -0
  49. package/skills/ultra11y/references/packs.md +193 -0
  50. package/skills/ultra11y/references/prd.md +104 -0
  51. package/skills/ultra11y/references/rendered.md +110 -0
  52. package/skills/ultra11y/references/scale.md +49 -0
  53. package/skills/ultra11y/references/standards.md +97 -0
  54. package/skills/ultra11y/references/structure.md +138 -0
  55. package/skills/ultra11y/references/verify.md +88 -0
  56. package/skills/ultra11y/references/widgets.md +106 -0
  57. package/skills/ultra11y/scripts/ultra11y.mjs +52739 -0
  58. package/src/act.ts +441 -0
  59. package/src/adjudicate.ts +630 -0
  60. package/src/aria.ts +230 -0
  61. package/src/audit.ts +396 -0
  62. package/src/auditor.ts +399 -0
  63. package/src/autofill.ts +102 -0
  64. package/src/axe-map.ts +294 -0
  65. package/src/baseline.ts +75 -0
  66. package/src/capture.ts +182 -0
  67. package/src/check.ts +243 -0
  68. package/src/cli.ts +1927 -0
  69. package/src/color.ts +102 -0
  70. package/src/config.ts +182 -0
  71. package/src/crawl.ts +75 -0
  72. package/src/criteria.ts +183 -0
  73. package/src/data/adjudication.json +770 -0
  74. package/src/data/guidance/rgaa.json +2395 -0
  75. package/src/data/standards/rgaa.glossary.json +478 -0
  76. package/src/data/standards/rgaa.json +3454 -0
  77. package/src/data/wcag-universe.json +530 -0
  78. package/src/data/wcag.json +1178 -0
  79. package/src/discover.ts +203 -0
  80. package/src/fix/codemods.ts +76 -0
  81. package/src/fix/diff.ts +64 -0
  82. package/src/fix/edits.ts +126 -0
  83. package/src/fix.ts +215 -0
  84. package/src/gh.ts +214 -0
  85. package/src/glob.ts +206 -0
  86. package/src/graph/build.ts +114 -0
  87. package/src/graph/engine-resolve.ts +0 -0
  88. package/src/graph/graph.ts +126 -0
  89. package/src/graph/imports.ts +359 -0
  90. package/src/graph/resolve.ts +73 -0
  91. package/src/graph/tsconfig.ts +113 -0
  92. package/src/grounding.ts +119 -0
  93. package/src/guidance/index.ts +45 -0
  94. package/src/guidance/types.ts +36 -0
  95. package/src/init.ts +113 -0
  96. package/src/mcp/handlers.ts +388 -0
  97. package/src/mcp/http.ts +260 -0
  98. package/src/mcp/prompts.ts +183 -0
  99. package/src/mcp/protocol.ts +187 -0
  100. package/src/mcp/resources.ts +136 -0
  101. package/src/mcp/server.ts +228 -0
  102. package/src/mcp/stdio.ts +119 -0
  103. package/src/mcp/tools.ts +347 -0
  104. package/src/messages.ts +1011 -0
  105. package/src/name.ts +174 -0
  106. package/src/orchestrate-templates.ts +231 -0
  107. package/src/orchestrate.ts +186 -0
  108. package/src/output.ts +108 -0
  109. package/src/pack.ts +187 -0
  110. package/src/parse/html.ts +258 -0
  111. package/src/parse/jsx-ast.ts +67 -0
  112. package/src/parse/jsx-bridge.ts +247 -0
  113. package/src/parse/jsx.ts +19 -0
  114. package/src/parse/source.ts +75 -0
  115. package/src/prd.ts +406 -0
  116. package/src/project-lock.ts +42 -0
  117. package/src/render.ts +307 -0
  118. package/src/report.ts +399 -0
  119. package/src/rules/colors.ts +125 -0
  120. package/src/rules/cross-registry.ts +264 -0
  121. package/src/rules/cross-rule.ts +83 -0
  122. package/src/rules/forms.ts +590 -0
  123. package/src/rules/frames.ts +33 -0
  124. package/src/rules/headings.ts +227 -0
  125. package/src/rules/images.ts +200 -0
  126. package/src/rules/links.ts +193 -0
  127. package/src/rules/mandatory.ts +173 -0
  128. package/src/rules/multimedia.ts +77 -0
  129. package/src/rules/navigation.ts +190 -0
  130. package/src/rules/presentation.ts +75 -0
  131. package/src/rules/registry.ts +57 -0
  132. package/src/rules/rule.ts +122 -0
  133. package/src/rules/scripts-aria.ts +636 -0
  134. package/src/rules/tables.ts +264 -0
  135. package/src/rules/timing.ts +55 -0
  136. package/src/sample.ts +176 -0
  137. package/src/scan-local.ts +866 -0
  138. package/src/scan.ts +538 -0
  139. package/src/standards/derive.ts +195 -0
  140. package/src/standards/index.ts +31 -0
  141. package/src/standards/pack-rules.ts +111 -0
  142. package/src/standards/pack.ts +81 -0
  143. package/src/standards/registry.ts +100 -0
  144. package/src/standards/types.ts +202 -0
  145. package/src/standards/validate.ts +544 -0
  146. package/src/standards/vocabulary.ts +75 -0
  147. package/src/types.ts +355 -0
  148. package/src/util.ts +32 -0
  149. package/src/vendor/codeindex-engine.d.mts +1009 -0
  150. package/src/vendor/codeindex-engine.mjs +13701 -0
  151. package/src/vendor/engine.meta.json +9 -0
  152. package/src/verify.ts +275 -0
  153. package/src/wcag.ts +128 -0
@@ -0,0 +1,90 @@
1
+ # Judgment phase (the criteria the AI agent adjudicates)
2
+
3
+ The engine decides the machine-checkable subset; the **AI agent** (Claude running this skill)
4
+ *adjudicates the rest itself*, gated: the **judgment** criteria (alt relevance, link purpose in
5
+ context, reading order) it rules on **statically, from the evidence the engine harvests**; the
6
+ **rendering** criteria (computed contrast, visible focus, zoom/reflow, content-on-hover) it routes
7
+ to the `scan` tier (never source); and it adversarially verifies the detected non-conformities.
8
+ This phase makes each verdict defensible and recorded, never invented, never silently "conforming".
9
+
10
+ ## Adjudication loop (the residual/manual criteria) — `verify --manual`
11
+
12
+ 1. **Harvest the worklist** from the audit's cwd (harvesting re-reads the audited source files):
13
+ ```
14
+ node scripts/ultra11y.mjs verify --report audits/wcag-YYYY-MM-DD.md --in audit.json --manual --out .
15
+ ```
16
+ writes an **ADJUDICATION worklist** — `ADJUDICATE.todo.json` + `ADJUDICATE.md`, one item per
17
+ residual (`manual`) criterion, each **pre-loaded with the harvested evidence**: every image's
18
+ `alt`, every link's text + context, the literal colour pairs, control labels, the heading
19
+ outline, ARIA state, `tabindex`, lang-of-parts.
20
+ 2. **Rule on each item**, filling its `verdict` in `ADJUDICATE.todo.json` (provenance
21
+ `decidedBy: "agent"` is recorded):
22
+ - `C` — conforming, with a `justification` from the evidence;
23
+ - `NC` — non-conforming, with ≥1 **groundable** finding (`file`/`line`/`message`/`snippet`)
24
+ **AND a `normativeRef`** citing the precise failed test of the *active standard* (a WCAG
25
+ technique/SC, or an RGAA test number under `--standard rgaa`). The fold rejects a missing
26
+ `normativeRef`, or one that does not resolve to a real test of the active standard
27
+ (anti-fabrication) — a non-conformity must always name the normative rule it breaks;
28
+ - `NA` — not applicable, with a `justification`;
29
+ - `manual` — undecidable from what is captured, with a `reason`: `"needs-rendered-dom"`
30
+ (decide via `scan`) or `"undecidable"`.
31
+ - `recommendations[]` — a **good practice with no failing normative test** (e.g. "state a
32
+ download link's format/weight", "one `<h1>` per page") is NOT an NC: record it as a
33
+ non-normative recommendation (groundable exactly like an NC, but **no `normativeRef`
34
+ required**). It renders under « Recommandations (non normatives) » and never flips the
35
+ criterion to NC. A purely UX concern is neither an NC nor a recommendation — leave it out.
36
+ 3. **Fold back FAIL-CLOSED**:
37
+ ```
38
+ node scripts/ultra11y.mjs verify --apply ADJUDICATE.todo.json --in audit.json --out .
39
+ ```
40
+ rejects a null verdict, a `C`/`NA` without a `justification`, an `NC` without a groundable
41
+ finding **or without a resolving `normativeRef`**, a `manual` without a `reason`, or any
42
+ uncovered residual criterion. Agent `NC`s become real `agent:<sc>` findings that re-render in
43
+ the report's §2 and re-enter the verify worklist; `report`/`prd` re-render with the adjudicated
44
+ statuses; §5 shrinks to only the still-`manual` items.
45
+
46
+ ### The decision protocol
47
+
48
+ Each residual criterion in `ADJUDICATE.md` is pre-loaded, alongside the harvested evidence,
49
+ with its **decision protocol** (`src/data/adjudication.json`, both languages, covering ALL 52
50
+ criteria the engine cannot decide):
51
+
52
+ - **the decision rule** — what makes this criterion Conforming vs Non-conforming. Not a hint:
53
+ the rule you apply. A criterion handed over with no stated rule is where an audit quietly
54
+ turns into an opinion;
55
+ - **when `NA` is legitimate** — so "not applicable" is a justified verdict rather than an
56
+ escape hatch;
57
+ - **the questions** an auditor asks to get there;
58
+ - **the citable references** — this criterion's W3C techniques and failures, i.e. exactly the
59
+ set a `normativeRef` may come from. `verify --apply` rejects one that does not resolve, so
60
+ proposing the valid ones is what stops an invented citation.
61
+
62
+ The same protocol is published as a standalone page, `references/adjudication.md`, generated
63
+ from that dataset — read it when you want the whole picture rather than one worklist item.
64
+ They are prompts, not verdicts: you still answer from the evidence and record
65
+ `C`/`NC`/`NA`/`manual` (+ a recommendation where a good practice has no failing normative
66
+ test).
67
+ 4. **Rendering required**: a `manual` item marked `needs-rendered-dom` (computed contrast, visible
68
+ focus, 200% zoom, 320px reflow, content-on-hover) is decided on the **render** (the `scan` tier,
69
+ or inspection) — never from the source.
70
+ 5. **Library code (DSFR…)**: a `<Button>`/`<Card>` does not show its HTML in source. Adjudicate on
71
+ the **produced** HTML (see `render` / audit the build), otherwise the verdict is a false
72
+ negative.
73
+
74
+ ## Adversarial verification of the non-conformities — `verify --report`
75
+
76
+ Unchanged: `node scripts/ultra11y.mjs verify --report audits/wcag-YYYY-MM-DD.md` writes `VERIFY.md`
77
+ + `VERIFY.todo.json`, one entry per detected non-conformity, each grounded in the **WCAG success
78
+ criterion's W3C Understanding reference** + techniques. Rule each, opening the file at the cited
79
+ line:
80
+ - `supported` — the non-conformity is real and correctly tied;
81
+ - `partial` — real but the criterion/wording is imprecise;
82
+ - `refuted` — false (the cited element is actually conforming);
83
+ - `unsupported` — the cited element is not enough to decide.
84
+
85
+ Then `node scripts/ultra11y.mjs verify --apply VERIFY.todo.json` is green again (fails on any
86
+ `refuted`/`unsupported`/missing verdict). `--semantic` folds the support-check into the same pass.
87
+
88
+ > For a country standard's own test grid, `criteria --standard rgaa <id>` shows the RGAA tests
89
+ > behind a WCAG SC. The pre-completion checklist stops you concluding too early. The rule that
90
+ > never bends: no residual criterion reaches `C` without a recorded, gated justification.
@@ -0,0 +1,116 @@
1
+ # Media, pointer input and motion
2
+
3
+ The criteria in this page are the ones a source-only audit is least able to settle: what a
4
+ video actually contains, how big a target renders, whether an animation lasts five seconds.
5
+ Each entry says what decides it — you, the `scan` tier, or the engine.
6
+
7
+ ## Time-based media (1.2.x)
8
+
9
+ | Media | What it needs | Level |
10
+ |---|---|---|
11
+ | Audio-only, prerecorded | A text transcript | A (1.2.1) |
12
+ | Video-only, prerecorded | A transcript OR an equivalent audio track | A (1.2.1) |
13
+ | Video with audio, prerecorded | Synchronized captions | A (1.2.2) |
14
+ | Video with audio, prerecorded | Audio description OR a full text alternative | A (1.2.3) |
15
+ | Video with audio, prerecorded | Audio description — a transcript no longer suffices | **AA (1.2.5)** |
16
+ | Any live audio | Real-time captions | AA (1.2.4) |
17
+
18
+ ```html
19
+ <video controls>
20
+ <source src="talk.mp4" type="video/mp4">
21
+ <track kind="captions" src="talk.en.vtt" srclang="en" label="English" default>
22
+ <track kind="descriptions" src="talk.desc.vtt" srclang="en" label="Audio description">
23
+ </video>
24
+ <p><a href="talk-transcript.html">Read the transcript of this talk</a></p>
25
+ ```
26
+
27
+ `kind` matters: `captions` include non-speech sounds and speaker identification;
28
+ `subtitles` are a translation for people who can hear; `descriptions` narrate the picture.
29
+ A `subtitles` track does not satisfy 1.2.2.
30
+
31
+ **Why the engine only recommends here.** A `<video>` without `<track>` may still carry
32
+ captions embedded in the container, and its audio track may be silent. Neither is knowable
33
+ from source, so `media-no-track` is a **recommendation**: 1.2.2 stays yours to adjudicate
34
+ against the real media. Check the file and the player before writing a verdict.
35
+
36
+ Also in scope: a media player's own controls must be keyboard-operable and named (2.1.1,
37
+ 4.1.2) — a custom player is a composite widget, see `references/widgets.md`.
38
+
39
+ ## Autoplay (1.4.2, 2.2.2)
40
+
41
+ Audio that plays automatically for more than 3 seconds needs a mechanism to pause, stop or
42
+ control its volume, independent of the system volume. Native `controls` is that mechanism.
43
+
44
+ ```html
45
+ <video autoplay muted playsinline>…</video> <!-- no audio: 1.4.2 not engaged -->
46
+ <audio src="jingle.mp3" autoplay controls> <!-- ✓ a stop mechanism exists -->
47
+ ```
48
+
49
+ Engine rule: `autoplay-media` — normative only when there is no `controls` and no `muted`;
50
+ the muted-video case is raised as a recommendation, since the source cannot show whether the
51
+ movement lasts the 5 seconds 2.2.2 requires.
52
+
53
+ ## Pointer input (2.5.x)
54
+
55
+ | SC | Requirement | How to satisfy it |
56
+ |---|---|---|
57
+ | **2.5.1** Pointer Gestures | No function requires a multipoint or path-based gesture | Add buttons beside the swipe/pinch: carousel arrows, map zoom controls |
58
+ | **2.5.2** Pointer Cancellation | No function executes on the DOWN event, or it is abortable | Act on `click`/`pointerup`, not `pointerdown`; allow the user to slide off to abort |
59
+ | **2.5.3** Label in Name | Visible label contained in the accessible name | See `references/naming.md` |
60
+ | **2.5.4** Motion Actuation | Motion-triggered functions have a UI equivalent AND can be disabled | Keep a button for "shake to undo"; offer a setting |
61
+ | **2.5.7** Dragging Movements (2.2) | Every drag has a single-pointer alternative | "Move up"/"Move down" buttons beside a drag handle; a numeric input beside a slider |
62
+ | **2.5.8** Target Size (2.2) | Targets are at least 24×24 CSS px | Or spaced so 24px circles do not overlap; inline links in text are exempt |
63
+
64
+ ```html
65
+ <!-- ✓ drag remains, but is never the only way -->
66
+ <li draggable="true">
67
+ Task one
68
+ <button aria-label="Move “Task one” up">↑</button>
69
+ <button aria-label="Move “Task one” down">↓</button>
70
+ </li>
71
+ ```
72
+
73
+ 2.5.8 is decided on the render — `scan` measures it through axe's `target-size` rule.
74
+
75
+ ## Orientation, reflow, spacing, zoom (1.3.4, 1.4.4, 1.4.10, 1.4.12)
76
+
77
+ All four are decided on the rendered page (`scan --runtime local` probes zoom, spacing and
78
+ overflow). What to check:
79
+
80
+ - **1.3.4 Orientation** — the content works in both portrait and landscape, unless the
81
+ orientation is genuinely essential (a piano keyboard, a cheque to photograph). A CSS
82
+ `transform` or a blocking orientation media query is the usual culprit.
83
+ - **1.4.4 Resize Text** — at 200% text zoom nothing is lost, clipped or overlapped. A
84
+ `meta viewport` blocking zoom fails outright (engine rule: `meta-viewport-zoom-block`).
85
+ - **1.4.10 Reflow** — at **320 CSS px** wide (the equivalent of 400% zoom on a 1280px
86
+ viewport) no two-dimensional scrolling is required, except for content that inherently
87
+ needs it: maps, data tables, diagrams, code.
88
+ - **1.4.12 Text Spacing** — forcing line height 1.5×, paragraph spacing 2×, letter spacing
89
+ 0.12× and word spacing 0.16× loses no content. Fixed-height containers are what break.
90
+
91
+ ## Motion and vestibular safety (2.3.1, 2.2.2)
92
+
93
+ ```css
94
+ /* Respect the OS-level preference — a one-line win that costs nothing */
95
+ @media (prefers-reduced-motion: reduce) {
96
+ *, *::before, *::after {
97
+ animation-duration: 0.01ms !important;
98
+ animation-iteration-count: 1 !important;
99
+ transition-duration: 0.01ms !important;
100
+ scroll-behavior: auto !important;
101
+ }
102
+ }
103
+ ```
104
+
105
+ `prefers-reduced-motion` is not itself a WCAG 2.2 AA requirement, but large parallax,
106
+ auto-playing motion and full-page transitions trigger vestibular disorders, and honouring it
107
+ is how you avoid the 2.2.2 problem entirely. 2.3.1 (nothing flashes more than three times a
108
+ second) is decided on the render.
109
+
110
+ ## What the engine settles, and what stays yours
111
+
112
+ | Decided by the engine | Decided by `scan` | Yours to adjudicate |
113
+ |---|---|---|
114
+ | `autoplay` with no stop mechanism; `meta viewport` blocking zoom; `blink`/`marquee`; timed `meta refresh` | Zoom 200%, reflow 320px, text spacing, target size, flashing | Whether captions/audio description/transcripts exist and are accurate (1.2.x) |
115
+ | Missing `<track>` — as a recommendation, not a failure | — | Whether a gesture, a drag or a motion trigger has an alternative (2.5.1/2.5.2/2.5.4/2.5.7) |
116
+ | — | — | Whether an orientation is genuinely essential (1.3.4) |
@@ -0,0 +1,72 @@
1
+ # Methodology & report format
2
+
3
+ ## Per-criterion statuses
4
+
5
+ - **C** — conforming (every applicable test passes).
6
+ - **NC** — non-conforming (at least one test fails; a finding cites `file:line`).
7
+ - **NA** — not applicable (no element in scope is concerned — justified).
8
+ - **To assess (manual)** — a criterion the engine cannot decide on its own: the AI agent
9
+ adjudicates the *judgment* ones itself (`verify --manual`, gated); the *rendering* ones are
10
+ decided by the `scan` tier. Only the still-undecidable residue stays listed here.
11
+
12
+ ## Pass rate
13
+
14
+ Conformance rate = **conforming criteria ÷ applicable criteria × 100**.
15
+
16
+ The engine only computes the **automatic static-check pass rate**, over the small
17
+ machine-decidable subset it can actually decide: `C ÷ (C + NC)`. Because WCAG success
18
+ criteria are coarser than the engine's rules, that denominator is deliberately small —
19
+ it is **not** a full conformance rate. Full WCAG 2.2 AA conformance requires completing
20
+ the "to assess" criteria by hand.
21
+
22
+ ## Non-conformity priorities
23
+
24
+ - 🔴 **Blocking** — prevents access to content/function (e.g. missing alt, unlabeled
25
+ field, empty link).
26
+ - 🟠 **Major** — high impact but workaroundable (heading order, contrast, invisible focus).
27
+ - 🟡 **Minor** — light friction (missing caption, redundant ARIA).
28
+
29
+ ## The division of labour (static / rendering / judgment)
30
+
31
+ ultra11y is honest about what a static analyzer can decide:
32
+
33
+ - **Automatable (static)** — decided by the engine: missing alt/lang/title, unlabeled
34
+ fields, `iframe` without a title, empty links/buttons, tables without headers, heading
35
+ skips, duplicate `id`s, invalid/broken ARIA, positive `tabindex`, autoplay…
36
+ - **Needs rendering** — computed contrast (1.4.3), focus visible (2.4.7), zoom/reflow
37
+ (1.4.4/1.4.10), content on hover/focus (1.4.13). **Out of the engine**: decided by the `scan`
38
+ tier (axe-core in a real browser), flagged as residual risk until then.
39
+ - **Agent judgment** — alt relevance (1.1.1), link purpose in context (2.4.4), reading/tab
40
+ order, navigation consistency, caption accuracy…: the AI agent adjudicates these itself from
41
+ the evidence the engine harvests (`verify --manual`, gated), never silently "conforming".
42
+
43
+ See the full table of the 55 WCAG 2.2 AA success criteria in `references/criteria.md`.
44
+
45
+ **Preliminary findings.** A finding raised on a `.vue`/`.svelte`/`.astro` source template (or
46
+ library-rendered JSX) carries `preliminary: true` in the `--json` output, and the run adds a
47
+ `scope.sourceTemplate` (or `scope.rendered`) caveat: the static parse cannot see slot/dynamic
48
+ content, so the verdict is provisional — confirm against the rendered DOM or refute it
49
+ (`references/rendered.md`, `references/false-positives.md`). It is never silently treated as final.
50
+
51
+ ## Report format (`report`)
52
+
53
+ `audits/wcag-YYYY-MM-DD.md` has 5 sections: (1) synthesis by WCAG guideline
54
+ (C/NC/NA/to assess), (2) non-conformities by priority — one **auditor conformance block**
55
+ per NC criterion (theme, criterion + official wording, test(s), WCAG mapping + level,
56
+ finding, expected state, verification, `file:line` occurrences), the SAME block `prd` and
57
+ `--gh-issues` emit (see `references/prd.md`), (3) conforming criteria, (4) justified
58
+ not-applicable criteria, (5) criteria to assess manually.
59
+
60
+ ## Worldwide: WCAG core, country standards as packs
61
+
62
+ WCAG 2.2 Level AA is the engine's canonical key — the worldwide standard. A country
63
+ standard (France's RGAA, the US Section 508, the EU's EN 301 549) is a pluggable in-repo
64
+ **pack** that maps its criteria onto WCAG success criteria; `report --standard rgaa` (and
65
+ `criteria`/`check`/`verify`/`prd --standard <pack>`) re-key the deliverable for that
66
+ standard (`rgaa-YYYY-MM-DD.md`). The WCAG report is the canonical, gated one; a pack report
67
+ is a derived view. See `references/standards.md`.
68
+
69
+ **International equivalence (version-accurate)**: Section 508 incorporates WCAG 2.0 AA;
70
+ EN 301 549 v3.2.1 references WCAG 2.1 and v4 references WCAG 2.2; AODA references WCAG 2.0
71
+ AA. A WCAG 2.2 AA audit therefore covers these standards' web requirements at their
72
+ respective WCAG versions.
@@ -0,0 +1,117 @@
1
+ # Accessible names — how one is computed, and how to get it right
2
+
3
+ Most of what an audit calls "4.1.2" is a naming problem. This page is the reference for
4
+ what a name IS, so a verdict rests on the computation rather than on the markup's appearance.
5
+
6
+ ## The precedence chain (accname), highest first
7
+
8
+ A control's accessible name is the FIRST of these that yields non-empty text:
9
+
10
+ | # | Source | Notes |
11
+ |---|---|---|
12
+ | 1 | `aria-labelledby` | Wins over everything, including visible content. Concatenates the text of every id, in the order listed. A dangling id contributes nothing — an all-dangling list leaves the element unnamed. |
13
+ | 2 | `aria-label` | Replaces the visible text entirely. This is what breaks 2.5.3 (see below). |
14
+ | 3 | Native host semantics | `<label for>` / wrapping `<label>` for form fields, `alt` for `<img>`, `<legend>` for `<fieldset>`, `<caption>` for `<table>`, `<figcaption>` for `<figure>`, `value` for `<input type="submit">`. |
15
+ | 4 | Text content | For elements whose role allows "name from content": button, link, heading, menuitem, tab, option… NOT for a `<div>` or an `<input>`. |
16
+ | 5 | `title` | Last resort. Hover-only: absent on touch, unevenly announced. A name that exists but should not be the plan. |
17
+
18
+ Two consequences worth stating explicitly, because both produce confident wrong verdicts:
19
+
20
+ - **A name that exists is not a name that works.** `aria-label="Button"` satisfies "has a
21
+ name" and helps nobody. Judge relevance, not presence.
22
+ - **The chain stops at the first hit.** `<button aria-label="Close">Save</button>` is named
23
+ "Close". The visible word "Save" never enters the computation.
24
+
25
+ ## 2.5.3 Label in Name — the rule voice control depends on
26
+
27
+ When a control shows visible text, that text must be **contained in** the accessible name.
28
+ A speech-input user says what they see; if the name differs, the command silently does nothing.
29
+
30
+ ```html
31
+ <!-- ✗ the spoken "click Send" matches nothing -->
32
+ <button aria-label="Submit form">Send</button>
33
+
34
+ <!-- ✓ the visible text is contained, and comes first -->
35
+ <button aria-label="Send message to support">Send</button>
36
+
37
+ <!-- ✓ best: no override at all — the text names the button -->
38
+ <button>Send</button>
39
+ ```
40
+
41
+ Rule of thumb: if you find yourself writing `aria-label` on an element that already shows
42
+ text, you are usually about to break 2.5.3. Prefer `aria-describedby` for the extra detail.
43
+ Engine rule: `label-in-name-mismatch`.
44
+
45
+ ## Visually-hidden text — the CSS that actually works
46
+
47
+ Naming an icon control with text only screen readers reach is the native-first answer.
48
+ `display:none` and `visibility:hidden` remove the text from the accessibility tree too;
49
+ `text-indent:-9999px` breaks in RTL; `font-size:0` is unreliable.
50
+
51
+ ```css
52
+ /* The only clipping recipe that survives every engine, and stays focusable. */
53
+ .visually-hidden {
54
+ position: absolute;
55
+ width: 1px;
56
+ height: 1px;
57
+ margin: -1px;
58
+ padding: 0;
59
+ overflow: hidden;
60
+ clip-path: inset(50%);
61
+ white-space: nowrap;
62
+ border: 0;
63
+ }
64
+ /* Skip links: visible again once focused. */
65
+ .visually-hidden:focus-visible {
66
+ position: static;
67
+ width: auto;
68
+ height: auto;
69
+ clip-path: none;
70
+ white-space: normal;
71
+ }
72
+ ```
73
+
74
+ ## Naming non-text content
75
+
76
+ ```html
77
+ <!-- Informative image: describe the MEANING in context, not the picture -->
78
+ <img src="chart.png" alt="Sales up 25% in Q3 2025">
79
+
80
+ <!-- Decorative image: an explicit empty alt, never a missing one -->
81
+ <img src="divider.png" alt="">
82
+
83
+ <!-- Icon control: the name describes the ACTION, not the glyph -->
84
+ <button aria-label="Delete this comment"><svg aria-hidden="true" focusable="false">…</svg></button>
85
+
86
+ <!-- Inline SVG treated as an image -->
87
+ <svg role="img" aria-labelledby="logo-title"><title id="logo-title">Acme</title>…</svg>
88
+
89
+ <!-- Decorative SVG: hide it and make sure it is not a tab stop in IE-era engines -->
90
+ <svg aria-hidden="true" focusable="false">…</svg>
91
+
92
+ <!-- Figure: the caption is visible text AND the accessible name -->
93
+ <figure>
94
+ <img src="plan.png" alt="Ground floor: reception, then three meeting rooms along a corridor">
95
+ <figcaption>Ground floor plan</figcaption>
96
+ </figure>
97
+
98
+ <!-- Abbreviation: expand on first use in text; title alone is hover-only -->
99
+ <p>The <abbr title="World Wide Web Consortium">W3C</abbr> publishes WCAG.</p>
100
+ ```
101
+
102
+ **Charts.** An `alt` naming the chart ("Sales chart") is not an alternative — it names the
103
+ container and drops the data. Either put the figures in the alt, or point at an equivalent
104
+ table nearby. Engine rule: `chart-no-accessible-name`.
105
+
106
+ ## What the engine settles, and what stays yours
107
+
108
+ | Decided by the engine | Yours to adjudicate |
109
+ |---|---|
110
+ | Name **absent** (`img-alt-missing`, `button-empty-name`, `link-empty-name`, `control-label-missing`, `iframe-title-missing`, `icon-only-control-unnamed`) | Whether the name is **relevant** (1.1.1, 2.4.4, 2.4.6) — the engine cannot read meaning |
111
+ | Name provided only by `title` (`control-name-title-only`, `img-alt-missing.title-only` as a recommendation) | Whether an `alt=""` image is genuinely decorative |
112
+ | Visible text not contained in the name (`label-in-name-mismatch`) | Whether an icon font makes the DOM text invisible (then 2.5.3 does not apply) |
113
+ | Names prohibited by the role (`aria-prohibited-attr`) | Whether two same-named links share a destination (2.4.4) |
114
+
115
+ Cross-file: under `--graph` the engine resolves a name passed as a prop into a component,
116
+ and flags one that gets lost on the way (`cross-prop-drilled-name-lost`). See
117
+ `references/cross-file.md`.
@@ -0,0 +1,44 @@
1
+ # Orchestration — fanning the judgment phases out
2
+
3
+ The two judgment phases are per-item worklists: `ADJUDICATE.todo.json` holds one entry per
4
+ residual criterion, `VERIFY.todo.json` one per detected non-conformity. Neither has
5
+ cross-item dependencies, so both fan out cleanly across subagents — and both work exactly as
6
+ well done sequentially. This is a wall-clock optimization, never a correctness requirement.
7
+
8
+ `orchestrate` emits the orchestration from the worklists that CURRENTLY exist, with absolute
9
+ paths and the real item ids baked in:
10
+
11
+ ```
12
+ node scripts/ultra11y.mjs orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list]
13
+ ```
14
+
15
+ ## Route by harness
16
+
17
+ | Your harness | How to run each judgment phase |
18
+ |---|---|
19
+ | Has the Workflow tool | `orchestrate --run <RUN> --phase <p>`, then run `<RUN>/orchestration/<p>.workflow.mjs`. Subagents RETURN verdict fragments; you fold them into the worklist, then `verify --apply` as usual. |
20
+ | Subagents but no Workflow tool | Same `orchestrate`; dispatch one subagent per batch following `<RUN>/orchestration/agents/<role>.md` (the workflow script lists the batches and their prompts). One writer: you fold the results in. |
21
+ | Eco mode, or no subagents | `orchestrate --run <RUN> --eco` → follow `<RUN>/orchestration/RUNBOOK.md` sequentially, playing each role yourself. Correctness-identical; only wall-clock differs. |
22
+
23
+ ## The rules that do not bend
24
+
25
+ - **Subagents never write.** Every emitted contract ends with the one-writer rule, and the
26
+ fail-closed `--apply` fold always stays with you, the orchestrator. Parallel writers to one
27
+ worklist is how a verdict silently disappears.
28
+ - **The gates are harness-independent.** `check` and `verify --apply` behave identically
29
+ whether one agent or twelve produced the verdicts, so a fan-out can never buy a greener
30
+ result than the sequential path.
31
+ - **Re-run `orchestrate` whenever a worklist changes.** Emission is deterministic and
32
+ idempotent, so re-running is free and keeps the batches aligned with reality.
33
+ - `--phase <p>` before its worklist exists fails, and names the command that produces it.
34
+
35
+ ## Splitting the work sensibly
36
+
37
+ An adjudication batch should hold criteria that share context — the media criteria together,
38
+ the form criteria together — so each subagent reads one part of the codebase rather than all
39
+ of it. The emitted contracts already group this way; if you dispatch by hand, keep it.
40
+
41
+ A subagent adjudicating a criterion needs the same three things you do: the harvested
42
+ evidence, the decision rule (`references/adjudication.md`), and the reminder that a
43
+ non-conformity must cite a normative test that resolves. Point each contract at the relevant
44
+ knowledge reference rather than restating it — a paraphrase drifts.
@@ -0,0 +1,193 @@
1
+ # Pluggable standards packs — runtime loading + AI ingestion, gated
2
+
3
+ WCAG 2.2 AA is the engine's canonical key. A **standards pack** is a localized criterion
4
+ set (RGAA, Section 508, EN 301 549…) that maps each of its criteria onto WCAG success
5
+ criteria. RGAA ships built-in; you can add more **without rebuilding** the engine.
6
+
7
+ ## Load an external pack at runtime
8
+
9
+ A pack is plain JSON conforming to `StandardPack` (see `references/standards.md` and
10
+ `CONTRIBUTING.md`). Point the CLI at one — no install, no rebuild:
11
+
12
+ ```
13
+ node scripts/ultra11y.mjs audit "src/**/*.tsx" --graph --pack ./packs/section508.json
14
+ node scripts/ultra11y.mjs report --in audit.json --standard section508 --pack ./packs/section508.json
15
+ ```
16
+
17
+ `--pack` accepts a JSON file, a directory holding `pack.json` (+ optional `glossary.json`
18
+ / `guidance.json`), or a comma-separated list. Or declare it once in a `.ultra11yrc.json`
19
+ at the project root:
20
+
21
+ ```json
22
+ { "packs": ["./packs/section508.json"], "guidance": ["./packs/section508.guidance.json"], "standard": "section508" }
23
+ ```
24
+
25
+ Every pack is validated **before** it is registered. An invalid pack is a hard error
26
+ (never a silent skip): the runtime loader, the `pack check` command, and the AI-ingestion
27
+ gate all share the one validator, so the rules below hold everywhere. A runtime pack whose
28
+ `key` collides with a **built-in or already-loaded** standard (e.g. `rgaa`) is rejected —
29
+ pass `--override` to deliberately replace it.
30
+
31
+ ## `pack check` — the anti-hallucination gate
32
+
33
+ ```
34
+ node scripts/ultra11y.mjs pack check ./packs/rgaa.json
35
+ node scripts/ultra11y.mjs pack check ./packs/rgaa.json --guidance ./packs/rgaa.guidance.json
36
+ node scripts/ultra11y.mjs pack scaffold > ./packs/mypack.json # a blank, valid skeleton to fill
37
+ ```
38
+
39
+ `pack check` fails (exit 1) on any error: a missing required field, a `key` that collides
40
+ with the reserved core `wcag`, an `idPattern` that won't compile, a theme `count` that
41
+ disagrees with its criteria, a criterion whose `wcag` SC is **not a recognized WCAG
42
+ success criterion**, and — with `--guidance` — a guidance entry whose `criterionId`
43
+ doesn't resolve to a real pack criterion, or whose code example won't parse. This is what
44
+ makes an AI-authored pack trustworthy: the model proposes, the deterministic gate refuses
45
+ fabrication.
46
+
47
+ **SC classification runs against the full WCAG 2.x universe** — every real 2.x success
48
+ criterion at every level (A/AA/AAA) plus the removed ones, vendored from the same W3C
49
+ source as the AA core (never invented, no id ever special-cased):
50
+
51
+ - **core** (in the WCAG 2.2 AA core) — the normal case; the engine can audit it.
52
+ - **out-of-core** (a real SC outside the AA core, e.g. an AAA criterion) or **removed**
53
+ (the obsolete `4.1.1 Parsing`) — accepted with a *warning*. At report/derive time a pack
54
+ criterion whose mapping is ENTIRELY out-of-core/removed is classed **out of engine
55
+ scope**: status `manual` with a dedicated out-of-scope justification (`outOfScope: true`
56
+ from `derivePackResults`), never a silent NA and never a fabricated verdict. RGAA 8.1
57
+ (doctype → the removed 4.1.1) is the shipped example.
58
+ - **unknown** (a well-formed id that never existed — `9.9.9`) — **rejected** (error).
59
+
60
+ **Pack locales are free-form.** `LocaleString` keys are BCP-47-ish tags (`fr`, `en`,
61
+ `pt-BR`, `nl-BE`…) validated by shape only — a pack may be authored in any language,
62
+ independent of the CLI's own `--lang fr|en` UI frame. The pack's `defaultLocale` also
63
+ feeds the CLI's `--lang auto` fallback chain (used when it is `fr`/`en` and neither an
64
+ explicit `--lang` nor a repo `<html lang>` signal decided the output language).
65
+
66
+ ## AI-assisted ingestion of any rule source (the primary path)
67
+
68
+ To turn an external rule source — the RGAA SocialGouv/etalab rule packs, or any other
69
+ national standard — into something ultra11y can use, drive this loop:
70
+
71
+ 1. **Read** the source (its criteria list, and its concrete good/bad implementation
72
+ rules). The fuzzy mapping `rule → criterion → WCAG SC` is yours to do — the model is
73
+ far better at it than a regex.
74
+ 2. **Draft** three artifacts: the `StandardPack` JSON (if the standard isn't already
75
+ built-in), a guidance dataset (`references/guidance.md`), and a list of *proposed*
76
+ new detectors (only the machine-checkable ones — see the honesty rule in
77
+ `references/guidance.md`).
78
+ 3. **Gate**: run `pack check <pack.json> --guidance <guidance.json>`. Fix every error.
79
+ A bogus criterion id misses; a bogus SC is rejected; an unparseable example fails.
80
+ 4. **Iterate** until green, then load it with `--pack` / `.ultra11yrc.json`.
81
+
82
+ An optional deterministic scaffolder, `scripts/import-pack.mjs`, pre-fills a *draft*
83
+ guidance dataset from a SocialGouv-style rules directory (frontmatter + before/after code
84
+ blocks + reference link), leaving any unresolved `criterionId` as `null` for you to
85
+ complete; it never finalizes — `pack check` is always the gate. See the script header for
86
+ usage.
87
+
88
+ ## Pack-only detection: declarative `rules` + normativity `overrides`
89
+
90
+ A pack can go beyond re-keying WCAG findings: it can ship its **own** detection and
91
+ **re-normativize** a finding within its own projection — *without forking the engine*. Both
92
+ mechanisms are pure data (no JavaScript plugins — a `--pack`-loaded pack stays fully
93
+ validatable), and both are enforced by the same `validatePack` / `pack check` gate.
94
+
95
+ ### `rules` — a bounded declarative matcher
96
+
97
+ Each rule matches source elements and emits a namespaced `pack:<key>:<id>` finding that
98
+ projects onto the pack criterion it reports under (via the same `appliesTo`/`ruleMatches`
99
+ machinery as engine findings). Rules run **after** the core engine rules and **never**
100
+ affect the WCAG core verdict — they surface only in the pack's own report.
101
+
102
+ ```json
103
+ {
104
+ "rules": [{
105
+ "id": "download-link-format",
106
+ "criterion": "6.1",
107
+ "wcag": ["2.4.4"],
108
+ "severity": "mineur",
109
+ "advisory": true,
110
+ "match": {
111
+ "tag": "a",
112
+ "attrs": [{ "name": "href", "op": "matches", "value": "\\.(pdf|docx?|zip)(\\?|#|$)" }],
113
+ "text": { "op": "lacks", "value": "(pdf|docx?|zip|\\d+\\s*(ko|mo|go))" }
114
+ },
115
+ "message": { "en": "…", "fr": "…" },
116
+ "remediation": { "en": "…", "fr": "…" }
117
+ }]
118
+ }
119
+ ```
120
+
121
+ The shipped RGAA pack uses exactly this rule (built by `scripts/build-pack-rgaa.mjs`, never
122
+ hand-edited): an **advisory** flag on download links whose visible text omits the file
123
+ format/weight — the DSFR auditor recommendation under criterion 6.1. Advisory ⇒ it renders
124
+ as a recommendation and **never** makes 6.1 non-conformant. Its `message`/`remediation` pair
125
+ is **localized**: the finding carries both `en` and `fr`, so a `--lang fr` RGAA report renders
126
+ the French recommendation text (not the English bake).
127
+
128
+ **The matcher vocabulary is deliberately CAPPED** at what RGAA needs today:
129
+
130
+ | Field | Meaning |
131
+ |---|---|
132
+ | `tag` | intrinsic tag (case-insensitive) |
133
+ | `attrs[]` | `{ name, op, value? }` — `op` ∈ `present` \| `absent` \| `equals` \| `matches` (regex, ReDoS-guarded, case-insensitive) |
134
+ | `text` | `{ op, value }` — `op` ∈ `matches` \| `lacks` over the element's visible text (regex, case-insensitive) |
135
+ | `has[]` / `lacks[]` | descendant conditions (each a match node); **nesting ≤ 3 levels** |
136
+ | `scope` | `page` (full document only) \| `fragment` (default) |
137
+
138
+ Anything more expressive — sibling/positional combinators, arbitrary CSS selectors,
139
+ cross-file resolution — is **out of scope on purpose**: it belongs in a core WCAG-keyed
140
+ engine rule instead, where every standard benefits. `pack check` rejects a rule whose
141
+ criterion doesn't exist, whose SC isn't a real WCAG criterion, whose regex is unsafe/won't
142
+ compile, whose has/lacks nest too deep, or whose `message`/`remediation` is missing `en`
143
+ or `fr`. It also rejects a **match node with no condition** (`{}` — it would fire on every
144
+ element) and a match node carrying an **unknown key** (a typo like `{ tgo: "a" }` is a hard
145
+ error, never silently ignored): every node must carry ≥1 of `tag`/`attrs`/`text`/`has`/`lacks`.
146
+ The **ReDoS guard** on every regex rejects the classic single-quantified-atom shape
147
+ (`(a+)+`), an overlapping alternation under a quantifier (`(a|a)*`), and a nested quantifier
148
+ over a group (`(ab+)+`) — while still accepting an anchored criterion grammar like `(\.\d+)*`
149
+ and a non-quantified extension alternation like `(pdf|docx?|zip)`.
150
+
151
+ ### `overrides` — re-normativize within a projection
152
+
153
+ ```json
154
+ { "overrides": { "pack:rgaa:some-rule": { "advisory": false, "severity": "majeur" } } }
155
+ ```
156
+
157
+ `overrides` is a map keyed by a finding's `ruleId` (a core engine rule, or a pack rule).
158
+ Applied **only** in `derivePackResults`, it flips a finding's normativity
159
+ (advisory↔normative) and/or re-grades its severity **inside that pack's projection** — the
160
+ core WCAG result is never mutated. This is the precise *"WCAG can differ from RGAA"*
161
+ mechanism: a standard can treat as normative something WCAG leaves advisory (or vice
162
+ versa) without changing the canonical engine verdict.
163
+
164
+ ### `sampleMethodology` — the standard's required page kinds
165
+
166
+ A country standard defines its audit over a **normative page sample** (see
167
+ `references/audit.md`). A pack declares the page KINDS a real audit of it must cover; the
168
+ mechanics (a project's actual `sample.pages`) live in `.ultra11yrc.json`, never in the pack.
169
+
170
+ ```json
171
+ { "sampleMethodology": {
172
+ "requiredKinds": [
173
+ { "id": "accueil", "label": { "fr": "Page d'accueil" }, "keywords": ["accueil", "home"] },
174
+ { "id": "mentions-legales", "label": { "fr": "Mentions légales" }, "keywords": ["mentions", "legal"] },
175
+ { "id": "declaration-accessibilite", "label": { "fr": "Déclaration d'accessibilité" }, "keywords": ["declaration", "accessibilite"] }
176
+ ] } }
177
+ ```
178
+
179
+ `sample check` fuzzy-matches (accent-insensitive) each required kind's `keywords` against the
180
+ configured pages' name/notes/url and reports the missing kinds. It is purely **advisory**
181
+ (the sample is opt-in, the missing kinds are guidance not a gate), so a malformed
182
+ `sampleMethodology` is a validator **warning** and the field is ignored — never a hard
183
+ failure that blocks the pack from deriving reports. RGAA ships the reference list (accueil,
184
+ contact, mentions légales, déclaration d'accessibilité, plan du site, aide, authentification,
185
+ pages représentatives + éléments transverses).
186
+
187
+ ## Gate-compatibility note (id grammar)
188
+
189
+ `check` and `verify` recognize a pack's criterion ids in a rendered report by building
190
+ their citation regex FROM the pack's own `idPattern` (already validated compilable by
191
+ `pack check`) — not a single fixed shape. RGAA's 2-segment `<n>.<n>` (`8.3`) and a
192
+ hypothetical Section 508 `E<n>.<n>` (`E205.4`) are both fully gate-compatible out of the
193
+ box; any `idPattern` a pack declares works, with zero engine changes.