@surea11y/core 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +24 -38
  3. package/docs/ACT_RULE_MAPPING.md +8 -6
  4. package/docs/API_STABILITY.md +51 -3
  5. package/docs/BINDING_AUTHORS_GUIDE.md +104 -2
  6. package/docs/DESIGN_CHALLENGES.md +66 -0
  7. package/docs/EARL.md +100 -0
  8. package/docs/ENGINE_OPTIONS.md +28 -2
  9. package/docs/INTEGRATION.md +4 -2
  10. package/docs/LIMITATIONS.md +3 -1
  11. package/docs/OUTPUT_SCHEMA.md +44 -6
  12. package/docs/POLICY.md +1 -1
  13. package/docs/RULE_AUTHORING.md +11 -12
  14. package/docs/RULE_CATALOG.md +76 -26
  15. package/docs/RULE_HELPERS.md +333 -0
  16. package/docs/RULE_TAXONOMY.md +25 -4
  17. package/docs/SARIF.md +21 -2
  18. package/docs/WCAG_CONFORMANCE.md +9 -1
  19. package/package.json +9 -3
  20. package/src/checks/automatic/aria-allowed-attr.js +6 -0
  21. package/src/checks/automatic/aria-allowed-role.js +32 -23
  22. package/src/checks/automatic/aria-braille-equivalent.js +18 -10
  23. package/src/checks/automatic/aria-conditional-attr.js +17 -10
  24. package/src/checks/automatic/aria-deprecated-role.js +12 -0
  25. package/src/checks/automatic/aria-hidden-body.js +1 -1
  26. package/src/checks/automatic/aria-prohibited-attr.js +5 -0
  27. package/src/checks/automatic/aria-prohibited-children.js +6 -6
  28. package/src/checks/automatic/aria-required-attr.js +59 -12
  29. package/src/checks/automatic/aria-required-children.js +33 -16
  30. package/src/checks/automatic/aria-required-parent.js +32 -6
  31. package/src/checks/automatic/aria-role-name-present.js +1 -1
  32. package/src/checks/automatic/aria-roles-valid.js +52 -21
  33. package/src/checks/automatic/aria-valid-attr-value.js +74 -21
  34. package/src/checks/automatic/aria-valid-attr.js +14 -9
  35. package/src/checks/automatic/avoid-inline-spacing.js +133 -6
  36. package/src/checks/automatic/contrast-computable.js +10 -0
  37. package/src/checks/automatic/contrast-enhanced.js +12 -0
  38. package/src/checks/automatic/contrast-minimum.js +12 -0
  39. package/src/checks/automatic/css-orientation-lock.js +42 -5
  40. package/src/checks/automatic/duplicate-id-aria.js +5 -0
  41. package/src/checks/automatic/duplicate-id.js +13 -8
  42. package/src/checks/automatic/form-control-single-label.js +9 -0
  43. package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
  44. package/src/checks/automatic/iframe-focusable-content.js +5 -0
  45. package/src/checks/automatic/label-in-name.js +38 -56
  46. package/src/checks/automatic/link-in-text-block.js +279 -23
  47. package/src/checks/automatic/target-size-minimum.js +84 -5
  48. package/src/checks/automatic/td-has-header.js +19 -18
  49. package/src/checks/manual/form-control-label-quality-manual.js +134 -24
  50. package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
  51. package/src/checks/manual/password-paste-enabled-manual.js +255 -0
  52. package/src/core.js +3863 -44184
  53. package/src/earl.js +144 -0
  54. package/src/sarif.js +22 -2
  55. package/surea11y.browser.js +10 -41039
  56. package/surea11y.i18n.de.js +2 -21
  57. package/surea11y.i18n.es.js +2 -21
  58. package/surea11y.i18n.fr.js +2 -21
  59. /package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +0 -0
@@ -2,29 +2,29 @@
2
2
 
3
3
  Generated from the compiled engine's own catalog (`getChecksCatalog()`/`getRulesCatalog()`) and each rule's source header. Run `node scripts/generate-rule-catalog.js` after `npm run build` to regenerate this file whenever rules change. Do not hand-edit.
4
4
 
5
- **130 rules total: 78 automatic (WCAG-normative, can return `fail`), 52 manual (advisory/judgment-required, capped at `cantTell`). 106 carry at least one formal WCAG Success Criterion mapping.**
5
+ **133 rules total: 79 automatic (WCAG-normative, can return `fail`), 54 manual (advisory/judgment-required, capped at `cantTell`). 107 carry at least one formal WCAG Success Criterion mapping.**
6
6
 
7
7
  The tables below are an index; [rule reference](#rule-reference) carries each rule's description, what it applies to and what it expects.
8
8
 
9
9
  See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`severity` mean on a scan result, and [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for how these roll up to an SC-level conformance claim. For WCAG-facet-level coverage-gap tracking (which parts of an SC are and aren't automatable yet), see `coverage/coverage-report.md` instead: that one is organized by facet, this one by rule.
10
10
 
11
- ## Automatic rules (78), can return `fail`
11
+ ## Automatic rules (79), can return `fail`
12
12
 
13
13
  | Rule ID | Title | WCAG SC | Level | Confidence | Default severity |
14
14
  |---|---|---|---|---|---|
15
15
  | [`area-alt-present`](#area-alt-present) | <area> must have an alt attribute | 1.1.1 | A | high | serious |
16
16
  | [`aria-allowed-attr`](#aria-allowed-attr) | aria-* attributes must be permitted for the element’s role | 4.1.2 | A | medium | moderate |
17
- | [`aria-allowed-role`](#aria-allowed-role) | Explicit role must be permitted for its host element | 4.1.2 | A | high | moderate |
18
- | [`aria-braille-equivalent`](#aria-braille-equivalent) | aria-braillelabel/aria-brailleroledescription must have a non-braille equivalent | 4.1.2 | A | high | serious |
19
- | [`aria-conditional-attr`](#aria-conditional-attr) | aria-errormessage requires aria-invalid to be set to a non-false value | 4.1.2 | A | high | serious |
17
+ | [`aria-allowed-role`](#aria-allowed-role) | Explicit role must be permitted for its host element | — | — | high | moderate |
18
+ | [`aria-braille-equivalent`](#aria-braille-equivalent) | aria-braillelabel/aria-brailleroledescription must have a non-braille equivalent | 4.1.2 | A | high | moderate |
19
+ | [`aria-conditional-attr`](#aria-conditional-attr) | aria-errormessage requires aria-invalid to be set to a non-false value | 4.1.2 | A | high | moderate |
20
20
  | [`aria-deprecated-role`](#aria-deprecated-role) | role attribute should not use a deprecated or author-discouraged ARIA role | 4.1.2 | A | high | moderate |
21
21
  | [`aria-hidden-body`](#aria-hidden-body) | The document <body> must not be aria-hidden | 1.3.1, 4.1.2 | A | high | critical |
22
22
  | [`aria-hidden-focus`](#aria-hidden-focus) | ARIA hidden elements must not be focusable | 2.4.7, 4.1.2 | AA | high | serious |
23
23
  | [`aria-prohibited-attr`](#aria-prohibited-attr) | ARIA naming attributes must not be used on roles that prohibit them | 4.1.2 | A | high | moderate |
24
- | [`aria-prohibited-children`](#aria-prohibited-children) | Container roles must not own a child with a disallowed role | 4.1.2 | A | medium | moderate |
24
+ | [`aria-prohibited-children`](#aria-prohibited-children) | Container roles must not own a child with a disallowed role | 1.3.1 | A | medium | moderate |
25
25
  | [`aria-required-attr`](#aria-required-attr) | Roles with a required ARIA state/property must carry it | 4.1.2 | A | high | serious |
26
- | [`aria-required-children`](#aria-required-children) | Container roles must own at least one required child role | 4.1.2 | A | medium | moderate |
27
- | [`aria-required-parent`](#aria-required-parent) | Roles requiring a specific context role must be in that context | 4.1.2 | A | medium | moderate |
26
+ | [`aria-required-children`](#aria-required-children) | Container roles must own at least one required child role | 1.3.1 | A | medium | moderate |
27
+ | [`aria-required-parent`](#aria-required-parent) | Roles requiring a specific context role must be in that context | 1.3.1 | A | medium | moderate |
28
28
  | [`aria-role-name-present`](#aria-role-name-present) | ARIA roles that require an accessible name have one | 4.1.2 | A | high | serious |
29
29
  | [`aria-roles-valid`](#aria-roles-valid) | role attribute must be a valid, non-abstract ARIA role | 4.1.2 | A | high | serious |
30
30
  | [`aria-valid-attr`](#aria-valid-attr) | aria-* attributes must be real, defined ARIA attributes | 4.1.2 | A | high | serious |
@@ -50,6 +50,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
50
50
  | [`form-control-single-label`](#form-control-single-label) | Form controls must not have multiple labels | 3.3.2 | A | high | moderate |
51
51
  | [`html-lang-attr-present`](#html-lang-attr-present) | Page language is declared | 3.1.1 | A | high | serious |
52
52
  | [`html-xml-lang-mismatch`](#html-xml-lang-mismatch) | lang and xml:lang must not disagree | 3.1.1 | A | high | serious |
53
+ | [`identical-iframes-same-purpose`](#identical-iframes-same-purpose) | Frames with the same name embed the same resource | 4.1.2 | A | medium | moderate |
53
54
  | [`iframe-focusable-content`](#iframe-focusable-content) | Frames with tabindex="-1" must not contain focusable content | 2.1.1 | A | high | moderate |
54
55
  | [`iframe-name-present`](#iframe-name-present) | Frames have an accessible name | 4.1.2 | A | high | serious |
55
56
  | [`iframe-title-unique`](#iframe-title-unique) | Frame titles must be unique | 4.1.2 | A | high | moderate |
@@ -91,7 +92,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
91
92
  | [`valid-lang`](#valid-lang) | Element lang attribute must be syntactically valid | 3.1.2 | AA | high | moderate |
92
93
  | [`video-poster-text-alternative-present`](#video-poster-text-alternative-present) | <video> poster must have a text alternative | 1.1.1 | A | medium | serious |
93
94
 
94
- ## Manual rules (52), advisory, capped at `cantTell`
95
+ ## Manual rules (54), advisory, capped at `cantTell`
95
96
 
96
97
  | Rule ID | Title | WCAG SC | Level | Confidence | Default severity |
97
98
  |---|---|---|---|---|---|
@@ -120,6 +121,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
120
121
  | [`input-image-alt-quality`](#input-image-alt-quality) | <input type="image"> alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
121
122
  | [`label-title-only`](#label-title-only) | Form controls should not use title as their only label | — | — | medium | minor |
122
123
  | [`landmark-banner-is-top-level`](#landmark-banner-is-top-level) | Banner landmark must be top-level | — | — | medium | minor |
124
+ | [`landmark-complementary-is-top-level`](#landmark-complementary-is-top-level) | Complementary landmark must be top-level | — | — | medium | minor |
123
125
  | [`landmark-contentinfo-is-top-level`](#landmark-contentinfo-is-top-level) | Contentinfo landmark must be top-level | — | — | medium | minor |
124
126
  | [`landmark-main-is-top-level`](#landmark-main-is-top-level) | Main landmark must be top-level | — | — | medium | minor |
125
127
  | [`landmark-no-duplicate-banner`](#landmark-no-duplicate-banner) | Page must not have more than one banner landmark | — | — | medium | minor |
@@ -137,6 +139,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
137
139
  | [`p-as-heading`](#p-as-heading) | A <p> styled to look like a heading should probably be a real heading | 1.3.1 | A | low | minor |
138
140
  | [`page-has-heading-one`](#page-has-heading-one) | Page should have a level-one heading | — | — | medium | minor |
139
141
  | [`page-title-patterns`](#page-title-patterns) | Page title patterns that may be insufficiently descriptive | 2.4.2 | A | medium | minor |
142
+ | [`password-paste-enabled`](#password-paste-enabled) | Authentication fields must not block pasting | 3.3.8 | AA | medium | serious |
140
143
  | [`presentation-role-conflict`](#presentation-role-conflict) | Presentational role must not conflict with a global ARIA attribute or focusability | — | — | medium | minor |
141
144
  | [`region`](#region) | Page content should be inside a landmark region | — | — | medium | minor |
142
145
  | [`scope-attr-valid`](#scope-attr-valid) | scope attribute must have a valid value | — | — | medium | minor |
@@ -148,7 +151,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
148
151
  | [`table-fake-caption`](#table-fake-caption) | A table's first row should not stand in for a real <caption> | 1.3.1 | A | low | minor |
149
152
  | [`video-caption`](#video-caption) | Prerecorded video should provide a captions track | 1.2.2 | A | low | moderate |
150
153
 
151
- ## Composite (WCAG-SC rollup) rules (33)
154
+ ## Composite (WCAG-SC rollup) rules (34)
152
155
 
153
156
  Composite rules aren't individually authored. They're generated rollups over the atomic rules above, one per WCAG Success Criterion with automatable coverage. See [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for rollup semantics.
154
157
 
@@ -157,7 +160,7 @@ Composite rules aren't individually authored. They're generated rollups over the
157
160
  | `wcag-1.1.1-non-text-content` | Non-text content: text alternatives | Rollup of checks ensuring non-text content has an appropriate text alternative. | 1.1.1 | A | 22 |
158
161
  | `wcag-1.2.1-audio-only-video-only-prerecorded` | Audio-only and video-only (prerecorded): transcript | Rollup of checks for transcript availability for prerecorded audio-only/video-only media. | 1.2.1 | A | 1 |
159
162
  | `wcag-1.2.2-captions-prerecorded` | Captions (Prerecorded) | Rollup of checks for captions-track evidence on prerecorded video. | 1.2.2 | A | 1 |
160
- | `wcag-1.3.1-info-and-relationships` | Info and Relationships | Rollup of checks ensuring information, structure, and relationships conveyed through presentation are programmatically determinable. | 1.3.1 | A | 11 |
163
+ | `wcag-1.3.1-info-and-relationships` | Info and Relationships | Rollup of checks ensuring information, structure, and relationships conveyed through presentation are programmatically determinable. | 1.3.1 | A | 14 |
161
164
  | `wcag-1.3.4-orientation` | Orientation | Rollup of checks ensuring content does not restrict its view to a single display orientation. | 1.3.4 | AA | 1 |
162
165
  | `wcag-1.3.5-identify-input-purpose` | Identify Input Purpose | Rollup of checks ensuring the autocomplete attribute correctly identifies input purpose. | 1.3.5 | AA | 1 |
163
166
  | `wcag-1.4.1-use-of-color` | Use of Color | Rollup of checks ensuring color is not used as the only visual means of conveying information. | 1.4.1 | A | 1 |
@@ -184,9 +187,10 @@ Composite rules aren't individually authored. They're generated rollups over the
184
187
  | `wcag-3.1.2-language-of-parts` | Language of Parts | Rollup of checks ensuring elements whose language differs from the page default declare it correctly. | 3.1.2 | AA | 1 |
185
188
  | `wcag-3.2.5-change-on-request` | Change on Request | Rollup of checks ensuring context changes only happen at the user's request (AAA). | 3.2.5 | AAA | 1 |
186
189
  | `wcag-3.3.2-labels-or-instructions` | Labels or Instructions | Rollup of checks ensuring form controls have unambiguous labeling. | 3.3.2 | A | 2 |
190
+ | `wcag-3.3.8-accessible-authentication-minimum` | Accessible Authentication (Minimum) | Rollup of checks ensuring an authentication step leaves the mechanisms that help a user through it in place. | 3.3.8 | AA | 1 |
187
191
  | `wcag-4.1.1-parsing` | Parsing | Rollup of checks ensuring id values are unique. WCAG 2.0/2.1 only: SC 4.1.1 was removed in WCAG 2.2, so this composite carries the wcag22-removed tag. | 4.1.1 | A | 1 |
188
- | `wcag-4.1.2-aria-validity` | Name, role, value: ARIA validity | Rollup of checks that ARIA role and attribute usage conforms to the WAI-ARIA specification (valid roles, valid attributes, valid values, required attributes/relationships, unique ARIA-referenced ids). | 4.1.2 | A | 17 |
189
- | `wcag-4.1.2-name` | Name, role, value: accessible name | Rollup of checks that common interactive elements expose a non-empty accessible name. | 4.1.2 | A | 23 |
192
+ | `wcag-4.1.2-aria-validity` | Name, role, value: ARIA validity | Rollup of checks that ARIA role and attribute usage conforms to the WAI-ARIA specification (valid roles, valid attributes, valid values, required attributes, unique ARIA-referenced ids). | 4.1.2 | A | 13 |
193
+ | `wcag-4.1.2-name` | Name, role, value: accessible name | Rollup of checks that common interactive elements expose a non-empty accessible name. | 4.1.2 | A | 24 |
190
194
 
191
195
  ## Rule reference
192
196
 
@@ -256,19 +260,19 @@ Checks that every recognized aria-* attribute present on an element with an expl
256
260
 
257
261
  **Explicit role must be permitted for its host element**
258
262
 
259
- automatic · WCAG 4.1.2 (A) · confidence high · default severity moderate
263
+ automatic · no formal WCAG SC mapping · confidence high · default severity moderate
260
264
 
261
265
  Checks that an explicit role="" attribute is one of the roles the ARIA-in-HTML specification permits for the host element (e.g. role="tab" is not permitted on <nav>).
262
266
 
263
267
  **Applies to.** Applies to elements with an explicit, valid, non-abstract role, where the host element/attribute combination has an asserted permitted-roles constraint in the ARIA-in-HTML table (src/core/aria-helpers.js ALLOWED_ROLES_BY_ELEMENT).
264
268
 
265
- **Expectation.** The explicit role is one of the roles the ARIA-in-HTML specification permits for that host element.
269
+ **Expectation.** The explicit role is one of the roles the ARIA-in-HTML specification permits for that host element. Reported at CANTTELL rather than FAIL: ARIA-in-HTML's permitted-roles table is an author conformance requirement with no ACT rule and no WCAG mapping in any source. The role the author asked for is still the role assistive technology exposes, so whether the combination harms anyone depends on the widget, not on the table.
266
270
 
267
271
  ### `aria-braille-equivalent`
268
272
 
269
273
  **aria-braillelabel/aria-brailleroledescription must have a non-braille equivalent**
270
274
 
271
- automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
275
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity moderate
272
276
 
273
277
  Checks that elements using aria-braillelabel also have a regular accessible name, and elements using aria-brailleroledescription also have aria-roledescription.
274
278
 
@@ -281,7 +285,7 @@ Per the ARIA specification, `aria-braillelabel` is a Braille-specific SUPPLEMENT
281
285
  - a non-empty accessible name from a non-braille mechanism, if it declares `aria-braillelabel`;
282
286
  - a non-empty `aria-roledescription`, if it declares `aria-brailleroledescription`.
283
287
 
284
- Using either braille-specific attribute as the ONLY naming mechanism leaves non-braille assistive technology (most screen readers, voice control, etc.) with no accessible name/role description at all.
288
+ Using either braille-specific attribute as the ONLY naming mechanism leaves non-braille assistive technology (most screen readers, voice control, etc.) with no accessible name/role description at all. Reported at CANTTELL rather than FAIL: aria-brailleroledescription without aria-roledescription reaches no user at all, and a missing accessible name is the naming rules' decision for the roles that require one. The braille attribute being unpaired is worth surfacing, but it is not itself a criterion failing.
285
289
 
286
290
  ### `aria-checked-state-mismatch`
287
291
 
@@ -299,13 +303,13 @@ Flags a native <input type="checkbox">/<input type="radio"> whose ex
299
303
 
300
304
  **aria-errormessage requires aria-invalid to be set to a non-false value**
301
305
 
302
- automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
306
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity moderate
303
307
 
304
308
  Checks that elements with aria-errormessage also have aria-invalid set to "true", "grammar", or "spelling"; otherwise the error message is dropped from the accessibility tree.
305
309
 
306
310
  **Applies to.** Elements with a non-empty `aria-errormessage` attribute.
307
311
 
308
- **Expectation.** Per the ARIA specification, `aria-errormessage` is only exposed to assistive technology when `aria-invalid` is present with a value other than `"false"` (i.e. `"true"`, `"grammar"`, or `"spelling"`). An element with `aria-errormessage` but `aria-invalid` absent or `"false"` silently drops the error message from the accessibility tree, authors almost always intend it to be exposed.
312
+ **Expectation.** Per the ARIA specification, `aria-errormessage` is only exposed to assistive technology when `aria-invalid` is present with a value other than `"false"` (i.e. `"true"`, `"grammar"`, or `"spelling"`). An element with `aria-errormessage` but `aria-invalid` absent or `"false"` silently drops the error message from the accessibility tree, authors almost always intend it to be exposed. Reported at CANTTELL rather than FAIL: aria-errormessage is only exposed once aria-invalid is set, so the reference is currently inert. Whether that costs the user anything depends on whether the message is conveyed some other way (visible text next to the field, aria-describedby), which static markup does not settle.
309
313
 
310
314
  ### `aria-deprecated-role`
311
315
 
@@ -372,7 +376,7 @@ Checks that aria-label/aria-labelledby are not present on WAI-ARIA roles whose s
372
376
 
373
377
  **Container roles must not own a child with a disallowed role**
374
378
 
375
- automatic · WCAG 4.1.2 (A) · confidence medium · default severity moderate
379
+ automatic · WCAG 1.3.1 (A) · confidence medium · default severity moderate
376
380
 
377
381
  Checks that every accessible-tree-owned child of a container role (list, listbox, menu, menubar, radiogroup, rowgroup, table, grid, treegrid, tablist, tree, row) has one of that role's allowed owned roles.
378
382
 
@@ -390,25 +394,30 @@ Checks that elements with an explicit role carry every unambiguous, context-inde
390
394
 
391
395
  **Applies to.** Applies to elements with an explicit, valid, non-abstract role that is also one of the small set of roles with a documented, context- independent required state/property (checkbox, combobox, heading, menuitemcheckbox, menuitemradio, meter, radio, scrollbar, separator, slider, switch) -- except when that explicit role is identical to the element's own native/implicit role (ACT 4e8ab6: e.g. <input type="checkbox" role="checkbox">, which is exempt because the native control's own state exposure already covers it; no aria-checked is required. helpers.aria.getNativeRoleForElement resolves this).
392
396
 
393
- **Expectation.** Every required aria-* attribute for that role is present (and non-empty).
397
+ **Expectation.**
398
+
399
+ Every required state/property for that role is present and non-empty. Graded by whether ARIA supplies a stand-in for the missing attribute:
400
+
401
+ - FAIL where it does not, so the state is simply not exposed (aria-checked on checkbox/radio/switch/menuitemcheckbox/menuitemradio, aria-valuenow on slider/scrollbar/meter and on a focusable separator).
402
+ - CANTTELL where ARIA defines an implicit value the role falls back to (aria-expanded on combobox, aria-level on heading), so the role still exposes a value and only the author knows whether it is the right one.
394
403
 
395
404
  ### `aria-required-children`
396
405
 
397
406
  **Container roles must own at least one required child role**
398
407
 
399
- automatic · WCAG 4.1.2 (A) · confidence medium · default severity moderate
408
+ automatic · WCAG 1.3.1 (A) · confidence medium · default severity moderate
400
409
 
401
410
  Checks that container roles with a documented "required owned elements" entry (list, listbox, menu, radiogroup, table, grid, tablist, tree, row, ...) contain at least one descendant or aria-owns-referenced element with an acceptable owned role.
402
411
 
403
412
  **Applies to.** Applies to elements with an explicit, valid, non-abstract role that is also one of the container roles with a documented "required owned elements" entry (list, listbox, menu, menubar, radiogroup, rowgroup, table, grid, treegrid, tablist, tree, row).
404
413
 
405
- **Expectation.** At least one descendant, or one aria-owns-referenced element, has one of the acceptable owned roles for that container role.
414
+ **Expectation.** At least one descendant, or one aria-owns-referenced element, has one of the acceptable owned roles for that container role. Reported at CANTTELL, never FAIL: this rule asks only whether the required content is PRESENT, and a container that owns nothing conveys nothing false -- an empty role="list" is announced as a list with no items, which is what it is. Whether the content a container does own is VALID is aria-prohibited-children's decision, and that rule still fails, so a genuinely misdescribed structure (a role="button" among list items, a tablist of plain buttons) is caught with the same strength as before. The native-HTML equivalents already work this way: nothing in this ruleset fails an empty <ul>, and list-children-valid judges only the children that exist.
406
415
 
407
416
  ### `aria-required-parent`
408
417
 
409
418
  **Roles requiring a specific context role must be in that context**
410
419
 
411
- automatic · WCAG 4.1.2 (A) · confidence medium · default severity moderate
420
+ automatic · WCAG 1.3.1 (A) · confidence medium · default severity moderate
412
421
 
413
422
  Checks that roles with a documented "required context role" entry (listitem, option, tab, treeitem, row, cell, ...) have an ancestor or aria-owns owner with an acceptable context role.
414
423
 
@@ -438,7 +447,12 @@ Checks that an explicit role="" attribute resolves to a real, non-abstract WAI-A
438
447
 
439
448
  **Applies to.** Applies to any element with a non-empty role="" attribute in the composed DOM.
440
449
 
441
- **Expectation.** The role attribute's first token (the role actually used by assistive technology; later space-separated tokens are author-supplied fallbacks and are not evaluated here) must be a real WAI-ARIA role name, and must not be an abstract role (abstract roles exist only for the specification's own role taxonomy and must never be used directly in markup).
450
+ **Expectation.**
451
+
452
+ At least one role token names a concrete, non-abstract ARIA role. Graded by what the element falls back to when none does:
453
+
454
+ - FAIL on a roleless host (div, span, custom element), which is left exposed as generic, so the role the author meant reaches no one.
455
+ - CANTTELL where the element has a native role (a <button>, <nav>, <a href>), which the accessibility tree keeps using. ACT 674b10 lists 4.1.2 as a secondary requirement only, "satisfied through the implicit role," so the bad token is worth reporting but is not itself the criterion failing.
442
456
 
443
457
  ### `aria-text`
444
458
 
@@ -462,7 +476,7 @@ Checks that every aria-* attribute name present in the DOM is a real attribute d
462
476
 
463
477
  **Applies to.** Applies to any element in the composed DOM that carries at least one attribute whose name starts with "aria-".
464
478
 
465
- **Expectation.** Each aria-* attribute name is a real attribute defined by the WAI-ARIA specification (catches typos / made-up attribute names, which are silently ignored by assistive technology and therefore a real, deterministic defect).
479
+ **Expectation.** Each aria-* attribute name is a real attribute defined by the WAI-ARIA specification (catches typos / made-up attribute names, which are silently ignored by assistive technology and therefore a real, deterministic defect). Reported at CANTTELL rather than FAIL: an aria-* attribute the spec does not define is inert, so nothing about the element's exposed name, role or value changes because it is there. Where the author meant a real attribute and the element ends up without a name, that absence is the naming rules' decision, not this one's. ACT 5f99a7 maps 1.3.1/4.1.2 as secondary requirements, "less strict" than the rule itself.
466
480
 
467
481
  ### `aria-valid-attr-value`
468
482
 
@@ -908,6 +922,18 @@ Checks that the <html> element's lang and xml:lang attributes declare the
908
922
 
909
923
  **Expectation.** The primary language subtag (the part before the first "-") of lang and xml:lang match, case-insensitively. When both attributes are present but declare different languages, assistive technology and user agents may resolve the page's language inconsistently.
910
924
 
925
+ ### `identical-iframes-same-purpose`
926
+
927
+ **Frames with the same name embed the same resource**
928
+
929
+ automatic · WCAG 4.1.2 (A) · confidence medium · default severity moderate
930
+
931
+ Checks that <iframe>/<frame> elements sharing an accessible name embed the same resource, since one name can only describe one resource.
932
+
933
+ **Applies to.** Applies to each set of two or more <iframe>/<frame> elements that are included in the accessibility tree and share the same non-empty accessible name, compared with whitespace collapsed. A frame named only by a mechanism that names nothing, or hidden from the accessibility tree, is not part of a set; a set needs two surviving members to exist at all.
934
+
935
+ **Expectation.** Every frame in a set resolves to the same resource. A shared name describes one resource, so two frames answering to it must embed the same one.
936
+
911
937
  ### `identical-links-same-purpose`
912
938
 
913
939
  **Links with the same accessible name should lead to the same destination**
@@ -1076,6 +1102,18 @@ Checks that the banner landmark (role="banner" or a non-nested <header>) i
1076
1102
 
1077
1103
  **Expectation.** No banner candidate has an ancestor that is itself any landmark region. A banner nested inside another landmark is not a top-level, whole-page banner and confuses landmark-based navigation for assistive technology users.
1078
1104
 
1105
+ ### `landmark-complementary-is-top-level`
1106
+
1107
+ **Complementary landmark must be top-level**
1108
+
1109
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1110
+
1111
+ Checks that the complementary landmark (role="complementary" or an <aside> that keeps its implicit role) is not nested inside another landmark region.
1112
+
1113
+ **Applies to.** Applies whenever the page contains at least one element carrying the complementary role: explicit role="complementary", or an <aside> that keeps its implicit role (see implementation notes on when it does not).
1114
+
1115
+ **Expectation.** No complementary candidate has an ancestor that is itself a landmark region. Complementary content supports the main content of the page and sits beside it; nested inside another landmark it is a section of that landmark instead, which is not what landmark navigation announces.
1116
+
1079
1117
  ### `landmark-contentinfo-is-top-level`
1080
1118
 
1081
1119
  **Contentinfo landmark must be top-level**
@@ -1463,6 +1501,18 @@ Checks that the page includes a non-empty <title> element.
1463
1501
 
1464
1502
  **Expectation.** The document has a <title> element, and document.title with whitespace collapsed is non-empty. The element is looked for anywhere in the document, not only inside <head>: a <title> the parser leaves outside <head> is still the document title in every browser. Whether that title describes the page is page-title-patterns' question.
1465
1503
 
1504
+ ### `password-paste-enabled`
1505
+
1506
+ **Authentication fields must not block pasting**
1507
+
1508
+ manual · WCAG 3.3.8 (AA) · confidence medium · default severity serious
1509
+
1510
+ Checks that a password or one-time-code field carries no inline paste handler that cancels the paste, which would remove the password manager or clipboard that WCAG 3.3.8 relies on as the assisting mechanism.
1511
+
1512
+ **Applies to.** Applies to any control whose autocomplete token is current-password, new-password or one-time-code, and to <input type="password"> unless its autocomplete names another purpose. A disabled or readonly field takes no input to block, and one outside the accessibility tree is not being asked for, so neither is in scope.
1513
+
1514
+ **Expectation.** A reviewer confirms the field can still be pasted into. Remembering a password is a cognitive function test, and 3.3.8 asks for a mechanism that helps the user through one; a password manager, or the clipboard for a one-time code, is that mechanism.
1515
+
1466
1516
  ### `presentation-role-conflict`
1467
1517
 
1468
1518
  **Presentational role must not conflict with a global ARIA attribute or focusability**
@@ -0,0 +1,333 @@
1
+ # RULE_HELPERS.md — `ctx.helpers` Reference (Canonical, repo-derived)
2
+
3
+ This is the full reference for `ctx.helpers`, the object every rule's `runInPage(ctx)`
4
+ receives (`const { helpers } = ctx`, per [`RULE_AUTHORING.md`](./RULE_AUTHORING.md) §6).
5
+ It exists because that doc's own helpers section only calls out the dozen or so most
6
+ common ones — the underlying object (`createDomHelpers()` in `src/core/dom-helpers.js`)
7
+ exports around 35, and several of them replace logic a new rule would otherwise
8
+ duplicate (often incorrectly — see the naming helpers below).
9
+
10
+ Read [`RULE_AUTHORING.md`](./RULE_AUTHORING.md) first for the rule contract itself
11
+ (module shape, `runInPage` constraints, occurrence reporting). This doc only covers
12
+ what each helper does and when to reach for it.
13
+
14
+ All helpers below are called as `helpers.<name>(...)` from inside `runInPage`. None of
15
+ them may be assigned to a module-scope variable and closed over — same
16
+ serialization constraint as everything else in `runInPage` (§1 of `RULE_AUTHORING.md`).
17
+
18
+ ---
19
+
20
+ ## 1) Query & traversal
21
+
22
+ ### `queryAll(selector)` → `Element[]`
23
+ Plain `querySelectorAll(selector)` across the resolved context root(s), deduped, with
24
+ self-match included (a root element matching `selector` itself is returned, which
25
+ `querySelectorAll` alone never does). Light DOM only.
26
+
27
+ ### `queryAllDeep(selector)` → `Element[]`
28
+ Same as `queryAll`, but also descends into open shadow roots (BFS over discovered
29
+ `shadowRoot`s). Ignores `includeShadowDom`/hidden-content policy — it's the raw
30
+ traversal `queryAllSmart` builds on.
31
+
32
+ ### `queryAllSmart(selector)` → `Element[]`
33
+ **The one almost every rule should use.** Honors `engineOptions.includeShadowDom`
34
+ (shadow-aware by default), applies the default hidden-content policy (filters out
35
+ `display:none`/`hidden`/etc. unless `includeHiddenElements:true`), and applies any
36
+ rule-scoped `excludeSelectors`. See `RULE_AUTHORING.md` §6.1 — write rules assuming
37
+ open shadow roots are in scope and let this helper honor the caller's choice.
38
+
39
+ ```js
40
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img') : helpers.queryAll('img');
41
+ ```
42
+
43
+ ### `composedParent(node)` → `Node | null`
44
+ One step up the *flat tree*: `assignedSlot` first (a slotted node's rendered parent is
45
+ its slot, not its light-DOM `parentNode`), then `parentNode`, then `.host` once you're
46
+ at a `ShadowRoot`. Use this instead of `parentNode`/`closest` for any ancestor walk that
47
+ must work correctly across shadow boundaries and slots.
48
+
49
+ ### `buildSimpleSelector(el, fallbackTag)` → `string`
50
+ A short, non-unique selector for an element: `#id`, else a `data-testid`/`data-test`/
51
+ `data-cy`/`data-qa` attribute selector, else `tag[name="..."]`, else just the tag name.
52
+ Cheap; not guaranteed unique. Prefer `reportOccurrence` (§6 below) over building
53
+ selectors by hand — see the perf note there.
54
+
55
+ ### `buildSelector(el)` → `string`
56
+ The engine's real, best-effort-unique CSS selector builder (cached per element per
57
+ run). Used internally by `reportOccurrence`'s finalization; rules generally don't need
58
+ to call this directly.
59
+
60
+ ### `getOuterHtmlSnippet(el)` → `string`
61
+ `el.outerHTML`, truncated to 2000 characters (with a trailing `…`) and cached per
62
+ element per run. `reportOccurrence` (§6 below) already fills in an occurrence's `html`
63
+ from the reported element, so most rules never call this directly — it's for the rare
64
+ case where a rule needs the HTML snippet itself, not just an occurrence carrying it.
65
+
66
+ ### `isExcluded(el)` → `boolean`
67
+ Whether `el` matches the currently effective `excludeSelectors` (global config ∪ the
68
+ active rule's own `engineOptions.rules[ruleId].excludeSelectors`), via `closest()`.
69
+ `queryAllSmart` already applies this filtering for you; reach for `isExcluded` directly
70
+ only if a rule walks the DOM some other way (e.g. following `composedParent`) and still
71
+ needs to respect exclusions on nodes found off that path.
72
+
73
+ ### `buildStructuralPath(node, selector)` → `number[] | null`
74
+ Sibling-index path from the document root to `node` (or, given only a `selector`,
75
+ re-resolves the element first — the same fallback `reportOccurrence` triggers, and the
76
+ same one `perfStats.counters['structuralPath.selectorFallback']` counts, per
77
+ `RULE_AUTHORING.md` §4.3). Rules don't normally call this either — it's what backs
78
+ `selector`/`structuralPath` finalization for reported occurrences.
79
+
80
+ ---
81
+
82
+ ## 2) Eligibility & visibility
83
+
84
+ These answer "is this node in scope" from different angles. They are easy to reach for
85
+ the wrong one, so the distinctions matter:
86
+
87
+ ### `isAccTreeEligible(node)` → `{ eligible, reasons[] }`
88
+ Whether a node is eligible for the accessibility tree, per an ordered set of checks
89
+ (display/visibility, `hidden`, `inert`, closed `<details>`, template content, etc.).
90
+ Deliberately keeps a focusable-but-`aria-hidden` element *eligible* — see the header
91
+ comment on `isIncludedInAccessibilityTree` below for why.
92
+
93
+ ### `isIncludedInAccessibilityTree(el)` → `boolean`
94
+ The narrower question most accessible-name rules actually want: `isAccTreeEligible`,
95
+ minus anything eligible only because of an `ariaHiddenOverridden*` reason. ACT scopes
96
+ name-computation rules (button-name, link-name, etc.) to elements genuinely included in
97
+ the accessibility tree; a focusable element hidden by `aria-hidden` is a real
98
+ issue, but it's `aria-hidden-focus`'s issue (WCAG 4.1.2), not a naming rule's — so
99
+ naming rules should check this, not `isAccTreeEligible`, to avoid double-flagging the
100
+ same element under the wrong rule.
101
+
102
+ ### `isDomVisibleEligible(node, ctx, opts)` → `{ eligible, reasons[], metrics }`
103
+ Style/geometry-only visibility (not accessibility-tree membership): `display`,
104
+ `visibility`, size/position, opacity, etc. `opts.visibilityMode` (`'styleOnly'` |
105
+ `'styleAndGeometry'`), `opts.disableGeometry`, `opts.ignoreOpacity` tune what's checked.
106
+ Use when a rule cares about visual rendering specifically, not AT exposure.
107
+
108
+ ### `getEligibilityInfo(node, ctx, opts)` → `{ eligible, reasons[], targetSet, accEligible }`
109
+ A single wrapper choosing between the two above via `opts.targetSet` (`'acc'` |
110
+ `'dom'`, default `'dom'`). **This is also the shape `RULE_AUTHORING.md` §7 requires every
111
+ occurrence to carry** as `data.visibilityFilter` — call this once per element and pass
112
+ its result straight through.
113
+
114
+ ### `getVisibilityHintsInfo(el, ctx, opts)` → `{ hints[], metrics, flags[] }`
115
+ Style-only visibility *hints* for triage/diagnostics (`opacityZero`, `clipped`, etc.) —
116
+ explicitly does **not** decide eligibility; a rule's own logic still owns the outcome.
117
+
118
+ ### `isWholeDocumentScope()` → `boolean`
119
+ `true` unless `engineOptions.fragment: true` was set, or `contextSelector` scoped the
120
+ run narrower than the whole document. Required for any rule checking a page-wide,
121
+ one-per-document property (`<title>`, `<html lang>`, landmark structure) — gate it with
122
+ an `applicability(ctx)` export using this, per `RULE_AUTHORING.md` §4.2/§11.2, or the
123
+ rule will wrongly fault a scoped subtree for lacking something it was never meant to
124
+ have.
125
+
126
+ ### `hasTruncatedAncestorWalk` (internal)
127
+ Backs confidence scoring during result normalization when a 200-step ancestor walk
128
+ didn't reach the root. Not something a rule calls directly.
129
+
130
+ ---
131
+
132
+ ## 3) Accessible naming & labeling
133
+
134
+ Naming is layered — each function below builds on the ones above it — and several exist
135
+ specifically to replace a naive reimplementation that gets an edge case wrong. Reach for
136
+ the highest-level one that answers your actual question before dropping to a primitive.
137
+
138
+ ### `getAriaLabelInfo(el)` → `{ present, value, mechanism, flags[] }`
139
+ Just `aria-label`, trimmed, presence-checked.
140
+
141
+ ### `getAriaLabelledByInfo(el, ctx, opts)` → `{ present, value, mechanism, refsCount, missing[], flags[] }`
142
+ Resolves `aria-labelledby` through `getTextFromIdRefs` (recursive, accname-aligned — see
143
+ §4 below), not raw `textContent`.
144
+
145
+ ### `getAriaNameInfo(el, ctx, opts)` → `{ present, value, mechanism, flags[] }`
146
+ ARIA-only name with correct precedence: `aria-labelledby` (if it resolves non-empty)
147
+ wins over `aria-label`. Use this rather than checking the two attributes yourself when
148
+ you specifically want "does ARIA name this," excluding native `<label>`/content/title.
149
+
150
+ ### `getLandmarkNameInfo(el, ctx)` → `{ present, value, mechanism, flags[] }`
151
+ Landmark-role naming (`nav`/`main`/`region`/`banner`/`contentinfo`, etc.): ARIA name,
152
+ then `title` — landmark roles don't get a name from content, and `title` must be
153
+ included or two landmarks distinguished only by `title` both read as unnamed and get
154
+ flagged as duplicates. Shared by all 7 landmark rule files; use this rather than
155
+ reimplementing landmark naming in a new landmark rule.
156
+
157
+ ### `getAccessibleNameInfo(el, ctx, opts)` → `{ present, value, mechanism, flags[] }`
158
+ The general-purpose accessible name: ARIA name → native `<label>` association → `alt`
159
+ (image-like elements) → `title` (flagged `title-used` — see the policy note in the
160
+ source; `title` is accepted per spec but is a weak mechanism in practice). This is what
161
+ almost every naming rule should call.
162
+
163
+ ### `getAccessibleDescriptionInfo(el, ctx, opts)` → `{ present, value, mechanism, flags[] }`
164
+ `aria-describedby` (via `getTextFromIdRefs`), then optionally `title` if
165
+ `opts.allowTitle === true`.
166
+
167
+ ### `getTextAlternativeInfo(el, ctx, opts)` → `{ present, value, mechanism, requiredMechanism, flags[] }`
168
+ Mechanism-aware text alternative for elements with a *specific required* mechanism:
169
+ `alt` for `img`/`area`/`input[type=image]` (missing `alt` is flagged even when an
170
+ accessible name exists elsewhere — that's still a real alt-text violation), fallback
171
+ content or ARIA/`title` for `<canvas>`. Use this for alt-text-shaped rules, not
172
+ `getAccessibleNameInfo`, when the rule cares which mechanism was used, not just whether
173
+ a name exists.
174
+
175
+ ### `getContentNameInfo(el, ctx, opts)` → `{ present, value, mechanism, flags[] }`
176
+ Recursive "name from content" (accname step 2F): walks children using each child's
177
+ *own* accessible name (not just literal text), so `<a href="…"><img alt="Company
178
+ Name"></a>` and `<button><span aria-label="Close"></span></button>` both name correctly.
179
+ A plain `TreeWalker(SHOW_TEXT)` walk misses both.
180
+
181
+ ### `getAssociatedLabelElements(el)` → `Element[]`
182
+ Real `<label>` element(s) associated with `el` — a `<label for="id">` pointing at it,
183
+ plus a wrapping `<label>` whose first labelable descendant it is. **Does not call the
184
+ native `.labels`/`.control` API** — in this project's supported jsdom runtime,
185
+ `.labels` is an expensive whole-document walk per element (`.control` resolution is
186
+ another one), which used to dominate whole-engine runtime on form-heavy pages. Use this
187
+ whenever a rule needs the actual label element(s), not just a yes/no.
188
+
189
+ ### `labelContributesAccessibleName(labelEl)` → `boolean`
190
+ Whether a `<label>` element itself carries text that would name its control: own
191
+ ARIA name, else rendered content (`getContentNameInfo`, so `aria-hidden`/`display:none`
192
+ descendants are correctly excluded), else its own `title`. Shared by
193
+ `form-control-single-label` and `form-control-programmatic-label-present` so they agree
194
+ on what "a label with content" means.
195
+
196
+ ### `getLabelMethod(el, ctx, opts)` → `{ method, value }`
197
+ Which mechanism actually labels `el` — `'label'` | `'aria-labelledby'` |
198
+ `'aria-label'` | `'title'` | `'placeholder'` | `'none'` — checked in that precedence
199
+ order.
200
+
201
+ ### `getLabelStrength(method)` → `'strong' | 'medium' | 'weak' | 'none'`
202
+ Policy classification of a `getLabelMethod` result (`label`/`aria-labelledby` →
203
+ strong, `aria-label` → medium, `title`/`placeholder` → weak). Deterministic and
204
+ intentionally tweakable in one place rather than per rule.
205
+
206
+ ### `hasAccessibleName(el)` → `boolean`
207
+ Back-compat convenience: `!!getAccessibleNameInfo(el).value`.
208
+
209
+ ---
210
+
211
+ ## 4) IDREF resolution
212
+
213
+ Backs `aria-labelledby`/`aria-describedby` and any other space-separated ID-reference
214
+ attribute.
215
+
216
+ ### `resolveIdRefs(idrefString, ctx, opts)` → `{ refs: Element[], missing: string[], flags[] }`
217
+ Splits and resolves a space-separated ID list to elements (deduped, cached per scope).
218
+ `opts.maxRefs` truncates deterministically (adds `'truncated'` to `flags`).
219
+
220
+ ### `getTextFromIdRefs(idrefString, ctx, opts)` → `{ text, refsCount, missing[], flags[] }`
221
+ Resolves refs, then computes **each target's own text alternative** recursively
222
+ (accname-aligned — a referenced element's name is recomputed, not read as raw
223
+ `textContent`), joins with spaces. This is what `getAriaLabelledByInfo`/
224
+ `getAccessibleDescriptionInfo` call internally.
225
+
226
+ ### `getTextFromIdRefsIdrefEligible(idrefString, ctx, opts)` → `{ text, refsCount, missing[], excluded[], flags[] }`
227
+ Same, but under IDREF eligibility rules specifically: hidden/`aria-hidden`/collapsed
228
+ targets are still included (IDREF targets aren't scoped by visibility the way rendered
229
+ content is — see the `root` note in the source), only `inert` targets are excluded.
230
+ `excluded` lists `{ id, reasons }` for anything dropped.
231
+
232
+ ---
233
+
234
+ ## 5) Role & focusability
235
+
236
+ ### `getRoleInfo(el, ctx, opts)` → `{ role, source, flags[] }`
237
+ Explicit `role` attribute if present (flags `'presentation'` for
238
+ `presentation`/`none`, `'multiple-roles'` if it contains whitespace), else a small,
239
+ deliberately minimal implicit-role mapping (`a[href]`→`link`, `button`→`button`,
240
+ `input[type=checkbox]`→`checkbox`, etc.) unless `opts.disallowImplicit`.
241
+
242
+ ### `getFocusableInfo(el, ctx, opts)` → `{ focusable, tabbable, mechanism, flags[] }`
243
+ Platform focusability: whether `el` can receive focus at all, and whether it's in the
244
+ default tab order.
245
+
246
+ ### `hasLandmarkScopingAncestor(el, ctx)` → `boolean`
247
+ Whether `el` sits inside a landmark-scoping ancestor — the role-aware
248
+ sectioning-content/`<main>` check backing `<header>`/`<footer>`/`<aside>`'s conditional
249
+ implicit roles (their implicit landmark role only applies when *not* nested inside
250
+ certain ancestors). Available both as `helpers.hasLandmarkScopingAncestor` and
251
+ `helpers.aria.hasLandmarkScopingAncestor` — same function, re-exported at the top level
252
+ so landmark-check files don't need to reach into `aria.*` for it.
253
+
254
+ ---
255
+
256
+ ## 6) Attributes, language, reporting, outcomes
257
+
258
+ ### `getAttributeInfo(el, attrName)` → `{ present, value, mechanism, flags[] }`
259
+ Generic trimmed-attribute presence/value check. Use for any plain attribute a rule
260
+ inspects directly (not one of the naming/ARIA attributes above, which have their own
261
+ dedicated helpers).
262
+
263
+ ### `isValidLanguageTag(value)` / `isRegisteredLanguageSubtag(subtag)` → `boolean`
264
+ BCP 47 well-formedness **plus** a real IANA subtag-registry check — shape alone accepts
265
+ `"eng"` or `"em-US"`, which look like language tags but use an unregistered primary
266
+ subtag (the registry only lists a three-letter subtag when no two-letter one exists,
267
+ so `"en"` is registered and `"eng"` is not). Use `isValidLanguageTag` for any
268
+ `lang`/`xml:lang`-checking rule instead of a regex-only check.
269
+
270
+ ### `reportOccurrence(node, partial)` → occurrence object
271
+ **Use this to build every occurrence.** Attaches the element so the engine fills in
272
+ `selector`, `html`, and `structuralPath` centrally — see `RULE_AUTHORING.md` §4.3/§9 for
273
+ the full contract and why hand-building these fields yourself is a real (measured:
274
+ 4 min → <1 s) performance regression on any rule reporting many occurrences.
275
+
276
+ ```js
277
+ occurrences.push(helpers.reportOccurrence(el, {
278
+ summary: '…',
279
+ hint: '…',
280
+ i18n: { summaryKey: '…', hintKey: '…', params: { element: 'img' } },
281
+ data: { visibilityFilter: eligInfo }
282
+ }));
283
+ ```
284
+
285
+ ### `resolveTieredOutcome(failOccurrences, cantTellOccurrences, severity)` → `{ outcome, severity, occurrences }`
286
+ For a rule that collects two confidence tiers in one run — some findings confident
287
+ enough for `fail`, others only `cantTell` ("needs human review"). Returns `fail` (with
288
+ **both** tiers' occurrences, tagged via `occurrenceOutcome`) whenever any fail-tier
289
+ finding exists, `cantTell` when only cantTell-tier findings exist, `pass` otherwise.
290
+ Reach for this instead of the naive `if (fails.length) return fail(fails); else if
291
+ (cantTells.length) return cantTell(cantTells);`, which silently drops every cantTell
292
+ finding whenever at least one fail finding also exists on the page.
293
+
294
+ ### `getPerfStats()` / `resetPerfStats()`
295
+ Only populated when the engine is run with `perfStats: true`. Not for rule logic —
296
+ useful when profiling a new rule's hot paths during development.
297
+
298
+ ---
299
+
300
+ ## 7) Namespaced helper groups
301
+
302
+ Two larger helper sets are exposed as namespaces rather than flattened, since their
303
+ member counts and internal cohesion (contrast math, ARIA validity data) don't fit the
304
+ flat `helpers.*` list above:
305
+
306
+ ### `helpers.contrast.*`
307
+ Color/contrast math and text-run analysis: `parseCssColorToRgba`, `compositeRgba`,
308
+ `relativeLuminance`, `contrastRatio`, `requiredRatio`, `isLargeText`,
309
+ `computeEffectiveForeground`/`computeEffectiveBackground`, `getComputabilityBlocker`,
310
+ `getTextScan`, `isInactiveUiComponent`, plus small numeric/formatting utilities
311
+ (`clamp01`, `round2`, `toHex2`, `pxToPt`, `fontWeightLabel`, …). Backs the
312
+ `contrast-*` rule family (`contrast-minimum`, `contrast-enhanced`,
313
+ `contrast-computable`) — see `src/core/contrast-helpers.js` if you're extending that
314
+ family specifically.
315
+
316
+ ### `helpers.aria.*`
317
+ ARIA validity/taxonomy data and checks: `isValidAriaAttrName`, `getAttrValueType`,
318
+ `validateAttrValue`, `getExplicitRole`, `getAllRoleTokens`, `isAbstractRole`,
319
+ `isDeprecatedRole`, `isAuthorDiscouragedRole`, `isAuthorProhibitedRole`,
320
+ `isDeprecatedAttr`, `getDeprecatedRoleGuidance`, `isKnownRole`, `isValidConcreteRole`,
321
+ `getRequiredAttrsForRole`, `getRequiredOwnedRoles`, `getRequiredContextRoles`,
322
+ `isRoleAllowedOnElement`, `getContainmentRole`, `getNativeRoleForElement`,
323
+ `hasLandmarkScopingAncestor` (also re-exported flat, see §5). Backs the whole
324
+ `aria-*` rule family — check here before hand-rolling role/attribute validity logic in
325
+ a new ARIA rule.
326
+
327
+ ---
328
+
329
+ ## 8) Not for rule use
330
+
331
+ `helpers.__setActiveRuleExcludeSelectors` exists on the object but is engine-internal —
332
+ `dom-runner.js` calls it before invoking each rule to scope that rule's
333
+ `engineOptions.rules[ruleId].excludeSelectors`. A rule itself never calls it.