@surea11y/core 1.5.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 (157) hide show
  1. package/CHANGELOG.md +240 -149
  2. package/README.md +51 -44
  3. package/docs/ACT_RULE_MAPPING.md +245 -0
  4. package/docs/API_STABILITY.md +53 -5
  5. package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
  6. package/docs/DESIGN_CHALLENGES.md +367 -0
  7. package/docs/EARL.md +100 -0
  8. package/docs/ENGINE_OPTIONS.md +42 -4
  9. package/docs/I18N.md +4 -4
  10. package/docs/INTEGRATION.md +4 -2
  11. package/docs/LIMITATIONS.md +9 -5
  12. package/docs/OUTPUT_SCHEMA.md +44 -6
  13. package/docs/POLICY.md +1 -1
  14. package/docs/REPORT.md +1 -1
  15. package/docs/RULE_AUTHORING.md +63 -36
  16. package/docs/RULE_CATALOG.md +1928 -169
  17. package/docs/RULE_HELPERS.md +333 -0
  18. package/docs/RULE_TAXONOMY.md +27 -6
  19. package/docs/SARIF.md +21 -2
  20. package/docs/TROUBLESHOOTING.md +2 -2
  21. package/docs/WCAG_CONFORMANCE.md +34 -10
  22. package/package.json +11 -9
  23. package/src/baseline.js +3 -3
  24. package/src/checks/automatic/area-alt-present.js +2 -2
  25. package/src/checks/automatic/aria-allowed-attr.js +74 -10
  26. package/src/checks/automatic/aria-allowed-role.js +34 -25
  27. package/src/checks/automatic/aria-braille-equivalent.js +21 -13
  28. package/src/checks/automatic/aria-conditional-attr.js +22 -15
  29. package/src/checks/automatic/aria-deprecated-role.js +13 -1
  30. package/src/checks/automatic/aria-hidden-body.js +3 -3
  31. package/src/checks/automatic/aria-hidden-focus.js +5 -5
  32. package/src/checks/automatic/aria-prohibited-attr.js +23 -18
  33. package/src/checks/automatic/aria-prohibited-children.js +136 -43
  34. package/src/checks/automatic/aria-required-attr.js +119 -24
  35. package/src/checks/automatic/aria-required-children.js +54 -30
  36. package/src/checks/automatic/aria-required-parent.js +93 -15
  37. package/src/checks/automatic/aria-role-name-present.js +37 -23
  38. package/src/checks/automatic/aria-roles-valid.js +52 -21
  39. package/src/checks/automatic/aria-valid-attr-value.js +89 -33
  40. package/src/checks/automatic/aria-valid-attr.js +15 -10
  41. package/src/checks/automatic/autocomplete-valid.js +2 -2
  42. package/src/checks/automatic/avoid-inline-spacing.js +133 -6
  43. package/src/checks/automatic/binary-control-name-present.js +27 -5
  44. package/src/checks/automatic/button-name-present.js +92 -6
  45. package/src/checks/automatic/combobox-name-present.js +26 -6
  46. package/src/checks/automatic/contrast-computable.js +42 -0
  47. package/src/checks/automatic/contrast-enhanced.js +33 -1
  48. package/src/checks/automatic/contrast-minimum.js +33 -1
  49. package/src/checks/automatic/css-orientation-lock.js +138 -24
  50. package/src/checks/automatic/definition-list-children-valid.js +7 -8
  51. package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
  52. package/src/checks/automatic/dialog-name-present.js +20 -2
  53. package/src/checks/automatic/duplicate-id-aria.js +10 -3
  54. package/src/checks/automatic/duplicate-id.js +203 -0
  55. package/src/checks/automatic/embed-text-alternative-present.js +2 -2
  56. package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
  57. package/src/checks/automatic/form-control-single-label.js +10 -1
  58. package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
  59. package/src/checks/automatic/iframe-focusable-content.js +68 -7
  60. package/src/checks/automatic/iframe-name-present.js +37 -3
  61. package/src/checks/automatic/iframe-title-unique.js +1 -1
  62. package/src/checks/automatic/img-alt-present.js +12 -4
  63. package/src/checks/automatic/label-in-name.js +204 -68
  64. package/src/checks/automatic/link-in-text-block.js +285 -29
  65. package/src/checks/automatic/link-name-present.js +22 -1
  66. package/src/checks/automatic/list-children-valid.js +6 -6
  67. package/src/checks/automatic/listbox-name-present.js +28 -8
  68. package/src/checks/automatic/listitem-parent-valid.js +4 -4
  69. package/src/checks/automatic/menuitem-name-present.js +20 -2
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
  72. package/src/checks/automatic/meter-name-present.js +23 -4
  73. package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
  74. package/src/checks/automatic/option-name-present.js +23 -4
  75. package/src/checks/automatic/page-title-present.js +21 -3
  76. package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
  77. package/src/checks/automatic/progressbar-name-present.js +23 -4
  78. package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
  79. package/src/checks/automatic/searchbox-name-present.js +28 -8
  80. package/src/checks/automatic/server-side-image-map-absent.js +1 -1
  81. package/src/checks/automatic/slider-name-present.js +27 -6
  82. package/src/checks/automatic/spinbutton-name-present.js +28 -8
  83. package/src/checks/automatic/summary-name-present.js +18 -2
  84. package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
  85. package/src/checks/automatic/svg-text-alternative-present.js +13 -10
  86. package/src/checks/automatic/tab-name-present.js +21 -2
  87. package/src/checks/automatic/table-headers-attr-valid.js +43 -8
  88. package/src/checks/automatic/table-th-has-data-cells.js +61 -5
  89. package/src/checks/automatic/target-size-minimum.js +155 -58
  90. package/src/checks/automatic/td-has-header.js +24 -23
  91. package/src/checks/automatic/textbox-name-present.js +28 -8
  92. package/src/checks/automatic/tooltip-name-present.js +21 -2
  93. package/src/checks/automatic/treeitem-name-present.js +23 -4
  94. package/src/checks/automatic/valid-lang.js +92 -7
  95. package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
  96. package/src/checks/manual/accesskeys-manual.js +3 -3
  97. package/src/checks/manual/area-alt-decorative-manual.js +7 -0
  98. package/src/checks/manual/area-alt-quality-manual.js +6 -0
  99. package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
  100. package/src/checks/manual/aria-text-manual.js +4 -4
  101. package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
  103. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
  105. package/src/checks/manual/empty-heading-manual.js +58 -11
  106. package/src/checks/manual/empty-table-header-manual.js +8 -8
  107. package/src/checks/manual/focus-order-semantics-manual.js +15 -15
  108. package/src/checks/manual/form-control-label-quality-manual.js +563 -0
  109. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
  110. package/src/checks/manual/heading-order-manual.js +3 -3
  111. package/src/checks/manual/heading-quality-manual.js +338 -0
  112. package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
  113. package/src/checks/manual/image-redundant-alt-manual.js +4 -4
  114. package/src/checks/manual/img-alt-decorative-manual.js +211 -52
  115. package/src/checks/manual/img-alt-quality-manual.js +7 -0
  116. package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
  117. package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
  118. package/src/checks/manual/label-title-only-manual.js +4 -4
  119. package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
  120. package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
  121. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
  122. package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
  123. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
  124. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
  125. package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
  126. package/src/checks/manual/landmark-one-main-manual.js +6 -6
  127. package/src/checks/manual/landmark-unique-manual.js +9 -9
  128. package/src/checks/manual/link-name-quality-manual.js +161 -32
  129. package/src/checks/manual/media-transcript-present-manual.js +2 -3
  130. package/src/checks/manual/meta-viewport-large-manual.js +2 -2
  131. package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
  132. package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
  133. package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
  134. package/src/checks/manual/p-as-heading-manual.js +4 -4
  135. package/src/checks/manual/page-has-heading-one-manual.js +6 -6
  136. package/src/checks/manual/page-title-patterns-manual.js +26 -3
  137. package/src/checks/manual/password-paste-enabled-manual.js +255 -0
  138. package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
  139. package/src/checks/manual/region-manual.js +19 -19
  140. package/src/checks/manual/scope-attr-valid-manual.js +2 -2
  141. package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
  142. package/src/checks/manual/skip-link-manual.js +5 -5
  143. package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
  144. package/src/checks/manual/tabindex-manual.js +2 -2
  145. package/src/checks/manual/table-duplicate-name-manual.js +2 -2
  146. package/src/checks/manual/table-fake-caption-manual.js +2 -2
  147. package/src/checks/manual/video-caption-manual.js +3 -3
  148. package/src/checks/manual-review.js +17 -1
  149. package/src/core.js +8880 -41883
  150. package/src/earl.js +144 -0
  151. package/src/report.js +2 -2
  152. package/src/sarif.js +22 -2
  153. package/surea11y.browser.js +10 -37882
  154. package/surea11y.i18n.de.js +2 -21
  155. package/surea11y.i18n.es.js +2 -21
  156. package/surea11y.i18n.fr.js +2 -21
  157. package/bin/surea11y-core.js +0 -20
@@ -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.
@@ -11,20 +11,41 @@ It reflects **what already exists in the rules and tests**, not theory.
11
11
  Encoded by: `meta.type`
12
12
 
13
13
  - `automatic`
14
- - Rule makes a **normative decision**
15
- - Allowed outcomes: `pass`, `fail`, `notApplicable`, and `cantTell` as a defensive fallback only (e.g. an internal-failure safety net, or a computability gate a rule can't resolve — see `contrast-minimum.js`/`contrast-enhanced.js`/`contrast-computable.js`/`target-size-minimum.js`), never as its primary intended path
14
+ - Rule **decides deterministically**, with no heuristics and no guessing
15
+ - Allowed outcomes: `pass`, `fail`, `notApplicable`, `cantTell`
16
+ - `cantTell` is the primary path in two distinct cases, and a defensive
17
+ fallback in a third:
18
+ - the rule decides that a real violation exists, but the violation does
19
+ not on its own establish that the mapped criterion fails — an ARIA
20
+ author requirement the exposed name, role and value survive
21
+ (`aria-valid-attr`, `aria-braille-equivalent`,
22
+ `aria-conditional-attr`), or the graded tier of a rule that fails
23
+ elsewhere (`aria-required-attr`, `aria-roles-valid`)
24
+ - the rule decides deterministically but claims no Success Criterion at
25
+ all (`aria-allowed-role`, tagged `best-practice`)
26
+ - the rule asks whether something is PRESENT, and its absence conveys
27
+ nothing false, while the question of whether what IS present is valid
28
+ belongs to a sibling rule that still fails
29
+ (`aria-required-children`, paired with `aria-prohibited-children`)
30
+ - a computability gate the rule cannot resolve, or an internal-failure
31
+ safety net (`contrast-minimum.js`/`contrast-enhanced.js`/
32
+ `contrast-computable.js`/`target-size-minimum.js`)
16
33
  - `manual`
17
- - Rule signals **human review required**
34
+ - Rule signals **human review required**: it cannot decide at all
18
35
  - Allowed outcomes: `cantTell`, `notApplicable`
19
36
 
20
- Manual rules MUST NOT make normative failure decisions.
37
+ Manual rules MUST NOT make normative failure decisions. The dividing line
38
+ between the two types is whether the rule can decide, not which outcome it
39
+ reports: an automatic rule that reports `cantTell` has decided, and is saying
40
+ what it found; a manual rule reports `cantTell` because the question is not
41
+ decidable from markup.
21
42
 
22
43
  ---
23
44
 
24
45
  ### 1.2 Intent
25
46
  Encoded by: **rule id suffix**
26
47
 
27
- Illustrative intents (from the image-alternatives family used as the running example in §2, not an exhaustive list — the ruleset's 125 rules use dozens of distinct suffixes; `docs/RULE_CATALOG.md` is the generated, always-current list):
48
+ Illustrative intents (from the image-alternatives family used as the running example in §2, not an exhaustive list — the ruleset's 130 rules use dozens of distinct suffixes; `docs/RULE_CATALOG.md` is the generated, always-current list):
28
49
 
29
50
  - `present`
30
51
  - Verifies that a required **mechanism exists**
@@ -43,7 +64,7 @@ Intent determines whether a rule can be automatic.
43
64
  ### 1.3 Target Family
44
65
  Encoded by: **rule id prefix**
45
66
 
46
- Illustrative families, from the image-alternatives cluster (WCAG 1.1.1) used as the running example in §2 — not an exhaustive list. The ruleset's 125 rules span dozens of families (`aria-*`, `contrast-*`, `dialog-*`, `iframe-*`, `label-*`, `link-*`, `list-*`, `landmark-*`, and more); see `docs/RULE_CATALOG.md` for the generated, always-current list:
67
+ Illustrative families, from the image-alternatives cluster (WCAG 1.1.1) used as the running example in §2 — not an exhaustive list. The ruleset's 130 rules span dozens of families (`aria-*`, `contrast-*`, `dialog-*`, `iframe-*`, `label-*`, `link-*`, `list-*`, `landmark-*`, and more); see `docs/RULE_CATALOG.md` for the generated, always-current list:
47
68
 
48
69
  - `img`
49
70
  - `area`
package/docs/SARIF.md CHANGED
@@ -14,11 +14,30 @@ See [`CI_INTEGRATIONS.md`](./CI_INTEGRATIONS.md) for a ready-to-paste GitHub Act
14
14
 
15
15
  ## What becomes a SARIF result
16
16
 
17
- Only `fail`/`cantTell` occurrences produce SARIF results — a `pass`/`notApplicable` check has no occurrences to report at all (same "violations only" framing as [`REPORT.md`](./REPORT.md)'s HTML report).
17
+ Only `fail`/`cantTell` occurrences produce SARIF results (same "violations only" framing as [`REPORT.md`](./REPORT.md)'s HTML report).
18
+
19
+ A `notApplicable` check is not always empty: a rule may attach one occurrence explaining why it had nothing to judge, which the contrast rules do when no text had a computable background. Those never become results — a consumer treats every result as an alert, and "this was not evaluated" is not one — but they are not dropped either. They are carried as `note`-level entries in `runs[0].invocations[0].toolExecutionNotices`, each naming the rule it came from via `associatedRule.id`:
20
+
21
+ ```json
22
+ "invocations": [
23
+ {
24
+ "executionSuccessful": true,
25
+ "toolExecutionNotices": [
26
+ {
27
+ "level": "note",
28
+ "message": { "text": "No eligible text had computable contrast (eligible text nodes: 13). See the contrast computability rule for details." },
29
+ "associatedRule": { "id": "contrast-minimum" }
30
+ }
31
+ ]
32
+ }
33
+ ]
34
+ ```
35
+
36
+ That keeps a SARIF-only pipeline from reading silence as a clean bill of health: no contrast alerts can mean the page is fine, or that contrast was never computable, and only the notice separates the two. The block is emitted only when there is something to say, so a run with nothing to report has no `invocations` key at all.
18
37
 
19
38
  | Engine outcome | SARIF `level` | Meaning |
20
39
  |---|---|---|
21
- | `fail` | `error` | Deterministic, high-confidence violation — the CI-gating case. |
40
+ | `fail` | `error` | Deterministic violation — the CI-gating case. |
22
41
  | `cantTell` | `warning` | Needs human review — surfaced, but shouldn't block a build on its own. |
23
42
 
24
43
  Every rule that ran (regardless of whether it produced a result) is listed once in `runs[0].tool.driver.rules`, with `defaultConfiguration.level` set from the rule's `type`: `automatic` (fail-capable) → `error`, `manual` (capped at `cantTell`) → `warning`.
@@ -18,7 +18,7 @@ Check the result's `error` field first — if it says `"<something> is not defin
18
18
 
19
19
  ## "A geometry-dependent rule (e.g. `target-size-minimum`) always says `notApplicable`"
20
20
 
21
- Plain jsdom (no real browser) doesn't implement CSS layout — `getBoundingClientRect()` always returns zero geometry. Rules that need real layout deliberately report `notApplicable` under jsdom rather than guess. Run through a real browser instead (Puppeteer/Playwright — see [`INTEGRATION.md`](./INTEGRATION.md) Pattern 2) to get real findings from these rules. See [`LIMITATIONS.md`](./LIMITATIONS.md).
21
+ Plain jsdom (no real browser) doesn't implement CSS layout — `getBoundingClientRect()` always returns zero geometry. Rules that need real layout report `notApplicable` under jsdom rather than guess. Run through a real browser instead (Puppeteer/Playwright — see [`INTEGRATION.md`](./INTEGRATION.md) Pattern 2) to get real findings from these rules. See [`LIMITATIONS.md`](./LIMITATIONS.md).
22
22
 
23
23
  ## "I only see `fail`/`cantTell` occurrences — where's the list of elements that passed?"
24
24
 
@@ -28,7 +28,7 @@ By design, this engine never enumerates the elements a rule *passed* — only th
28
28
 
29
29
  Two common causes, in order of likelihood:
30
30
 
31
- 1. **The element is excluded from the accessibility tree** — `aria-hidden="true"`, `display: none`, `visibility: hidden`, `hidden`, or an `inert` ancestor. Most rules deliberately skip content that's already invisible to assistive technology (checking a hidden element would be meaningless, and could produce a misleading `fail` on content no user encounters). Some rules explicitly opt out of this gating when it wouldn't make sense to (e.g. `no-autoplay-audio` — hidden audio still plays sound) — check the specific rule's file header comment (`@applicability`) in `src/checks/`.
31
+ 1. **The element is excluded from the accessibility tree** — `aria-hidden="true"`, `display: none`, `visibility: hidden`, `hidden`, or an `inert` ancestor. Most rules skip content that's already invisible to assistive technology, since checking a hidden element would be meaningless and could produce a misleading `fail` on content no user encounters. Some rules explicitly opt out of this gating when it wouldn't make sense to (e.g. `no-autoplay-audio` — hidden audio still plays sound) — check the specific rule's file header comment (`@applicability`) in `src/checks/`.
32
32
  2. **`excludeSelectors`** — if you've configured this (directly or inherited from a shared config), confirm the element in question isn't matched by it. Remember this can also be scoped to a single rule via `engineOptions.rules[ruleId].excludeSelectors` (see [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#rule-scoped-excludeselectors)) — if a rule you expect to fire keeps coming back `notApplicable`/`pass` for one element only, check whether that rule specifically has its own exclude list configured, not just the global one.
33
33
 
34
34
  ## "Does a clean scan (`pass` everywhere) mean the page is WCAG conformant?"
@@ -4,9 +4,9 @@ How individual rule results relate to a WCAG Success Criterion (SC), and what su
4
4
 
5
5
  ## The three layers
6
6
 
7
- 1. **Atomic rules** (`checksResults[]`) — one normative decision each, e.g. "does this `<img>` have an `alt` attribute." See [`RULE_CATALOG.md`](./RULE_CATALOG.md) for all 125.
7
+ 1. **Atomic rules** (`checksResults[]`) — one normative decision each, e.g. "does this `<img>` have an `alt` attribute." See [`RULE_CATALOG.md`](./RULE_CATALOG.md) for all 130.
8
8
  2. **Facets** — a WCAG SC is usually bigger than any one rule can decide deterministically. Internally, each SC is broken into named "facets" (e.g. 1.1.1 Non-text Content has facets like `img-alt-attr-present`, `text-alternative-quality`, `decorative-null`) tracked in `src/coverage/wcag-facets.js`, each marked `full` (a rule decides it with high confidence), `partial` (a rule decides *part* of it — see each rule's own scope notes), or `manual` (no safe automated heuristic exists at all). Run `npm run coverage` to regenerate `coverage/coverage-report.md`, the per-SC facet breakdown.
9
- 3. **Composite (WCAG-SC rollup) rules** (`rulesResults[]`) — a generated aggregate of every atomic rule mapped to one SC, giving you one pass/fail/cantTell/notApplicable verdict per SC instead of having to roll up dozens of atomic results yourself. See [`RULE_CATALOG.md`](./RULE_CATALOG.md#composite-wcag-sc-rollup-rules-31) for the full list (e.g. `wcag-1.1.1-non-text-content` rolls up 22 atomic rules).
9
+ 3. **Composite (WCAG-SC rollup) rules** (`rulesResults[]`) — a generated aggregate of every atomic rule mapped to one SC, giving you one pass/fail/cantTell/notApplicable verdict per SC instead of having to roll up dozens of atomic results yourself. See [`RULE_CATALOG.md`](./RULE_CATALOG.md#composite-wcag-sc-rollup-rules-33) for the full list (e.g. `wcag-1.1.1-non-text-content` rolls up 22 atomic rules).
10
10
 
11
11
  ## How a composite's outcome is computed
12
12
 
@@ -25,24 +25,48 @@ A composite's `data.details.contributors` array (see [`OUTPUT_SCHEMA.md`](./OUTP
25
25
 
26
26
  ## Targeting a conformance level (A / AA / AAA)
27
27
 
28
- Pass `runOnly.tags` (or `engineOptions.tags.include`) with one of `wcag2a`, `wcag2aa`, `wcag2aaa`:
28
+ Pass `runOnly.tags` (or `engineOptions.tags.include`) with the level tags you want:
29
29
 
30
30
  ```js
31
- runDomRulesInPage(url, null, {}, { tags: ['wcag2aa'] });
31
+ // WCAG 2.0 A and AA. Both tags are required: they are not cumulative.
32
+ runDomRulesInPage(url, null, {}, { tags: ['wcag2a', 'wcag2aa'] });
32
33
  ```
33
34
 
34
- This does two things at once:
35
- - **Filters atomic rules** to ones tagged at or under that level (a `wcag2aa`-tagged rule also carries `wcag2a`, since AA is cumulative on top of A — WCAG conformance is always defined this way).
36
- - **Suppresses composites above the target level** — e.g. requesting `wcag2aa` will not return a `rulesResults` entry for an AAA-only SC's composite, even if some AAA-level atomic rules happen to be tagged loosely. This is `inferTargetLevelFromRunOnly`/`isAllowedByTargetLevel` in the runner — see `src/core/dom-runner.js` if you need the exact precedence logic.
37
-
38
- Omit `tags` entirely (the default) and every rule at every level runs, with no composite suppression.
35
+ **Level tags do not nest, and this is the easiest thing to get wrong here.** A rule
36
+ carries one level tag per Success Criterion it maps to, and nothing more: a rule
37
+ mapped only to an AA criterion is tagged `wcag2aa` and *not* `wcag2a`. Asking for
38
+ `{ tags: ['wcag2aa'] }` on its own therefore runs the 10 rules mapped to a 2.0 AA
39
+ criterion, not the ~100 that make up an A + AA target. List every level you mean.
40
+
41
+ The same applies across WCAG versions — a criterion introduced in 2.1 or 2.2 carries
42
+ only its own origin tag — so a full conformance target is a union of tag sets. See
43
+ [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#filtering-by-wcag-version-21-vs-22) for the
44
+ ready-made sets per version, including the one criterion WCAG 2.2 removed rather than
45
+ added.
46
+
47
+ **The removed criterion is handled for you.** Every run resolves a target WCAG version
48
+ (`engineOptions.wcagVersion`, else whatever your version tags imply, else `2.2`) and
49
+ reports it back as `engine.wcagVersion`. Under a 2.2 target, a rule mapped only to SC
50
+ 4.1.1 Parsing cannot report `fail` — it runs, reports its occurrences, and comes back
51
+ `cantTell` with a `wcagVersionScope` field explaining the coercion. So a default scan
52
+ never gates on a criterion WCAG 2.2 does not contain, and a 2.0/2.1 scan still gets a
53
+ real 4.1.1 verdict.
54
+
55
+ Composites, unlike atomic rules, *are* filtered cumulatively. The runner reads the
56
+ highest level named in `tags` and drops every composite above it, so requesting
57
+ `['wcag2a', 'wcag2aa']` returns no `rulesResults` entry for an AAA-only SC. That is
58
+ `inferTargetLevelFromRunOnly`/`isAllowedByTargetLevel` in `src/core/dom-runner.js` if
59
+ you need the exact precedence.
60
+
61
+ Omit `tags` entirely (the default) and every rule at every level runs, with no composite
62
+ suppression.
39
63
 
40
64
  ## What this engine cannot tell you
41
65
 
42
66
  No automated tool — this one included — can certify full WCAG conformance. That's not a limitation specific to surea11y; it's inherent to WCAG itself; a meaningful fraction of Success Criteria require human judgment (is this alt text *accurate*, not just *present*; is this error message *understandable*) or dynamic testing this engine's static-DOM-scan architecture cannot do at all (keyboard-trap detection, real layout/reflow at zoom). See [`LIMITATIONS.md`](./LIMITATIONS.md) for the full, explicit list of what's out of scope and why.
43
67
 
44
68
  What surea11y *can* give you, honestly:
45
- - Every `fail` is a real, deterministic, normative violation — never a guess.
69
+ - Every `fail` is a real, deterministic, normative violation under the version you targeted — never a guess.
46
70
  - Every `cantTell` is an explicit flag for human review, not a swallowed uncertainty.
47
71
  - The facet coverage table tells you exactly which parts of which SCs have zero automated coverage, so you know where a `pass` is silent rather than exhaustive.
48
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surea11y/core",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Deterministic WCAG 2.2 accessibility engine that tells you what it can't tell you. Zero dependencies.",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -25,15 +25,14 @@
25
25
  "vitest"
26
26
  ],
27
27
  "main": "src/index.js",
28
- "bin": {
29
- "surea11y-core": "bin/surea11y-core.js"
30
- },
31
28
  "exports": {
32
29
  ".": "./src/index.js",
33
30
  "./baseline": "./src/baseline.js",
34
31
  "./report": "./src/report.js",
35
32
  "./sarif": "./src/sarif.js",
33
+ "./earl": "./src/earl.js",
36
34
  "./browser": "./surea11y.browser.js",
35
+ "./i18n/*": "./surea11y.i18n.*.js",
37
36
  "./package.json": "./package.json"
38
37
  },
39
38
  "author": "Jorge Rumoroso",
@@ -58,8 +57,8 @@
58
57
  "src/baseline.js",
59
58
  "src/report.js",
60
59
  "src/sarif.js",
60
+ "src/earl.js",
61
61
  "src/checks/**/*.js",
62
- "bin/surea11y-core.js",
63
62
  "surea11y.browser.js",
64
63
  "surea11y.i18n.*.js",
65
64
  "docs/**/*.md",
@@ -74,11 +73,11 @@
74
73
  "scripts": {
75
74
  "lint": "eslint .",
76
75
  "lint:fix": "eslint . --fix",
77
- "format": "prettier --write \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
78
- "format:check": "prettier --check \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
76
+ "format": "prettier --write \"src/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
77
+ "format:check": "prettier --check \"src/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
79
78
  "build": "node scripts/build-core.js && node scripts/build-browser.js",
80
79
  "pretest": "playwright install chromium",
81
- "test": "npm run format:check && npm run build && node scripts/run-tests.js",
80
+ "test": "npm run lint && npm run format:check && npm run build && npm run validate:rules && node scripts/run-tests.js",
82
81
  "test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js",
83
82
  "test:contrast-helpers": "node tests/contrast-helpers.test.js",
84
83
  "helpers-perf-bench": "node --expose-gc scripts/dom-helpers-perf-bench.js",
@@ -89,11 +88,13 @@
89
88
  "coverage": "node scripts/generate-wcag-coverage.js --rulesDir src/checks",
90
89
  "coverage:strict": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --strictFacets",
91
90
  "coverage:check": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --check",
91
+ "finding-ids": "npm run build && node scripts/generate-finding-ids.js",
92
92
  "fixtures:index": "node scripts/generate-fixture-index.js",
93
+ "fixtures:check": "node scripts/generate-fixture-index.js --check",
93
94
  "docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
94
95
  "validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
95
96
  "validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
96
- "validate:rules": "npm run validate:automatic-rules && npm run validate:manual-rules",
97
+ "validate:rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic src/checks/manual",
97
98
  "i18n:new": "node scripts/i18n-scaffold.js",
98
99
  "i18n:sync": "node scripts/i18n-sync.js",
99
100
  "i18n:check": "node scripts/i18n-sync.js --check",
@@ -102,6 +103,7 @@
102
103
  "devDependencies": {
103
104
  "@eslint/js": "^10.0.1",
104
105
  "aria-query": "^5.3.2",
106
+ "esbuild": "^0.28.2",
105
107
  "eslint": "^10.8.0",
106
108
  "eslint-config-prettier": "^10.1.8",
107
109
  "globals": "^17.8.0",
package/src/baseline.js CHANGED
@@ -3,13 +3,13 @@
3
3
  'use strict';
4
4
 
5
5
  // docs/BASELINE.md: identity for one violation occurrence is
6
- // `ruleId + reasonCode + html` -- deliberately NOT `selector`/`structuralPath`
6
+ // `ruleId + reasonCode + html`, on purpose NOT `selector`/`structuralPath`
7
7
  // (both position-derived, so they shift when unrelated markup changes
8
8
  // elsewhere on the page; see buildSelector/buildStructuralPath in
9
9
  // src/core/dom-helpers.js). Unlike src/explain/group.js's computeGroupKey,
10
- // this does not use a coarse structural signature: that's deliberately lossy
10
+ // this does not use a coarse structural signature: that's lossy on purpose
11
11
  // for AI-explanation dedup (one prompt per shape), which would risk a CI gate
12
- // silently treating a genuinely new violation as "known" just because it
12
+ // silently treating an actually new violation as "known" just because it
13
13
  // shares tag/class shape with an old baselined one -- the wrong failure mode
14
14
  // here. Content-based matching survives incidental DOM changes elsewhere on
15
15
  // the page; its known limitation is a flagged element with dynamic content
@@ -21,9 +21,9 @@
21
21
  const id = 'area-alt-present';
22
22
 
23
23
  const meta = {
24
- title: '&lt;area&gt; must have an alt attribute',
24
+ title: '<area> must have an alt attribute',
25
25
  description:
26
- 'Checks that &lt;area&gt; elements provide an alt attribute to support a text alternative mechanism.',
26
+ 'Checks that <area> elements provide an alt attribute to support a text alternative mechanism.',
27
27
  i18n: {
28
28
  titleKey: 'area_altPresent_title',
29
29
  descriptionKey: 'area_altPresent_description'