pour-engine 1.45.0 → 1.46.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.
@@ -6,6 +6,6 @@
6
6
  export default {
7
7
  engine: {
8
8
  name: 'pour engine',
9
- version: '1.45.0',
9
+ version: '1.46.0',
10
10
  },
11
11
  };
package/engine/README.md CHANGED
@@ -113,18 +113,20 @@ pour engine therefore reports on **every** in-scope criterion, one of two ways:
113
113
 
114
114
  ### WCAG 3 Working Draft
115
115
 
116
- A tier tag, `wcag3-bronze`, `wcag3-silver` or `wcag3-gold`, reports against
117
- the WCAG 3.0 Working Draft of 10 September 2026 instead (`wcag3.js`). The
116
+ A check set tag, `wcag3-core` or `wcag3-supplemental`, reports against the
117
+ WCAG 3.0 Working Draft of 10 September 2026 instead (`wcag3.js`). The
118
118
  rules are still WCAG 2 rules: each is matched, by reading both documents,
119
119
  to the draft requirement its failure also fails, and a rule with no such
120
120
  requirement does not run (`UNMAPPED_RULES` gives each reason). Where the
121
121
  draft leaves a value to be determined, text contrast among them, the
122
- rule's WCAG 2 value stands in. `results.standard` is stamped draft,
123
- `manualReview` becomes the tier's provisions, and each rule result carries
124
- `wcag3`. The draft's tiers build on conformance and leave their counts to
125
- be decided, so pour reads them as: Bronze, the core requirements and the
126
- content assertions; Silver, plus every supplemental requirement; Gold,
127
- plus the assertions about the organization. WCAG 2 stays the default.
122
+ rule's WCAG 2 value stands in. `wcag3-core` runs the rules matched to core
123
+ requirements, the ones the draft says must all be met to conform;
124
+ `wcag3-supplemental` adds the rules matched to supplemental requirements.
125
+ `results.standard` is stamped draft and names the set, `manualReview`
126
+ becomes the set's requirements, and each rule result carries `wcag3`.
127
+ The draft's Bronze, Silver and Gold sit above conformance and each needs
128
+ a number of supplemental requirements and assertions it has yet to
129
+ decide, so no check set stands for one. WCAG 2 stays the default.
128
130
 
129
131
  The checklist accounts for the criteria; it does not mean every page state
130
132
  or possible failure has been tested. A snapshot audit still needs human
package/engine/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import config from '../config/project.config.js';
5
5
  import rules from './rules/index.js';
6
6
  import wcagCatalog from './wcag22.js';
7
- import { ruleMatchesTags, tierFromTags, manualReviewChecklist, requirementsForRule, standardFor } from './wcag3.js';
7
+ import { ruleMatchesTags, checkSetFromTags, manualReviewChecklist, requirementsForRule, standardFor } from './wcag3.js';
8
8
  import { isVisible, isRendered, cssPath, htmlSnippet, ownText, collectRoots, resetDOMCaches, releaseDOMCaches } from './lib/dom.js';
9
9
  import { accessibleName } from './lib/accessible-name.js';
10
10
  import { resetAuditCaches } from './lib/contrast.js';
@@ -82,10 +82,11 @@ function toResultNode(element, outcome) {
82
82
  * @param {Document|Element} context - what to scan
83
83
  * @param {{ tags?: string[], exclude?: string, signal?: AbortSignal }} options -
84
84
  * rule selection by tag (wcag2a…wcag22aa, best-practice), empty selects
85
- * every rule; a WCAG 3 tier tag (wcag3-bronze, -silver, -gold) selects
86
- * the rules mapped to that tier's draft requirements and reframes the
87
- * report against the WCAG 3.0 Working Draft: results.standard is
88
- * stamped draft, the manual checklist becomes the tier's provisions,
85
+ * every rule; a WCAG 3 check set tag (wcag3-core, wcag3-supplemental)
86
+ * selects the rules matched to that set's draft requirements and
87
+ * reframes the report against the WCAG 3.0 Working Draft:
88
+ * results.standard is stamped draft, the manual checklist becomes the
89
+ * set's requirements,
89
90
  * and each rule result carries the requirements it maps to (wcag3); exclude is a CSS selector — elements matching it, or inside
90
91
  * a match (including across shadow boundaries), are left out of every
91
92
  * rule; signal aborts the run between rules (throws AbortError)
@@ -97,17 +98,17 @@ function toResultNode(element, outcome) {
97
98
  */
98
99
  export async function run(context = document, options = {}, onProgress) {
99
100
  const auditStarted = performance.now();
100
- const tier = tierFromTags(options.tags);
101
+ const checkSet = checkSetFromTags(options.tags);
101
102
  const results = {
102
103
  testEngine: { name, version },
103
104
  timestamp: new Date().toISOString(),
104
105
  url: context.location?.href ?? context.ownerDocument?.location?.href ?? '',
105
- standard: tier ? standardFor(tier) : null,
106
+ standard: checkSet ? standardFor(checkSet) : null,
106
107
  violations: [],
107
108
  passes: [],
108
109
  incomplete: [],
109
110
  inapplicable: [],
110
- manualReview: tier ? manualReviewChecklist(tier) : manualReviewCriteria(options.tags),
111
+ manualReview: checkSet ? manualReviewChecklist(checkSet) : manualReviewCriteria(options.tags),
111
112
  ruleTimings: [],
112
113
  };
113
114
 
@@ -254,7 +255,7 @@ export async function run(context = document, options = {}, onProgress) {
254
255
  };
255
256
  // In a WCAG 3 audit, the draft requirements this rule speaks to (empty
256
257
  // for a best-practice rule, which maps to none).
257
- if (tier) ruleResult.wcag3 = requirementsForRule(rule.id);
258
+ if (checkSet) ruleResult.wcag3 = requirementsForRule(rule.id);
258
259
  if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
259
260
  if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
260
261
  if (buckets.incomplete.length) results.incomplete.push({ ...ruleResult, nodes: buckets.incomplete });
@@ -1,8 +1,11 @@
1
1
  // WCAG SC 2.4.7 Focus Visible (Level AA)
2
- // A stylesheet that strips focus outlines without providing a replacement
3
- // indicator is the classic 2.4.7 failure (F78). Cross-origin sheets are
4
- // unreadable and skipped; a replacement indicator can live in a different
5
- // rule entirely — so this flags for review, it never asserts a fail.
2
+ // A stylesheet or style attribute that strips focus outlines without
3
+ // providing a replacement indicator is the classic 2.4.7 failure (F78).
4
+ // Cross-origin sheets are unreadable and skipped; a replacement indicator can
5
+ // live in a different rule entirely — so this flags for review, it never
6
+ // asserts a fail.
7
+ import { isRendered } from '../../lib/dom.js';
8
+
6
9
  // The focus pseudo-classes themselves, and nothing else: :focus-within is a
7
10
  // PARENT's state and stripping ":focus" out of it would leave "-within".
8
11
  const FOCUS_TOKEN = /:focus(?:-visible)?(?![\w-])/g;
@@ -33,7 +36,8 @@ function focusSuppressors(doc) {
33
36
  // human question.
34
37
  let sheet = null;
35
38
  const inspect = (rule) => {
36
- if (!rule.selectorText || !/:focus/.test(rule.selectorText)) return;
39
+ if (!rule.selectorText) return;
40
+ if (!/:focus/.test(rule.selectorText)) { inspectResting(rule); return; }
37
41
  // `:focus:not(:focus-visible) { outline: none }` is the PUBLISHED pattern
38
42
  // for showing the ring to keyboard users only: it strips the indicator
39
43
  // precisely in the state where the browser has decided not to show focus,
@@ -65,7 +69,38 @@ function focusSuppressors(doc) {
65
69
  provider.order > suspect.order && covered(subject, provider)));
66
70
  if (!restored) suspects.push(suspect.selector);
67
71
  }
72
+ allProviders.push(...sheet.providers);
73
+ };
74
+ // A rule with no focus state in it removes the indicator too: author
75
+ // styles outrank the browser's own :focus-visible ring whatever their
76
+ // specificity, so `* { outline: none }` or `a, button { outline: 0 }`
77
+ // leaves a focused link with no ring (measured in Chromium 2026-09-24).
78
+ // Such a rule matters only where it reaches something Tab lands on, and a
79
+ // box-shadow or border in the same rule is not a replacement: it is there
80
+ // at rest, so it marks nothing when focus arrives.
81
+ const resting = [];
82
+ const allProviders = [];
83
+ const tabbable = (el) => !el.disabled && (el.tabIndex >= 0 || el.isContentEditable) && isRendered(el);
84
+ const reachesTabbable = (part) => {
85
+ if (part.includes('::')) return false; // a pseudo-element's outline is not the control's ring
86
+ try { return [...doc.querySelectorAll(part)].some(tabbable); } catch { return false; }
87
+ };
88
+ const inspectResting = (rule) => {
89
+ const style = rule.style;
90
+ if (!style || !rule.selectorText) return;
91
+ const styles = [style, ...[...(rule.cssRules ?? [])]
92
+ .filter((child) => child.style && !child.selectorText).map((child) => child.style)];
93
+ if (!styles.map(outlineOf).some(removesOutline)) return;
94
+ const parts = subjects(rule.selectorText).filter(reachesTabbable);
95
+ if (parts.length) resting.push({ selector: rule.selectorText, subjects: parts });
68
96
  };
97
+ // A focus rule outranks a resting one only when its specificity is
98
+ // higher, which order in the sheet cannot change. `a:focus-visible` always
99
+ // beats `a`; a bare `:focus-visible` (0,1,0) beats only selectors made of
100
+ // type names and `*`, and ties or loses to `.btn` or `.nav .btn`.
101
+ const typeOnly = (subject) => !/[#.[:]/.test(subject);
102
+ const outranked = (subject) => allProviders.some((provider) => provider.subjects.some((p) =>
103
+ p === subject || ((p === '' || p === '*') && typeOnly(subject))));
69
104
  // Inspect each rule, THEN descend into any children. Testing `rule.cssRules`
70
105
  // first and skipping used to walk straight past every plain style rule:
71
106
  // since CSS Nesting shipped, a CSSStyleRule carries its own cssRules list,
@@ -84,6 +119,14 @@ function focusSuppressors(doc) {
84
119
  try { scan(styleSheet.cssRules); } catch { /* cross-origin sheet */ }
85
120
  settle();
86
121
  }
122
+ for (const suspect of resting) {
123
+ if (!suspect.subjects.every(outranked)) suspects.push(suspect.selector);
124
+ }
125
+ // A style attribute outranks every selector, so no focus rule without
126
+ // !important gives the ring back to `<a style="outline: none">`.
127
+ for (const el of doc.querySelectorAll('[style]')) {
128
+ if (removesOutline(outlineOf(el.style)) && tabbable(el)) suspects.push(`the style attribute of <${el.localName}>`);
129
+ }
87
130
  return suspects;
88
131
  }
89
132
 
@@ -102,7 +145,7 @@ export default {
102
145
  const shown = suspects.slice(0, 3).join(', ');
103
146
  return {
104
147
  status: 'incomplete',
105
- message: `${suspects.length} CSS rule(s) remove the focus outline without setting a replacement in the same rule (e.g. ${shown}) — if no other rule provides a visible indicator, keyboard users lose their place. Tab through the page to check.`,
148
+ message: `${suspects.length} CSS rule(s) or style attribute(s) remove the focus outline with no replacement found (e.g. ${shown}). If nothing else draws a visible indicator, keyboard users lose their place. Tab through the page to check.`,
106
149
  };
107
150
  },
108
151
  };
package/engine/wcag3.js CHANGED
@@ -7,21 +7,18 @@
7
7
  // surface that shows it must say so. WCAG 2 stays the default everywhere.
8
8
  //
9
9
  // The draft replaces success criteria and A/AA/AAA with three kinds of
10
- // provision under each guideline: core requirements (all must be met to
11
- // conform), supplemental requirements (beyond conformance) and assertions
12
- // (documented statements about an organization's practice, which no tool
13
- // can check). Its Reporting section (4.1.4, Exploratory) lays out tiers
14
- // that build on conformance: Bronze and Silver add a to-be-decided number
15
- // of supplemental requirements and content assertions, Gold adds the
16
- // assertions about the organization. Until the draft fixes those numbers,
17
- // pour reads the tiers this way, and says so:
18
- // Bronze: the core requirements, plus the content assertions to make
19
- // Silver: Bronze plus every supplemental requirement
20
- // Gold: Silver plus the assertions about the organization
21
- //
22
- // The draft tags each assertion Content or Organization in principle but
23
- // not yet one by one; an assertion about a style guide, policy or process
24
- // the organization keeps is read as Organization, any other as Content.
10
+ // provision under each guideline: core requirements, supplemental
11
+ // requirements and assertions (documented statements about how content is
12
+ // made or how an organization works, which no tool can check). To conform
13
+ // (3.2), every core requirement must be met; supplemental requirements and
14
+ // assertions are not needed to conform. The draft's Reporting section (4,
15
+ // Exploratory) puts Bronze, Silver and Gold above conformance, each taking
16
+ // a number of supplemental requirements and assertions still to be
17
+ // decided, so none of them is a set of checks a tool can run. pour offers
18
+ // two check sets instead, and no tier:
19
+ // core: the rules matched to core requirements
20
+ // supplemental: those rules plus the ones matched to supplemental
21
+ // requirements
25
22
  //
26
23
  // No rule here was written for WCAG 3. The rules test WCAG 2 criteria;
27
24
  // RULE_REQUIREMENTS names, rule by rule, the draft requirement that a
@@ -41,28 +38,32 @@ export const draftInfo = {
41
38
  note: 'An unfinished standard. Findings come from WCAG 2 rules matched to draft requirements, with WCAG 2 values where the draft has none yet. Not for conformance claims.',
42
39
  };
43
40
 
44
- export const TIERS = ['bronze', 'silver', 'gold'];
45
- export const TIER_LABELS = { bronze: 'Bronze', silver: 'Silver', gold: 'Gold' };
46
-
47
- // Which provision types each tier takes in. Cumulative, as in the draft.
48
- const TIER_TYPES = {
49
- bronze: new Set(['core', 'content']),
50
- silver: new Set(['core', 'content', 'supplemental']),
51
- gold: new Set(['core', 'content', 'supplemental', 'organization']),
41
+ // The check sets, cumulative, and the provision types each takes in.
42
+ export const CHECK_SETS = ['core', 'supplemental'];
43
+ const SET_TYPES = {
44
+ core: new Set(['core']),
45
+ supplemental: new Set(['core', 'supplemental']),
46
+ };
47
+ const SET_SCOPES = {
48
+ core: 'core requirements',
49
+ supplemental: 'core and supplemental requirements',
52
50
  };
51
+ // Names the first WCAG 3 draft mode gave these sets, read the way they ran
52
+ // then, so a saved setting, a script or a tag list that still uses one
53
+ // selects the same rules. They are never shown.
54
+ const RENAMED_SETS = { bronze: 'core', silver: 'supplemental', gold: 'supplemental' };
53
55
 
54
56
  const TYPE_LABELS = {
55
57
  core: 'Core',
56
58
  supplemental: 'Supplemental',
57
- content: 'Assertion',
58
- organization: 'Assertion',
59
+ assertion: 'Assertion',
59
60
  };
60
61
 
61
62
  // Every requirement and assertion of the draft, in document order, under
62
63
  // its guideline. The one recommended practice (2.7.2.7) is left out: the
63
- // draft says recommended practices are not needed to conform or to reach
64
- // any tier. Guidelines with nothing past Exploratory (2.1.2 Figure
65
- // captions, 2.12.2 Control text) have no provisions to list.
64
+ // draft says recommended practices are not needed to conform. Guidelines
65
+ // with nothing past Exploratory (2.1.2 Figure captions, 2.12.2 Control
66
+ // text) have no provisions to list.
66
67
  const RAW = [
67
68
  g('2.1.1', 'Image alternatives'),
68
69
  p('2.1.1.1', 'Images detectable', 'core', 'x2-1-1-1-images-detectable'),
@@ -81,9 +82,9 @@ const RAW = [
81
82
  p('2.1.4.7', 'Speaker language identified in transcripts', 'supplemental', 'x2-1-4-7-speaker-language-identified-in-transcripts'),
82
83
  p('2.1.4.8', 'Sounds identified in transcripts', 'core', 'x2-1-4-8-sounds-identified-in-transcripts'),
83
84
  p('2.1.4.9', 'Visual information identified in transcripts', 'core', 'x2-1-4-9-visual-information-identified-in-transcripts'),
84
- p('2.1.4.10', 'Transcripts style guide', 'organization', 'x2-1-4-10-transcripts-style-guide'),
85
- p('2.1.4.11', 'Transcripts usability testing', 'content', 'x2-1-4-11-transcripts-usability-testing'),
86
- p('2.1.4.12', 'Transcripts reviewed by content authors', 'content', 'x2-1-4-12-transcripts-reviewed-by-content-authors'),
85
+ p('2.1.4.10', 'Transcripts style guide', 'assertion', 'x2-1-4-10-transcripts-style-guide'),
86
+ p('2.1.4.11', 'Transcripts usability testing', 'assertion', 'x2-1-4-11-transcripts-usability-testing'),
87
+ p('2.1.4.12', 'Transcripts reviewed by content authors', 'assertion', 'x2-1-4-12-transcripts-reviewed-by-content-authors'),
87
88
  g('2.1.5', 'Captions'),
88
89
  p('2.1.5.1', 'Captions adjustable', 'supplemental', 'x2-1-5-1-captions-adjustable'),
89
90
  p('2.1.5.2', 'Captions available (prerecorded)', 'core', 'x2-1-5-2-captions-available-prerecorded'),
@@ -98,9 +99,9 @@ const RAW = [
98
99
  p('2.1.5.11', 'Speakers identified in captions', 'supplemental', 'x2-1-5-11-speakers-identified-in-captions'),
99
100
  p('2.1.5.12', 'Speaker language identified in captions', 'supplemental', 'x2-1-5-12-speaker-language-identified-in-captions'),
100
101
  p('2.1.5.13', 'Sounds identified in captions', 'core', 'x2-1-5-13-sounds-identified-in-captions'),
101
- p('2.1.5.14', 'Captions style guide', 'organization', 'x2-1-5-14-captions-style-guide'),
102
- p('2.1.5.15', 'Captions usability testing', 'content', 'x2-1-5-15-captions-usability-testing'),
103
- p('2.1.5.16', 'Captions reviewed by content authors', 'content', 'x2-1-5-16-captions-reviewed-by-content-authors'),
102
+ p('2.1.5.14', 'Captions style guide', 'assertion', 'x2-1-5-14-captions-style-guide'),
103
+ p('2.1.5.15', 'Captions usability testing', 'assertion', 'x2-1-5-15-captions-usability-testing'),
104
+ p('2.1.5.16', 'Captions reviewed by content authors', 'assertion', 'x2-1-5-16-captions-reviewed-by-content-authors'),
104
105
  g('2.1.6', 'Audio descriptions'),
105
106
  p('2.1.6.1', 'Audio descriptions available (prerecorded)', 'core', 'x2-1-6-1-audio-descriptions-available-prerecorded'),
106
107
  p('2.1.6.2', 'Audio descriptions equivalent (prerecorded)', 'core', 'x2-1-6-2-audio-descriptions-equivalent-prerecorded'),
@@ -114,13 +115,13 @@ const RAW = [
114
115
  p('2.1.6.10', 'Speaker language identified in audio descriptions', 'supplemental', 'x2-1-6-10-speaker-language-identified-in-audio-descriptions'),
115
116
  p('2.1.6.11', 'Sounds identified in audio descriptions', 'core', 'x2-1-6-11-sounds-identified-in-audio-descriptions'),
116
117
  p('2.1.6.12', 'Visual information identified in audio descriptions', 'core', 'x2-1-6-12-visual-information-identified-in-audio-descriptions'),
117
- p('2.1.6.13', 'Audio descriptions style guide', 'organization', 'x2-1-6-13-audio-descriptions-style-guide'),
118
- p('2.1.6.14', 'Audio descriptions usability testing', 'content', 'x2-1-6-14-audio-descriptions-usability-testing'),
119
- p('2.1.6.15', 'Audio descriptions reviewed by content authors', 'content', 'x2-1-6-15-audio-descriptions-reviewed-by-content-authors'),
118
+ p('2.1.6.13', 'Audio descriptions style guide', 'assertion', 'x2-1-6-13-audio-descriptions-style-guide'),
119
+ p('2.1.6.14', 'Audio descriptions usability testing', 'assertion', 'x2-1-6-14-audio-descriptions-usability-testing'),
120
+ p('2.1.6.15', 'Audio descriptions reviewed by content authors', 'assertion', 'x2-1-6-15-audio-descriptions-reviewed-by-content-authors'),
120
121
  g('2.1.7', 'Sign language'),
121
122
  p('2.1.7.1', 'Sign language available (prerecorded)', 'supplemental', 'x2-1-7-1-sign-language-available-prerecorded'),
122
123
  p('2.1.7.2', 'Sign language controllable', 'supplemental', 'x2-1-7-2-sign-language-controllable'),
123
- p('2.1.7.3', 'Sign language policy (live)', 'organization', 'x2-1-7-3-sign-language-policy-live'),
124
+ p('2.1.7.3', 'Sign language policy (live)', 'assertion', 'x2-1-7-3-sign-language-policy-live'),
124
125
  g('2.1.8', 'Single sense'),
125
126
  p('2.1.8.1', 'Hue not relied on', 'core', 'x2-1-8-1-hue-not-relied-on'),
126
127
  p('2.1.8.2', 'Graphical object contrast sufficient', 'core', 'x2-1-8-2-graphical-object-contrast-sufficient'),
@@ -128,8 +129,8 @@ const RAW = [
128
129
  p('2.1.8.4', 'Sound not relied on', 'core', 'x2-1-8-4-sound-not-relied-on'),
129
130
  p('2.1.8.5', 'Spatial audio not relied on', 'core', 'x2-1-8-5-spatial-audio-not-relied-on'),
130
131
  g('2.1.9', 'Accessible media player'),
131
- p('2.1.9.1', 'Accessible video player selected', 'content', 'x2-1-9-1-accessible-video-player-selected'),
132
- p('2.1.9.2', 'Accessible audio player selected', 'content', 'x2-1-9-2-accessible-audio-player-selected'),
132
+ p('2.1.9.1', 'Accessible video player selected', 'assertion', 'x2-1-9-1-accessible-video-player-selected'),
133
+ p('2.1.9.2', 'Accessible audio player selected', 'assertion', 'x2-1-9-2-accessible-audio-player-selected'),
133
134
  g('2.2.1', 'Text appearance'),
134
135
  p('2.2.1.1', 'Blocks of text readable (minimum)', 'core', 'x2-2-1-1-blocks-of-text-readable-minimum'),
135
136
  p('2.2.1.2', 'Text style readable (minimum)', 'core', 'x2-2-1-2-text-style-readable-minimum'),
@@ -154,13 +155,13 @@ const RAW = [
154
155
  p('2.2.3.5', 'Diacritics available', 'core', 'x2-2-3-5-diacritics-available'),
155
156
  p('2.2.3.6', 'No nested clauses', 'supplemental', 'x2-2-3-6-no-nested-clauses'),
156
157
  p('2.2.3.7', 'No unnecessary words', 'supplemental', 'x2-2-3-7-no-unnecessary-words'),
157
- p('2.2.3.8', 'Clear language review', 'organization', 'x2-2-3-8-clear-language-review'),
158
- p('2.2.3.9', 'Visual aids review', 'organization', 'x2-2-3-9-visual-aids-review'),
158
+ p('2.2.3.8', 'Clear language review', 'assertion', 'x2-2-3-8-clear-language-review'),
159
+ p('2.2.3.9', 'Visual aids review', 'assertion', 'x2-2-3-9-visual-aids-review'),
159
160
  g('2.3.1', 'Keyboard focus appearance'),
160
161
  p('2.3.1.1', 'Default focus indicator used', 'supplemental', 'x2-3-1-1-default-focus-indicator-used'),
161
162
  p('2.3.1.2', 'Focus indicator contrast sufficient', 'core', 'x2-3-1-2-focus-indicator-contrast-sufficient'),
162
163
  p('2.3.1.3', 'Focus indicator size sufficient', 'supplemental', 'x2-3-1-3-focus-indicator-size-sufficient'),
163
- p('2.3.1.4', 'Focus indicator style guide', 'organization', 'x2-3-1-4-focus-indicator-style-guide'),
164
+ p('2.3.1.4', 'Focus indicator style guide', 'assertion', 'x2-3-1-4-focus-indicator-style-guide'),
164
165
  g('2.3.2', 'Pointer focus appearance'),
165
166
  p('2.3.2.1', 'Pointer activation indicated (minimum)', 'core', 'x2-3-2-1-pointer-activation-indicated-minimum'),
166
167
  p('2.3.2.2', 'Pointer activation indicated (enhanced)', 'supplemental', 'x2-3-2-2-pointer-activation-indicated-enhanced'),
@@ -174,9 +175,9 @@ const RAW = [
174
175
  p('2.3.3.2', 'Focus retained', 'supplemental', 'x2-3-3-2-focus-retained'),
175
176
  p('2.3.3.3', 'Focus order meaningful', 'core', 'x2-3-3-3-focus-order-meaningful'),
176
177
  g('2.3.4', 'Expected behavior'),
177
- p('2.3.4.1', 'Consistent interactions', 'content', 'x2-3-4-1-consistent-interactions'),
178
+ p('2.3.4.1', 'Consistent interactions', 'assertion', 'x2-3-4-1-consistent-interactions'),
178
179
  p('2.3.4.2', 'Consistent control location', 'supplemental', 'x2-3-4-2-consistent-control-location'),
179
- p('2.3.4.3', 'Conventional pattern used', 'content', 'x2-3-4-3-conventional-pattern-used'),
180
+ p('2.3.4.3', 'Conventional pattern used', 'assertion', 'x2-3-4-3-conventional-pattern-used'),
180
181
  g('2.3.5', 'Control information'),
181
182
  p('2.3.5.1', 'Interactive element contrast sufficient', 'core', 'x2-3-5-1-interactive-element-contrast-sufficient'),
182
183
  p('2.3.5.2', 'Interactive element names available', 'core', 'x2-3-5-2-interactive-element-names-available'),
@@ -197,7 +198,7 @@ const RAW = [
197
198
  g('2.4.2', 'Physical or cognitive effort when using keyboard'),
198
199
  p('2.4.2.1', 'Navigation keys described', 'supplemental', 'x2-4-2-1-navigation-keys-described'),
199
200
  p('2.4.2.2', 'No repetitive adjacent interactive elements', 'supplemental', 'x2-4-2-2-no-repetitive-adjacent-interactive-elements'),
200
- p('2.4.2.3', 'Keyboard effort comparable', 'content', 'x2-4-2-3-keyboard-effort-comparable'),
201
+ p('2.4.2.3', 'Keyboard effort comparable', 'assertion', 'x2-4-2-3-keyboard-effort-comparable'),
201
202
  g('2.4.3', 'Pointer input'),
202
203
  p('2.4.3.1', 'Pointer activation controllable', 'core', 'x2-4-3-1-pointer-activation-controllable'),
203
204
  p('2.4.3.2', 'Simple pointer input available', 'core', 'x2-4-3-2-simple-pointer-input-available'),
@@ -207,7 +208,7 @@ const RAW = [
207
208
  g('2.4.4', 'Speech and voice input'),
208
209
  p('2.4.4.1', 'Speech not relied on', 'core', 'x2-4-4-1-speech-not-relied-on'),
209
210
  p('2.4.4.2', 'Real-time text available', 'core', 'x2-4-4-2-real-time-text-available'),
210
- p('2.4.4.3', 'Generated speech testing', 'content', 'x2-4-4-3-generated-speech-testing'),
211
+ p('2.4.4.3', 'Generated speech testing', 'assertion', 'x2-4-4-3-generated-speech-testing'),
211
212
  g('2.4.5', 'Input operation'),
212
213
  p('2.4.5.1', 'Hover or focus content dismissible', 'core', 'x2-4-5-1-hover-or-focus-content-dismissible'),
213
214
  p('2.4.5.2', 'Hover content persistent', 'core', 'x2-4-5-2-hover-content-persistent'),
@@ -231,7 +232,7 @@ const RAW = [
231
232
  p('2.5.2.1', 'Errors preventable', 'core', 'x2-5-2-1-errors-preventable'),
232
233
  p('2.5.2.2', 'Submission status notified', 'supplemental', 'x2-5-2-2-submission-status-notified'),
233
234
  p('2.5.2.3', 'Data entry validated', 'supplemental', 'x2-5-2-3-data-entry-validated'),
234
- p('2.5.2.4', 'Error prevention review', 'content', 'x2-5-2-4-error-prevention-review'),
235
+ p('2.5.2.4', 'Error prevention review', 'assertion', 'x2-5-2-4-error-prevention-review'),
235
236
  g('2.6.1', 'Avoid physical harm'),
236
237
  p('2.6.1.1', 'No flashing over threshold', 'core', 'x2-6-1-1-no-flashing-over-threshold'),
237
238
  p('2.6.1.2', 'No flashing over threshold (no exceptions)', 'supplemental', 'x2-6-1-2-no-flashing-over-threshold-no-exceptions'),
@@ -240,12 +241,12 @@ const RAW = [
240
241
  p('2.6.1.5', 'Trigger warning available', 'core', 'x2-6-1-5-trigger-warning-available'),
241
242
  p('2.6.1.6', 'Haptic stimulation adjustable', 'core', 'x2-6-1-6-haptic-stimulation-adjustable'),
242
243
  p('2.6.1.7', 'Audio shifting adjustable', 'core', 'x2-6-1-7-audio-shifting-adjustable'),
243
- p('2.6.1.8', 'Safe content review', 'content', 'x2-6-1-8-safe-content-review'),
244
+ p('2.6.1.8', 'Safe content review', 'assertion', 'x2-6-1-8-safe-content-review'),
244
245
  g('2.7.1', 'Recognizable layouts'),
245
- p('2.7.1.1', 'Conventional layout review', 'content', 'x2-7-1-1-conventional-layout-review'),
246
+ p('2.7.1.1', 'Conventional layout review', 'assertion', 'x2-7-1-1-conventional-layout-review'),
246
247
  g('2.7.2', 'User orientation'),
247
248
  p('2.7.2.1', 'Page/view title available', 'core', 'x2-7-2-1-page-view-title-available'),
248
- p('2.7.2.2', 'Location within product review', 'content', 'x2-7-2-2-location-within-product-review'),
249
+ p('2.7.2.2', 'Location within product review', 'assertion', 'x2-7-2-2-location-within-product-review'),
249
250
  p('2.7.2.3', 'All steps listed', 'core', 'x2-7-2-3-all-steps-listed'),
250
251
  p('2.7.2.4', 'Current step indicated', 'core', 'x2-7-2-4-current-step-indicated'),
251
252
  p('2.7.2.5', 'Page/view change notified', 'core', 'x2-7-2-5-page-view-change-notified'),
@@ -257,8 +258,8 @@ const RAW = [
257
258
  p('2.7.3.4', 'Heading structure available', 'supplemental', 'x2-7-3-4-heading-structure-available'),
258
259
  p('2.7.3.5', 'Order detectable', 'core', 'x2-7-3-5-order-detectable'),
259
260
  p('2.7.3.6', 'Blocks of content available (enhanced)', 'supplemental', 'x2-7-3-6-blocks-of-content-available-enhanced'),
260
- p('2.7.3.7', 'Clear structure review', 'organization', 'x2-7-3-7-clear-structure-review'),
261
- p('2.7.3.8', 'Key information usability testing', 'content', 'x2-7-3-8-key-information-usability-testing'),
261
+ p('2.7.3.7', 'Clear structure review', 'assertion', 'x2-7-3-7-clear-structure-review'),
262
+ p('2.7.3.8', 'Key information usability testing', 'assertion', 'x2-7-3-8-key-information-usability-testing'),
262
263
  g('2.7.4', 'No obstruction'),
263
264
  p('2.7.4.1', 'Overlay content dismissible', 'core', 'x2-7-4-1-overlay-content-dismissible'),
264
265
  g('2.8.1', 'Consistency'),
@@ -271,12 +272,12 @@ const RAW = [
271
272
  g('2.9.2', 'Adequate time'),
272
273
  p('2.9.2.1', 'Timeout adjustable', 'supplemental', 'x2-9-2-1-timeout-adjustable'),
273
274
  p('2.9.2.2', 'No time limits', 'supplemental', 'x2-9-2-2-no-time-limits'),
274
- p('2.9.2.3', 'No unnecessary time limits', 'content', 'x2-9-2-3-no-unnecessary-time-limits'),
275
+ p('2.9.2.3', 'No unnecessary time limits', 'assertion', 'x2-9-2-3-no-unnecessary-time-limits'),
275
276
  p('2.9.2.4', 'Time limits conveyed', 'supplemental', 'x2-9-2-4-time-limits-conveyed'),
276
277
  g('2.9.3', 'Avoid deception'),
277
278
  p('2.9.3.1', 'Preselections visible', 'core', 'x2-9-3-1-preselections-visible'),
278
- p('2.9.3.2', 'Deceptive practices usability testing', 'content', 'x2-9-3-2-deceptive-practices-usability-testing'),
279
- p('2.9.3.3', 'Deceptive messaging expert review', 'content', 'x2-9-3-3-deceptive-messaging-expert-review'),
279
+ p('2.9.3.2', 'Deceptive practices usability testing', 'assertion', 'x2-9-3-2-deceptive-practices-usability-testing'),
280
+ p('2.9.3.3', 'Deceptive messaging expert review', 'assertion', 'x2-9-3-3-deceptive-messaging-expert-review'),
280
281
  g('2.9.4', 'Retain information'),
281
282
  p('2.9.4.1', 'Going back supported', 'supplemental', 'x2-9-4-1-going-back-supported'),
282
283
  p('2.9.4.2', 'No redundant entry', 'supplemental', 'x2-9-4-2-no-redundant-entry'),
@@ -286,22 +287,22 @@ const RAW = [
286
287
  p('2.9.5.2', 'Information requirements available at start', 'supplemental', 'x2-9-5-2-information-requirements-available-at-start'),
287
288
  p('2.9.5.3', 'Process instructions available', 'supplemental', 'x2-9-5-3-process-instructions-available'),
288
289
  g('2.9.6', 'Unnecessary steps'),
289
- p('2.9.6.1', 'Usability testing for unnecessary steps', 'content', 'x2-9-6-1-usability-testing-for-unnecessary-steps'),
290
+ p('2.9.6.1', 'Usability testing for unnecessary steps', 'assertion', 'x2-9-6-1-usability-testing-for-unnecessary-steps'),
290
291
  g('2.10.1', 'Risk'),
291
292
  p('2.10.1.1', 'Consequences of choices explained', 'core', 'x2-10-1-1-consequences-of-choices-explained'),
292
293
  p('2.10.1.2', 'Consequences explained before agreement', 'supplemental', 'x2-10-1-2-consequences-explained-before-agreement'),
293
- p('2.10.1.3', 'Diverse disabilities considered', 'content', 'x2-10-1-3-diverse-disabilities-considered'),
294
- p('2.10.1.4', 'Algorithm inclusivity review', 'organization', 'x2-10-1-4-algorithm-inclusivity-review'),
294
+ p('2.10.1.3', 'Diverse disabilities considered', 'assertion', 'x2-10-1-3-diverse-disabilities-considered'),
295
+ p('2.10.1.4', 'Algorithm inclusivity review', 'assertion', 'x2-10-1-4-algorithm-inclusivity-review'),
295
296
  g('2.10.2', 'Algorithms'),
296
- p('2.10.2.1', 'Inclusive data set', 'content', 'x2-10-2-1-inclusive-data-set'),
297
- p('2.10.2.2', 'No harm from algorithms', 'content', 'x2-10-2-2-no-harm-from-algorithms'),
297
+ p('2.10.2.1', 'Inclusive data set', 'assertion', 'x2-10-2-1-inclusive-data-set'),
298
+ p('2.10.2.2', 'No harm from algorithms', 'assertion', 'x2-10-2-2-no-harm-from-algorithms'),
298
299
  g('2.11.1', 'Help available'),
299
300
  p('2.11.1.1', 'Consistent help available', 'supplemental', 'x2-11-1-1-consistent-help-available'),
300
301
  p('2.11.1.2', 'Contextual help available', 'supplemental', 'x2-11-1-2-contextual-help-available'),
301
302
  p('2.11.1.3', 'Disabled controls explained', 'supplemental', 'x2-11-1-3-disabled-controls-explained'),
302
303
  p('2.11.1.4', 'Sensory characteristics not relied on', 'core', 'x2-11-1-4-sensory-characteristics-not-relied-on'),
303
- p('2.11.1.5', 'Supported decision-making review', 'content', 'x2-11-1-5-supported-decision-making-review'),
304
- p('2.11.1.6', 'Help usability testing', 'content', 'x2-11-1-6-help-usability-testing'),
304
+ p('2.11.1.5', 'Supported decision-making review', 'assertion', 'x2-11-1-5-supported-decision-making-review'),
305
+ p('2.11.1.6', 'Help usability testing', 'assertion', 'x2-11-1-6-help-usability-testing'),
305
306
  g('2.11.2', 'Feedback'),
306
307
  p('2.11.2.1', 'Feedback mechanism available', 'supplemental', 'x2-11-2-1-feedback-mechanism-available'),
307
308
  g('2.12.1', 'Assistive technology control'),
@@ -437,13 +438,20 @@ export const UNMAPPED_RULES = {
437
438
  'on-input-navigation': 'The draft\'s change notifications (2.7.2.5, 2.12.5) cover different triggers from a change of context on input.',
438
439
  };
439
440
 
440
- /** The WCAG 3 tier a tag list asks for: wcag3-bronze, -silver or -gold,
441
- * the highest when several are present; null for a WCAG 2 audit. */
442
- export function tierFromTags(tags) {
441
+ /** The check set a value names, 'core' or 'supplemental', or null. */
442
+ export function checkSetOf(value) {
443
+ const name = String(value ?? '').toLowerCase();
444
+ return CHECK_SETS.includes(name) ? name : RENAMED_SETS[name] ?? null;
445
+ }
446
+
447
+ /** The WCAG 3 check set a tag list asks for, wcag3-core or
448
+ * wcag3-supplemental, the larger when both are present; null for a
449
+ * WCAG 2 audit. */
450
+ export function checkSetFromTags(tags) {
443
451
  let found = null;
444
452
  for (const tag of tags ?? []) {
445
- const tier = /^wcag3-(bronze|silver|gold)$/.exec(tag)?.[1];
446
- if (tier && (!found || TIERS.indexOf(tier) > TIERS.indexOf(found))) found = tier;
453
+ const set = /^wcag3-/.test(tag) ? checkSetOf(tag.slice(6)) : null;
454
+ if (set && (!found || CHECK_SETS.indexOf(set) > CHECK_SETS.indexOf(found))) found = set;
447
455
  }
448
456
  return found;
449
457
  }
@@ -456,33 +464,35 @@ export function requirementsForRule(ruleId) {
456
464
  });
457
465
  }
458
466
 
459
- /** Does the tier take in any requirement this rule tests? */
460
- export function ruleInTier(ruleId, tier) {
461
- const types = TIER_TYPES[tier];
467
+ /** Does the check set take in any requirement this rule tests? */
468
+ export function ruleInCheckSet(ruleId, set) {
469
+ const types = SET_TYPES[set];
462
470
  return Boolean(types) && (RULE_REQUIREMENTS[ruleId] ?? [])
463
471
  .some((num) => types.has(provisionByNum.get(num).type));
464
472
  }
465
473
 
466
474
  /**
467
475
  * Rule selection shared by every runner. A tag list selects the rules that
468
- * carry any of its tags; a WCAG 3 tier tag also selects the rules its
469
- * requirements map to. An empty or absent list selects every rule.
476
+ * carry any of its tags; a WCAG 3 check set tag also selects the rules
477
+ * matched to its requirements. An empty or absent list selects every rule.
470
478
  */
471
479
  export function ruleMatchesTags(rule, tags) {
472
480
  if (!tags?.length) return true;
473
481
  if (rule.tags.some((tag) => tags.includes(tag))) return true;
474
- const tier = tierFromTags(tags);
475
- return Boolean(tier) && ruleInTier(rule.id, tier);
482
+ const set = checkSetFromTags(tags);
483
+ return Boolean(set) && ruleInCheckSet(rule.id, set);
476
484
  }
477
485
 
478
486
  /**
479
- * The tier as a human checklist, shaped like the WCAG 2 manual-review
480
- * entries: `level` carries the provision type and `principle` the
481
- * guideline. Every provision in the tier is listed, since no rule was
487
+ * The check set's requirements as a human checklist, shaped like the WCAG 2
488
+ * manual-review entries: `level` carries the provision type and `principle`
489
+ * the guideline. Every requirement in the set is listed, since no rule was
482
490
  * written for any of them; the ones a rule speaks to are 'partial'.
491
+ * Assertions are in neither set: they are not needed to conform, and only
492
+ * the people who make the content can make them.
483
493
  */
484
- export function manualReviewChecklist(tier) {
485
- const types = TIER_TYPES[tier];
494
+ export function manualReviewChecklist(set) {
495
+ const types = SET_TYPES[set];
486
496
  if (!types) return [];
487
497
  const partly = new Set(Object.values(RULE_REQUIREMENTS).flat());
488
498
  return provisions
@@ -498,12 +508,13 @@ export function manualReviewChecklist(tier) {
498
508
  }));
499
509
  }
500
510
 
501
- /** The results header for a WCAG 3 audit. */
502
- export function standardFor(tier) {
503
- return { ...draftInfo, tier, tierLabel: TIER_LABELS[tier] };
511
+ /** The results header for a WCAG 3 audit: the draft, the check set and
512
+ * what it covers, as "core requirements". */
513
+ export function standardFor(set) {
514
+ return { ...draftInfo, checkSet: set, scope: SET_SCOPES[set] };
504
515
  }
505
516
 
506
517
  export default {
507
- draftInfo, TIERS, TIER_LABELS, provisions, RULE_REQUIREMENTS, UNMAPPED_RULES,
508
- tierFromTags, requirementsForRule, ruleInTier, ruleMatchesTags, manualReviewChecklist, standardFor,
518
+ draftInfo, CHECK_SETS, provisions, RULE_REQUIREMENTS, UNMAPPED_RULES,
519
+ checkSetOf, checkSetFromTags, requirementsForRule, ruleInCheckSet, ruleMatchesTags, manualReviewChecklist, standardFor,
509
520
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "description": "Accessibility (WCAG 2.2) audit engine written from the W3C specifications — runs entirely in the browser, no dependencies.",
5
5
  "type": "module",
6
6
  "main": "engine/index.js",