a11y-loop 0.1.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 +22 -0
- package/README.md +409 -0
- package/THIRD-PARTY-NOTICES.md +32 -0
- package/package.json +51 -0
- package/skill/a11y-loop/SKILL.md +332 -0
- package/skill/a11y-loop/evals/evals.json +168 -0
- package/skill/a11y-loop/evals/trigger-evals.json +20 -0
- package/skill/a11y-loop/references/ai-failure-modes.md +272 -0
- package/skill/a11y-loop/references/apg-patterns.md +264 -0
- package/skill/a11y-loop/references/manual-testing.md +224 -0
- package/skill/a11y-loop/references/wcag22-quick-ref.md +224 -0
- package/src/cli.js +207 -0
- package/src/commands/audit.js +125 -0
- package/src/commands/contrast.js +141 -0
- package/src/commands/diff.js +65 -0
- package/src/lib/axe-runner.js +400 -0
- package/src/lib/browser-utils.js +221 -0
- package/src/lib/checks/dialog.js +341 -0
- package/src/lib/checks/div-button.js +87 -0
- package/src/lib/checks/focus-visible.js +296 -0
- package/src/lib/checks/keyboard.js +235 -0
- package/src/lib/checks/link-text.js +83 -0
- package/src/lib/checks/reduced-motion.js +139 -0
- package/src/lib/checks/reflow.js +101 -0
- package/src/lib/checks/target-size.js +128 -0
- package/src/lib/contrast-math.js +189 -0
- package/src/lib/diff.js +118 -0
- package/src/lib/finding.js +164 -0
- package/src/lib/fingerprint.js +0 -0
- package/src/lib/format/checklist.js +281 -0
- package/src/lib/format/human.js +175 -0
- package/src/lib/format/json.js +139 -0
- package/src/lib/format/sarif.js +111 -0
- package/src/lib/serve.js +189 -0
- package/src/lib/suggest-color.js +169 -0
- package/src/lib/wcag-map.js +271 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCAG success-criterion metadata, and the mapping from axe-core tags to it.
|
|
3
|
+
*
|
|
4
|
+
* Why a table rather than deriving everything from tags: axe tags tell you the
|
|
5
|
+
* SC number and (redundantly) a level, but not the criterion's NAME, and the
|
|
6
|
+
* name is what makes a finding triageable by a human. The table below is the
|
|
7
|
+
* authority for name / level / first version; tags supply the SC number.
|
|
8
|
+
*
|
|
9
|
+
* Counts, per the WCAG 2.2 Recommendation: 31 Level A + 24 Level AA = 55
|
|
10
|
+
* criteria in the AA conformance set, 86 in total. 4.1.1 Parsing was REMOVED in
|
|
11
|
+
* WCAG 2.2 and is deliberately absent — a tool still reporting "duplicate id"
|
|
12
|
+
* as a WCAG failure is citing a criterion that no longer exists.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
/** @type {Record<string, {name:string, level:'A'|'AA'|'AAA', minVersion:'2.0'|'2.1'|'2.2'}>} */
|
|
20
|
+
export const SC_TABLE = {
|
|
21
|
+
// ---- Principle 1: Perceivable ----
|
|
22
|
+
'1.1.1': { name: 'Non-text Content', level: 'A', minVersion: '2.0' },
|
|
23
|
+
'1.2.1': { name: 'Audio-only and Video-only (Prerecorded)', level: 'A', minVersion: '2.0' },
|
|
24
|
+
'1.2.2': { name: 'Captions (Prerecorded)', level: 'A', minVersion: '2.0' },
|
|
25
|
+
'1.2.3': {
|
|
26
|
+
name: 'Audio Description or Media Alternative (Prerecorded)',
|
|
27
|
+
level: 'A',
|
|
28
|
+
minVersion: '2.0',
|
|
29
|
+
},
|
|
30
|
+
'1.2.4': { name: 'Captions (Live)', level: 'AA', minVersion: '2.0' },
|
|
31
|
+
'1.2.5': { name: 'Audio Description (Prerecorded)', level: 'AA', minVersion: '2.0' },
|
|
32
|
+
'1.2.6': { name: 'Sign Language (Prerecorded)', level: 'AAA', minVersion: '2.0' },
|
|
33
|
+
'1.2.7': {
|
|
34
|
+
name: 'Extended Audio Description (Prerecorded)',
|
|
35
|
+
level: 'AAA',
|
|
36
|
+
minVersion: '2.0',
|
|
37
|
+
},
|
|
38
|
+
'1.2.8': { name: 'Media Alternative (Prerecorded)', level: 'AAA', minVersion: '2.0' },
|
|
39
|
+
'1.2.9': { name: 'Audio-only (Live)', level: 'AAA', minVersion: '2.0' },
|
|
40
|
+
'1.3.1': { name: 'Info and Relationships', level: 'A', minVersion: '2.0' },
|
|
41
|
+
'1.3.2': { name: 'Meaningful Sequence', level: 'A', minVersion: '2.0' },
|
|
42
|
+
'1.3.3': { name: 'Sensory Characteristics', level: 'A', minVersion: '2.0' },
|
|
43
|
+
'1.3.4': { name: 'Orientation', level: 'AA', minVersion: '2.1' },
|
|
44
|
+
'1.3.5': { name: 'Identify Input Purpose', level: 'AA', minVersion: '2.1' },
|
|
45
|
+
'1.3.6': { name: 'Identify Purpose', level: 'AAA', minVersion: '2.1' },
|
|
46
|
+
'1.4.1': { name: 'Use of Color', level: 'A', minVersion: '2.0' },
|
|
47
|
+
'1.4.2': { name: 'Audio Control', level: 'A', minVersion: '2.0' },
|
|
48
|
+
'1.4.3': { name: 'Contrast (Minimum)', level: 'AA', minVersion: '2.0' },
|
|
49
|
+
'1.4.4': { name: 'Resize Text', level: 'AA', minVersion: '2.0' },
|
|
50
|
+
'1.4.5': { name: 'Images of Text', level: 'AA', minVersion: '2.0' },
|
|
51
|
+
'1.4.6': { name: 'Contrast (Enhanced)', level: 'AAA', minVersion: '2.0' },
|
|
52
|
+
'1.4.7': { name: 'Low or No Background Audio', level: 'AAA', minVersion: '2.0' },
|
|
53
|
+
'1.4.8': { name: 'Visual Presentation', level: 'AAA', minVersion: '2.0' },
|
|
54
|
+
'1.4.9': { name: 'Images of Text (No Exception)', level: 'AAA', minVersion: '2.0' },
|
|
55
|
+
'1.4.10': { name: 'Reflow', level: 'AA', minVersion: '2.1' },
|
|
56
|
+
'1.4.11': { name: 'Non-text Contrast', level: 'AA', minVersion: '2.1' },
|
|
57
|
+
'1.4.12': { name: 'Text Spacing', level: 'AA', minVersion: '2.1' },
|
|
58
|
+
'1.4.13': { name: 'Content on Hover or Focus', level: 'AA', minVersion: '2.1' },
|
|
59
|
+
|
|
60
|
+
// ---- Principle 2: Operable ----
|
|
61
|
+
'2.1.1': { name: 'Keyboard', level: 'A', minVersion: '2.0' },
|
|
62
|
+
'2.1.2': { name: 'No Keyboard Trap', level: 'A', minVersion: '2.0' },
|
|
63
|
+
'2.1.3': { name: 'Keyboard (No Exception)', level: 'AAA', minVersion: '2.0' },
|
|
64
|
+
'2.1.4': { name: 'Character Key Shortcuts', level: 'A', minVersion: '2.1' },
|
|
65
|
+
'2.2.1': { name: 'Timing Adjustable', level: 'A', minVersion: '2.0' },
|
|
66
|
+
'2.2.2': { name: 'Pause, Stop, Hide', level: 'A', minVersion: '2.0' },
|
|
67
|
+
'2.2.3': { name: 'No Timing', level: 'AAA', minVersion: '2.0' },
|
|
68
|
+
'2.2.4': { name: 'Interruptions', level: 'AAA', minVersion: '2.0' },
|
|
69
|
+
'2.2.5': { name: 'Re-authenticating', level: 'AAA', minVersion: '2.0' },
|
|
70
|
+
'2.2.6': { name: 'Timeouts', level: 'AAA', minVersion: '2.1' },
|
|
71
|
+
'2.3.1': { name: 'Three Flashes or Below Threshold', level: 'A', minVersion: '2.0' },
|
|
72
|
+
'2.3.2': { name: 'Three Flashes', level: 'AAA', minVersion: '2.0' },
|
|
73
|
+
'2.3.3': { name: 'Animation from Interactions', level: 'AAA', minVersion: '2.1' },
|
|
74
|
+
'2.4.1': { name: 'Bypass Blocks', level: 'A', minVersion: '2.0' },
|
|
75
|
+
'2.4.2': { name: 'Page Titled', level: 'A', minVersion: '2.0' },
|
|
76
|
+
'2.4.3': { name: 'Focus Order', level: 'A', minVersion: '2.0' },
|
|
77
|
+
'2.4.4': { name: 'Link Purpose (In Context)', level: 'A', minVersion: '2.0' },
|
|
78
|
+
'2.4.5': { name: 'Multiple Ways', level: 'AA', minVersion: '2.0' },
|
|
79
|
+
'2.4.6': { name: 'Headings and Labels', level: 'AA', minVersion: '2.0' },
|
|
80
|
+
'2.4.7': { name: 'Focus Visible', level: 'AA', minVersion: '2.0' },
|
|
81
|
+
'2.4.8': { name: 'Location', level: 'AAA', minVersion: '2.0' },
|
|
82
|
+
'2.4.9': { name: 'Link Purpose (Link Only)', level: 'AAA', minVersion: '2.0' },
|
|
83
|
+
'2.4.10': { name: 'Section Headings', level: 'AAA', minVersion: '2.0' },
|
|
84
|
+
'2.4.11': { name: 'Focus Not Obscured (Minimum)', level: 'AA', minVersion: '2.2' },
|
|
85
|
+
'2.4.12': { name: 'Focus Not Obscured (Enhanced)', level: 'AAA', minVersion: '2.2' },
|
|
86
|
+
'2.4.13': { name: 'Focus Appearance', level: 'AAA', minVersion: '2.2' },
|
|
87
|
+
'2.5.1': { name: 'Pointer Gestures', level: 'A', minVersion: '2.1' },
|
|
88
|
+
'2.5.2': { name: 'Pointer Cancellation', level: 'A', minVersion: '2.1' },
|
|
89
|
+
'2.5.3': { name: 'Label in Name', level: 'A', minVersion: '2.1' },
|
|
90
|
+
'2.5.4': { name: 'Motion Actuation', level: 'A', minVersion: '2.1' },
|
|
91
|
+
'2.5.5': { name: 'Target Size (Enhanced)', level: 'AAA', minVersion: '2.1' },
|
|
92
|
+
'2.5.6': { name: 'Concurrent Input Mechanisms', level: 'AAA', minVersion: '2.1' },
|
|
93
|
+
'2.5.7': { name: 'Dragging Movements', level: 'AA', minVersion: '2.2' },
|
|
94
|
+
'2.5.8': { name: 'Target Size (Minimum)', level: 'AA', minVersion: '2.2' },
|
|
95
|
+
|
|
96
|
+
// ---- Principle 3: Understandable ----
|
|
97
|
+
'3.1.1': { name: 'Language of Page', level: 'A', minVersion: '2.0' },
|
|
98
|
+
'3.1.2': { name: 'Language of Parts', level: 'AA', minVersion: '2.0' },
|
|
99
|
+
'3.1.3': { name: 'Unusual Words', level: 'AAA', minVersion: '2.0' },
|
|
100
|
+
'3.1.4': { name: 'Abbreviations', level: 'AAA', minVersion: '2.0' },
|
|
101
|
+
'3.1.5': { name: 'Reading Level', level: 'AAA', minVersion: '2.0' },
|
|
102
|
+
'3.1.6': { name: 'Pronunciation', level: 'AAA', minVersion: '2.0' },
|
|
103
|
+
'3.2.1': { name: 'On Focus', level: 'A', minVersion: '2.0' },
|
|
104
|
+
'3.2.2': { name: 'On Input', level: 'A', minVersion: '2.0' },
|
|
105
|
+
'3.2.3': { name: 'Consistent Navigation', level: 'AA', minVersion: '2.0' },
|
|
106
|
+
'3.2.4': { name: 'Consistent Identification', level: 'AA', minVersion: '2.0' },
|
|
107
|
+
'3.2.5': { name: 'Change on Request', level: 'AAA', minVersion: '2.0' },
|
|
108
|
+
'3.2.6': { name: 'Consistent Help', level: 'A', minVersion: '2.2' },
|
|
109
|
+
'3.3.1': { name: 'Error Identification', level: 'A', minVersion: '2.0' },
|
|
110
|
+
'3.3.2': { name: 'Labels or Instructions', level: 'A', minVersion: '2.0' },
|
|
111
|
+
'3.3.3': { name: 'Error Suggestion', level: 'AA', minVersion: '2.0' },
|
|
112
|
+
'3.3.4': { name: 'Error Prevention (Legal, Financial, Data)', level: 'AA', minVersion: '2.0' },
|
|
113
|
+
'3.3.5': { name: 'Help', level: 'AAA', minVersion: '2.0' },
|
|
114
|
+
'3.3.6': { name: 'Error Prevention (All)', level: 'AAA', minVersion: '2.0' },
|
|
115
|
+
'3.3.7': { name: 'Redundant Entry', level: 'A', minVersion: '2.2' },
|
|
116
|
+
'3.3.8': { name: 'Accessible Authentication (Minimum)', level: 'AA', minVersion: '2.2' },
|
|
117
|
+
'3.3.9': { name: 'Accessible Authentication (Enhanced)', level: 'AAA', minVersion: '2.2' },
|
|
118
|
+
|
|
119
|
+
// ---- Principle 4: Robust ----
|
|
120
|
+
// 4.1.1 Parsing was removed in WCAG 2.2 and is intentionally not listed.
|
|
121
|
+
'4.1.2': { name: 'Name, Role, Value', level: 'A', minVersion: '2.0' },
|
|
122
|
+
'4.1.3': { name: 'Status Messages', level: 'AA', minVersion: '2.1' },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** WCAG 2.2 A/AA conformance-set size, for honest denominators in reports. */
|
|
126
|
+
export const CONFORMANCE_SET = Object.freeze({
|
|
127
|
+
total: 86,
|
|
128
|
+
levelA: 31,
|
|
129
|
+
levelAA: 24,
|
|
130
|
+
aaSet: 55,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Turn an axe `wcagNNN` tag into a dotted SC number.
|
|
135
|
+
* The first two digits are principle and guideline; everything after is the
|
|
136
|
+
* criterion, so `wcag2410` is 2.4.10 rather than 2.41.0.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} tag
|
|
139
|
+
* @returns {string|null}
|
|
140
|
+
*/
|
|
141
|
+
export function scFromTag(tag) {
|
|
142
|
+
const m = /^wcag(\d{3,})$/.exec(tag);
|
|
143
|
+
if (!m) return null;
|
|
144
|
+
const digits = m[1];
|
|
145
|
+
return `${digits[0]}.${digits[1]}.${digits.slice(2)}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Level implied by axe's `wcag2a` / `wcag21aa` / `wcag22aa` style tags. */
|
|
149
|
+
export function levelFromTags(tags = []) {
|
|
150
|
+
if (tags.some((t) => /^wcag2\d?aaa$/.test(t))) return 'AAA';
|
|
151
|
+
if (tags.some((t) => /^wcag2\d?aa$/.test(t))) return 'AA';
|
|
152
|
+
if (tags.some((t) => /^wcag2\d?a$/.test(t))) return 'A';
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** First WCAG version containing the criterion, implied by axe's version tags. */
|
|
157
|
+
export function versionFromTags(tags = []) {
|
|
158
|
+
if (tags.some((t) => /^wcag22a{1,3}$/.test(t))) return '2.2';
|
|
159
|
+
if (tags.some((t) => /^wcag21a{1,3}$/.test(t))) return '2.1';
|
|
160
|
+
if (tags.some((t) => /^wcag2a{1,3}$/.test(t))) return '2.0';
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Metadata for a known SC number.
|
|
166
|
+
* @param {string} sc e.g. '4.1.2'
|
|
167
|
+
* @returns {{sc:string,name:string,level:string,minVersion:string,wcag22Only:boolean}|null}
|
|
168
|
+
*/
|
|
169
|
+
export function wcagForSc(sc) {
|
|
170
|
+
const entry = SC_TABLE[sc];
|
|
171
|
+
if (!entry) return null;
|
|
172
|
+
return {
|
|
173
|
+
sc,
|
|
174
|
+
name: entry.name,
|
|
175
|
+
level: entry.level,
|
|
176
|
+
minVersion: entry.minVersion,
|
|
177
|
+
wcag22Only: entry.minVersion === '2.2',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Derive WCAG metadata from an axe rule's tags.
|
|
183
|
+
* Returns null for best-practice rules, which have no success criterion behind
|
|
184
|
+
* them — that distinction is why best-practice findings are never blocking.
|
|
185
|
+
*
|
|
186
|
+
* @param {string[]} tags
|
|
187
|
+
*/
|
|
188
|
+
export function wcagFromTags(tags = []) {
|
|
189
|
+
const scs = tags.map(scFromTag).filter(Boolean);
|
|
190
|
+
if (scs.length === 0) return null;
|
|
191
|
+
// A rule can map to several criteria (e.g. link-name → 2.4.4 + 4.1.2).
|
|
192
|
+
// Report the lowest-numbered as primary and keep the rest.
|
|
193
|
+
const sorted = scs.sort(compareSc);
|
|
194
|
+
const primary = sorted[0];
|
|
195
|
+
const known = wcagForSc(primary);
|
|
196
|
+
if (known) {
|
|
197
|
+
return { ...known, alsoSc: sorted.slice(1) };
|
|
198
|
+
}
|
|
199
|
+
// Unknown criterion (a future axe release): fall back to the tags.
|
|
200
|
+
return {
|
|
201
|
+
sc: primary,
|
|
202
|
+
name: null,
|
|
203
|
+
level: levelFromTags(tags),
|
|
204
|
+
minVersion: versionFromTags(tags),
|
|
205
|
+
wcag22Only: versionFromTags(tags) === '2.2',
|
|
206
|
+
alsoSc: sorted.slice(1),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Numeric sort for dotted SC numbers, so 2.4.9 comes before 2.4.10. */
|
|
211
|
+
export function compareSc(a, b) {
|
|
212
|
+
const pa = a.split('.').map(Number);
|
|
213
|
+
const pb = b.split('.').map(Number);
|
|
214
|
+
for (let i = 0; i < 3; i++) {
|
|
215
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** True if a rule is best-practice only (no success criterion behind it). */
|
|
221
|
+
export function isBestPractice(tags = []) {
|
|
222
|
+
return tags.includes('best-practice') && !tags.some((t) => scFromTag(t));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let ruleIndex = null;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Rule metadata straight from the installed axe-core, keyed by rule id.
|
|
229
|
+
* axe-core carries ACT rule ids inline in `actIds`, so the ACT mapping is free
|
|
230
|
+
* and, unlike the W3C implementation report, is never stale relative to the
|
|
231
|
+
* version we actually run.
|
|
232
|
+
*/
|
|
233
|
+
export function axeRuleIndex() {
|
|
234
|
+
if (ruleIndex) return ruleIndex;
|
|
235
|
+
ruleIndex = new Map();
|
|
236
|
+
try {
|
|
237
|
+
const axe = require('axe-core');
|
|
238
|
+
for (const rule of axe.getRules()) {
|
|
239
|
+
ruleIndex.set(rule.ruleId, rule);
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// axe-core metadata unavailable: findings still carry SC data from tags.
|
|
243
|
+
}
|
|
244
|
+
return ruleIndex;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** ACT rule ids for an axe rule, e.g. `button-name` → `['97a4e1','m6b1q3']`. */
|
|
248
|
+
export function actIdsForRule(ruleId) {
|
|
249
|
+
return axeRuleIndex().get(ruleId)?.actIds ?? [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The installed axe-core version, recorded in every report's provenance. */
|
|
253
|
+
export function axeCoreVersion() {
|
|
254
|
+
try {
|
|
255
|
+
return require('axe-core/package.json').version;
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `SC 4.1.2 Name, Role, Value (Level A)` — the citation format used in output.
|
|
263
|
+
* The SC is always the source of the obligation; ACT ids are secondary
|
|
264
|
+
* identifiers (ACT rules are informative, not normative).
|
|
265
|
+
*/
|
|
266
|
+
export function formatCitation(wcag) {
|
|
267
|
+
if (!wcag?.sc) return null;
|
|
268
|
+
const name = wcag.name ? ` ${wcag.name}` : '';
|
|
269
|
+
const level = wcag.level ? ` (Level ${wcag.level})` : '';
|
|
270
|
+
return `SC ${wcag.sc}${name}${level}`;
|
|
271
|
+
}
|