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.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/docker/Dockerfile +7 -0
- package/docker/package.json +9 -0
- package/docker/runner.mjs +23 -0
- package/package.json +73 -0
- package/scripts/bench.mjs +151 -0
- package/scripts/build-act-corpus.mjs +143 -0
- package/scripts/build-adjudication.mjs +503 -0
- package/scripts/build-pack-rgaa.mjs +397 -0
- package/scripts/build-standards.mjs +548 -0
- package/scripts/copy-bundle.mjs +39 -0
- package/scripts/import-pack.mjs +107 -0
- package/scripts/sync-engine.mjs +84 -0
- package/scripts/sync-version.mjs +43 -0
- package/scripts/ultra11y.mjs +52739 -0
- package/scripts/vendor/act-testcases.json +12297 -0
- package/scripts/vendor/rgaa/SOURCE.json +8 -0
- package/scripts/vendor/rgaa/criteres.json +4144 -0
- package/scripts/vendor/rgaa/glossaire.json +480 -0
- package/scripts/vendor/wcag-2.2-fr.json +82 -0
- package/scripts/vendor/wcag-2.2-sc-universe.json +950 -0
- package/scripts/vendor/wcag-2.2-sc.json +574 -0
- package/scripts/verify-rgaa-source.mjs +126 -0
- package/scripts/verify-skill-bundle.mjs +91 -0
- package/skills/review-a11y/SKILL.md +123 -0
- package/skills/review-a11y/scripts/ultra11y.mjs +52739 -0
- package/skills/ultra11y/SKILL.md +292 -0
- package/skills/ultra11y/references/act.md +163 -0
- package/skills/ultra11y/references/adjudication.md +599 -0
- package/skills/ultra11y/references/audit.md +100 -0
- package/skills/ultra11y/references/authoring.md +37 -0
- package/skills/ultra11y/references/automation.md +113 -0
- package/skills/ultra11y/references/correction.md +41 -0
- package/skills/ultra11y/references/criteria.md +129 -0
- package/skills/ultra11y/references/cross-file.md +82 -0
- package/skills/ultra11y/references/dynamic.md +152 -0
- package/skills/ultra11y/references/false-positives.md +81 -0
- package/skills/ultra11y/references/fix.md +46 -0
- package/skills/ultra11y/references/focus-and-logic.md +64 -0
- package/skills/ultra11y/references/forbidden-patterns.md +61 -0
- package/skills/ultra11y/references/forms-and-errors.md +147 -0
- package/skills/ultra11y/references/guidance.md +55 -0
- package/skills/ultra11y/references/judgment.md +90 -0
- package/skills/ultra11y/references/media-and-motion.md +116 -0
- package/skills/ultra11y/references/methodology.md +72 -0
- package/skills/ultra11y/references/naming.md +117 -0
- package/skills/ultra11y/references/orchestration.md +44 -0
- package/skills/ultra11y/references/packs.md +193 -0
- package/skills/ultra11y/references/prd.md +104 -0
- package/skills/ultra11y/references/rendered.md +110 -0
- package/skills/ultra11y/references/scale.md +49 -0
- package/skills/ultra11y/references/standards.md +97 -0
- package/skills/ultra11y/references/structure.md +138 -0
- package/skills/ultra11y/references/verify.md +88 -0
- package/skills/ultra11y/references/widgets.md +106 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +52739 -0
- package/src/act.ts +441 -0
- package/src/adjudicate.ts +630 -0
- package/src/aria.ts +230 -0
- package/src/audit.ts +396 -0
- package/src/auditor.ts +399 -0
- package/src/autofill.ts +102 -0
- package/src/axe-map.ts +294 -0
- package/src/baseline.ts +75 -0
- package/src/capture.ts +182 -0
- package/src/check.ts +243 -0
- package/src/cli.ts +1927 -0
- package/src/color.ts +102 -0
- package/src/config.ts +182 -0
- package/src/crawl.ts +75 -0
- package/src/criteria.ts +183 -0
- package/src/data/adjudication.json +770 -0
- package/src/data/guidance/rgaa.json +2395 -0
- package/src/data/standards/rgaa.glossary.json +478 -0
- package/src/data/standards/rgaa.json +3454 -0
- package/src/data/wcag-universe.json +530 -0
- package/src/data/wcag.json +1178 -0
- package/src/discover.ts +203 -0
- package/src/fix/codemods.ts +76 -0
- package/src/fix/diff.ts +64 -0
- package/src/fix/edits.ts +126 -0
- package/src/fix.ts +215 -0
- package/src/gh.ts +214 -0
- package/src/glob.ts +206 -0
- package/src/graph/build.ts +114 -0
- package/src/graph/engine-resolve.ts +0 -0
- package/src/graph/graph.ts +126 -0
- package/src/graph/imports.ts +359 -0
- package/src/graph/resolve.ts +73 -0
- package/src/graph/tsconfig.ts +113 -0
- package/src/grounding.ts +119 -0
- package/src/guidance/index.ts +45 -0
- package/src/guidance/types.ts +36 -0
- package/src/init.ts +113 -0
- package/src/mcp/handlers.ts +388 -0
- package/src/mcp/http.ts +260 -0
- package/src/mcp/prompts.ts +183 -0
- package/src/mcp/protocol.ts +187 -0
- package/src/mcp/resources.ts +136 -0
- package/src/mcp/server.ts +228 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/mcp/tools.ts +347 -0
- package/src/messages.ts +1011 -0
- package/src/name.ts +174 -0
- package/src/orchestrate-templates.ts +231 -0
- package/src/orchestrate.ts +186 -0
- package/src/output.ts +108 -0
- package/src/pack.ts +187 -0
- package/src/parse/html.ts +258 -0
- package/src/parse/jsx-ast.ts +67 -0
- package/src/parse/jsx-bridge.ts +247 -0
- package/src/parse/jsx.ts +19 -0
- package/src/parse/source.ts +75 -0
- package/src/prd.ts +406 -0
- package/src/project-lock.ts +42 -0
- package/src/render.ts +307 -0
- package/src/report.ts +399 -0
- package/src/rules/colors.ts +125 -0
- package/src/rules/cross-registry.ts +264 -0
- package/src/rules/cross-rule.ts +83 -0
- package/src/rules/forms.ts +590 -0
- package/src/rules/frames.ts +33 -0
- package/src/rules/headings.ts +227 -0
- package/src/rules/images.ts +200 -0
- package/src/rules/links.ts +193 -0
- package/src/rules/mandatory.ts +173 -0
- package/src/rules/multimedia.ts +77 -0
- package/src/rules/navigation.ts +190 -0
- package/src/rules/presentation.ts +75 -0
- package/src/rules/registry.ts +57 -0
- package/src/rules/rule.ts +122 -0
- package/src/rules/scripts-aria.ts +636 -0
- package/src/rules/tables.ts +264 -0
- package/src/rules/timing.ts +55 -0
- package/src/sample.ts +176 -0
- package/src/scan-local.ts +866 -0
- package/src/scan.ts +538 -0
- package/src/standards/derive.ts +195 -0
- package/src/standards/index.ts +31 -0
- package/src/standards/pack-rules.ts +111 -0
- package/src/standards/pack.ts +81 -0
- package/src/standards/registry.ts +100 -0
- package/src/standards/types.ts +202 -0
- package/src/standards/validate.ts +544 -0
- package/src/standards/vocabulary.ts +75 -0
- package/src/types.ts +355 -0
- package/src/util.ts +32 -0
- package/src/vendor/codeindex-engine.d.mts +1009 -0
- package/src/vendor/codeindex-engine.mjs +13701 -0
- package/src/vendor/engine.meta.json +9 -0
- package/src/verify.ts +275 -0
- 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.
|