@surea11y/core 1.3.0 → 1.4.1

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 (166) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/README.md +109 -35
  3. package/bin/surea11y-core.js +20 -0
  4. package/docs/API_STABILITY.md +26 -0
  5. package/docs/ARIA_DEPRECATION.md +95 -0
  6. package/docs/CI_INTEGRATIONS.md +7 -7
  7. package/docs/ENGINE_OPTIONS.md +1 -1
  8. package/docs/I18N.md +12 -9
  9. package/docs/INTEGRATION.md +1 -1
  10. package/docs/LIMITATIONS.md +1 -1
  11. package/docs/REPORT.md +1 -1
  12. package/docs/RULE_CATALOG.md +6 -6
  13. package/package.json +52 -16
  14. package/src/baseline.js +0 -0
  15. package/src/checks/automatic/area-alt-present.js +4 -6
  16. package/src/checks/automatic/aria-allowed-attr.js +663 -134
  17. package/src/checks/automatic/aria-allowed-role.js +2 -0
  18. package/src/checks/automatic/aria-braille-equivalent.js +2 -0
  19. package/src/checks/automatic/aria-conditional-attr.js +8 -7
  20. package/src/checks/automatic/aria-deprecated-role.js +107 -38
  21. package/src/checks/automatic/aria-hidden-body.js +6 -4
  22. package/src/checks/automatic/aria-hidden-focus.js +12 -13
  23. package/src/checks/automatic/aria-prohibited-attr.js +98 -105
  24. package/src/checks/automatic/aria-prohibited-children.js +56 -87
  25. package/src/checks/automatic/aria-required-attr.js +6 -7
  26. package/src/checks/automatic/aria-required-children.js +7 -10
  27. package/src/checks/automatic/aria-required-parent.js +20 -25
  28. package/src/checks/automatic/aria-role-name-present.js +2 -0
  29. package/src/checks/automatic/aria-roles-valid.js +33 -6
  30. package/src/checks/automatic/aria-valid-attr-value.js +18 -15
  31. package/src/checks/automatic/aria-valid-attr.js +2 -0
  32. package/src/checks/automatic/autocomplete-valid.js +39 -1
  33. package/src/checks/automatic/avoid-inline-spacing.js +181 -20
  34. package/src/checks/automatic/binary-control-name-present.js +13 -3
  35. package/src/checks/automatic/button-name-present.js +54 -21
  36. package/src/checks/automatic/canvas-text-alternative-present.js +17 -8
  37. package/src/checks/automatic/combobox-name-present.js +10 -1
  38. package/src/checks/automatic/contrast-computable.js +2 -0
  39. package/src/checks/automatic/contrast-enhanced.js +2 -0
  40. package/src/checks/automatic/contrast-minimum.js +2 -0
  41. package/src/checks/automatic/css-orientation-lock.js +21 -27
  42. package/src/checks/automatic/definition-list-children-valid.js +6 -6
  43. package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
  44. package/src/checks/automatic/dialog-name-present.js +18 -11
  45. package/src/checks/automatic/dlitem-parent-valid.js +2 -0
  46. package/src/checks/automatic/duplicate-id-aria.js +4 -3
  47. package/src/checks/automatic/embed-text-alternative-present.js +2 -0
  48. package/src/checks/automatic/form-control-programmatic-label-present.js +50 -4
  49. package/src/checks/automatic/form-control-single-label.js +110 -43
  50. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
  51. package/src/checks/automatic/iframe-focusable-content.js +246 -18
  52. package/src/checks/automatic/iframe-name-present.js +2 -0
  53. package/src/checks/automatic/iframe-title-unique.js +3 -1
  54. package/src/checks/automatic/img-alt-present.js +23 -18
  55. package/src/checks/automatic/input-image-alt-present.js +101 -52
  56. package/src/checks/automatic/label-in-name.js +97 -27
  57. package/src/checks/automatic/language-page-present.js +7 -1
  58. package/src/checks/automatic/link-in-text-block.js +2 -0
  59. package/src/checks/automatic/link-name-present.js +52 -17
  60. package/src/checks/automatic/list-children-valid.js +14 -24
  61. package/src/checks/automatic/listbox-name-present.js +10 -1
  62. package/src/checks/automatic/listitem-parent-valid.js +30 -7
  63. package/src/checks/automatic/menuitem-name-present.js +10 -1
  64. package/src/checks/automatic/meta-refresh-no-exceptions.js +33 -4
  65. package/src/checks/automatic/meta-refresh-timing-absent.js +32 -4
  66. package/src/checks/automatic/meta-viewport-zoom-enabled.js +39 -15
  67. package/src/checks/automatic/meter-name-present.js +12 -4
  68. package/src/checks/automatic/nested-interactive-controls-absent.js +177 -25
  69. package/src/checks/automatic/object-text-alternative-present.js +16 -7
  70. package/src/checks/automatic/option-name-present.js +10 -1
  71. package/src/checks/automatic/page-title-present.js +2 -0
  72. package/src/checks/automatic/progressbar-name-present.js +16 -11
  73. package/src/checks/automatic/role-img-alt-present.js +4 -4
  74. package/src/checks/automatic/searchbox-name-present.js +10 -1
  75. package/src/checks/automatic/server-side-image-map-absent.js +4 -3
  76. package/src/checks/automatic/slider-name-present.js +13 -2
  77. package/src/checks/automatic/spinbutton-name-present.js +10 -1
  78. package/src/checks/automatic/summary-name-present.js +10 -1
  79. package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
  80. package/src/checks/automatic/svg-text-alternative-present.js +17 -5
  81. package/src/checks/automatic/tab-name-present.js +10 -1
  82. package/src/checks/automatic/table-headers-attr-valid.js +3 -2
  83. package/src/checks/automatic/table-th-has-data-cells.js +69 -6
  84. package/src/checks/automatic/target-size-minimum.js +5 -0
  85. package/src/checks/automatic/td-has-header.js +24 -1
  86. package/src/checks/automatic/textbox-name-present.js +10 -1
  87. package/src/checks/automatic/tooltip-name-present.js +10 -1
  88. package/src/checks/automatic/treeitem-name-present.js +10 -1
  89. package/src/checks/automatic/valid-lang.js +18 -3
  90. package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
  91. package/src/checks/manual/accesskeys-manual.js +3 -1
  92. package/src/checks/manual/area-alt-decorative-manual.js +2 -0
  93. package/src/checks/manual/area-alt-quality-manual.js +2 -0
  94. package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
  95. package/src/checks/manual/aria-text-manual.js +6 -5
  96. package/src/checks/manual/bypass-blocks-present-manual.js +279 -0
  97. package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
  98. package/src/checks/manual/css-hidden-focus.js +184 -9
  99. package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
  100. package/src/checks/manual/empty-heading-manual.js +17 -17
  101. package/src/checks/manual/empty-table-header-manual.js +52 -25
  102. package/src/checks/manual/focus-order-semantics-manual.js +16 -4
  103. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
  104. package/src/checks/manual/heading-order-manual.js +28 -1
  105. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
  106. package/src/checks/manual/image-redundant-alt-manual.js +21 -1
  107. package/src/checks/manual/img-alt-decorative-manual.js +2 -0
  108. package/src/checks/manual/img-alt-quality-manual.js +2 -0
  109. package/src/checks/manual/input-image-alt-decorative-manual.js +26 -0
  110. package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
  111. package/src/checks/manual/label-title-only-manual.js +29 -22
  112. package/src/checks/manual/landmark-banner-is-top-level-manual.js +51 -55
  113. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +44 -31
  114. package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
  115. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
  116. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
  117. package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
  118. package/src/checks/manual/landmark-one-main-manual.js +12 -23
  119. package/src/checks/manual/landmark-unique-manual.js +37 -52
  120. package/src/checks/manual/link-name-quality-manual.js +2 -0
  121. package/src/checks/manual/media-transcript-present-manual.js +2 -0
  122. package/src/checks/manual/meta-viewport-large-manual.js +3 -1
  123. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
  124. package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
  125. package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
  126. package/src/checks/manual/p-as-heading-manual.js +2 -0
  127. package/src/checks/manual/page-has-heading-one-manual.js +12 -11
  128. package/src/checks/manual/page-title-patterns-manual.js +2 -0
  129. package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
  130. package/src/checks/manual/region-manual.js +27 -36
  131. package/src/checks/manual/scope-attr-valid-manual.js +3 -1
  132. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
  133. package/src/checks/manual/skip-link-manual.js +7 -6
  134. package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
  135. package/src/checks/manual/tabindex-manual.js +3 -1
  136. package/src/checks/manual/table-duplicate-name-manual.js +5 -4
  137. package/src/checks/manual/table-fake-caption-manual.js +24 -3
  138. package/src/checks/manual/video-caption-manual.js +2 -0
  139. package/src/checks/manual-review.js +2 -0
  140. package/src/core.js +11820 -3317
  141. package/src/index.js +2 -0
  142. package/src/report.js +51 -9
  143. package/src/sarif.js +20 -5
  144. package/surea11y.browser.js +4943 -1388
  145. package/bin/core.js +0 -473
  146. package/docs/CLI.md +0 -128
  147. package/src/catalogs/composites.wcag.js +0 -454
  148. package/src/checks/automatic/bypass-blocks-present.js +0 -215
  149. package/src/checks/rules-and-tags.full.csv +0 -19
  150. package/src/checks/rules-and-tags.full.json +0 -259
  151. package/src/core/aria-helpers.js +0 -1211
  152. package/src/core/contrast-helpers.js +0 -1302
  153. package/src/core/dom-helpers.js +0 -4493
  154. package/src/core/dom-runner.js +0 -787
  155. package/src/core/frame-messaging.js +0 -261
  156. package/src/core/frame-scan.js +0 -190
  157. package/src/core/rollup-composites.js +0 -127
  158. package/src/core/rule-meta.js +0 -176
  159. package/src/coverage/wcag-facets.js +0 -1079
  160. package/src/coverage/wcag-version-map.js +0 -84
  161. package/src/i18n/en.js +0 -1228
  162. package/src/i18n/fr.js +0 -1185
  163. package/src/policy/contracts.js +0 -18
  164. package/src/policy/resolvePolicy.js +0 -59
  165. package/src/policy/schemas/engine-options.schema.json +0 -103
  166. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,6 +1,6 @@
1
1
  # CI/CD pipeline integrations
2
2
 
3
- Ready-to-paste templates wrapping the [CLI](./CLI.md) (`npx @surea11y/core scan ...`) in GitHub Actions and Bitbucket Pipelines. If you're calling the library directly from your own Node script instead of the CLI, see [`INTEGRATION.md`](./INTEGRATION.md#ci-gating-a-build-on-the-result) instead — this page is specifically about the CLI as a pipeline step.
3
+ Ready-to-paste templates wrapping the [CLI](https://github.com/SureA11y/cli#readme) (`npx @surea11y/cli scan ...`) in GitHub Actions and Bitbucket Pipelines. If you're calling the library directly from your own Node script instead of the CLI, see [`INTEGRATION.md`](./INTEGRATION.md#ci-gating-a-build-on-the-result) instead — this page is specifically about the CLI as a pipeline step.
4
4
 
5
5
  All of these rely on the CLI's own exit codes (`0` clean, `1` at least one — or one *new*, with `--baseline` — `fail` outcome, `2` a usage/scan error) to gate the pipeline; no extra scripting is required for basic pass/fail gating.
6
6
 
@@ -21,7 +21,7 @@ jobs:
21
21
  with:
22
22
  node-version: 20
23
23
  - run: npm ci && npm run build # produce whatever static HTML you're scanning
24
- - run: npx @surea11y/core scan ./dist/index.html
24
+ - run: npx @surea11y/cli scan ./dist/index.html
25
25
  ```
26
26
 
27
27
  This fails the job the moment any `fail` outcome is found. For an existing site with pre-existing violations, see the baseline variant below instead of disabling the step.
@@ -30,13 +30,13 @@ This fails the job the moment any `fail` outcome is found. For an existing site
30
30
 
31
31
  ```sh
32
32
  # Once, locally: record every current fail occurrence, commit the file.
33
- npx @surea11y/core scan ./dist/index.html --write-baseline a11y-baseline.json
33
+ npx @surea11y/cli scan ./dist/index.html --write-baseline a11y-baseline.json
34
34
  git add a11y-baseline.json
35
35
  ```
36
36
 
37
37
  ```yaml
38
38
  - run: npm ci && npm run build
39
- - run: npx @surea11y/core scan ./dist/index.html --baseline a11y-baseline.json
39
+ - run: npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json
40
40
  ```
41
41
 
42
42
  See [`BASELINE.md`](./BASELINE.md) for what counts as "known" vs. "new", and how to regenerate the file as violations get fixed.
@@ -63,7 +63,7 @@ jobs:
63
63
  node-version: 20
64
64
  - run: npm ci && npm run build
65
65
  - name: Scan (report, don't fail the job here)
66
- run: npx @surea11y/core scan ./dist/index.html --baseline a11y-baseline.json --sarif results.sarif
66
+ run: npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json --sarif results.sarif
67
67
  continue-on-error: true
68
68
  id: scan
69
69
  - name: Upload SARIF to Code Scanning
@@ -91,7 +91,7 @@ pipelines:
91
91
  script:
92
92
  - npm ci
93
93
  - npm run build
94
- - npx @surea11y/core scan ./dist/index.html --baseline a11y-baseline.json --html a11y-report.html
94
+ - npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json --html a11y-report.html
95
95
  artifacts:
96
96
  - a11y-report.html
97
97
  ```
@@ -100,4 +100,4 @@ The step fails the pipeline on the CLI's exit code exactly like any other `scrip
100
100
 
101
101
  ## Free-tier/private-repo minute limits
102
102
 
103
- If your pipeline provider's free tier is minute-limited (Bitbucket Pipelines' free tier is 50 build-minutes/month on private workspaces, for example), a `jsdom`-based scan of static/server-rendered HTML (what the CLI does) is far cheaper than driving a real browser — see [`CLI.md`](./CLI.md#what-it-can-and-cant-scan) for what that trades away (no client-rendered content, no real CSS layout).
103
+ If your pipeline provider's free tier is minute-limited (Bitbucket Pipelines' free tier is 50 build-minutes/month on private workspaces, for example), a `jsdom`-based scan of static/server-rendered HTML (what the CLI does) is far cheaper than driving a real browser — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md#what-it-can-and-cant-scan) for what that trades away (no client-rendered content, no real CSS layout).
@@ -224,7 +224,7 @@ See the option-by-option table above for anything not shown here, and the `custo
224
224
 
225
225
  Every shipped rule is baked into `src/core.js` at build time. `engineOptions.customRules` is the runtime escape hatch: an array of rule descriptors registered for that one call only — nothing is added to the static catalog (`getRulesCatalog()`/`getChecksCatalog()`), and nothing persists between calls. This is deliberate, not a limitation to work around: surea11y already takes fresh `engineOptions` per call with no mutable global config (unlike some other engines, which need a `configure()`/`reset()` step against a shared runtime), and custom rules follow that same per-call model.
226
226
 
227
- Calling the library directly is one way in; the CLI also exposes this via `--custom-rules <path>` (a local file, loaded once per scan) — see [`CLI.md`](./CLI.md#custom-rules).
227
+ Calling the library directly is one way in; the CLI also exposes this via `--custom-rules <path>` (a local file, loaded once per scan) — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md#custom-rules).
228
228
 
229
229
  A descriptor has the *same shape as an internal rule module's own export* — if you already know how to write a rule file for this engine, you already know this API:
230
230
 
package/docs/I18N.md CHANGED
@@ -6,10 +6,12 @@ Every rule's title/description, and every occurrence's summary/hint, is localize
6
6
 
7
7
  | Locale | File | Keys | Coverage vs. English |
8
8
  |---|---|---|---|
9
- | `en` (English) | `src/i18n/en.js` | 600 | 100% (the canonical/fallback set) |
10
- | `fr` (French) | `src/i18n/fr.js` | 600 | 100% |
9
+ | `en` (English) | `src/i18n/en.js` | 614 | 100% (the canonical/fallback set) |
10
+ | `fr` (French) | `src/i18n/fr.js` | 614 | 100% |
11
+ | `de` (German) | `src/i18n/de.js` | 614 | 100% |
12
+ | `es` (Spanish) | `src/i18n/es.js` | 614 | 100% |
11
13
 
12
- Both locales are fully translated as of this writing. That won't stay automatically true — every time a new rule (or a new i18n key) is added to `en.js`, `fr.js` needs the same key added, or it silently falls back to English for that string (see the fallback behavior below). There's no automated check for this yet; diff `Object.keys(require('./src/i18n/en.js'))` against `fr.js` after adding a rule to catch drift before it ships.
14
+ All four locales are fully translated as of this writing. That won't stay automatically true — every time a new rule (or a new i18n key) is added to `en.js`, every other locale needs the same key added, or it silently falls back to English for that string (see the fallback behavior below). `tests/i18n/i18n-locale-completeness.test.js` catches this: it fails the build if a locale listed in `FULLY_TRANSLATED_LOCALES` (currently `fr`, `de`, `es`) is missing any key present in `en.js`, and fails for *any* locale that has an orphaned key not in `en.js` (a sign of a typo or a stale key left behind after a rule was removed). Run `npm run i18n:report` any time to see per-locale coverage.
13
15
 
14
16
  ## Selecting a locale
15
17
 
@@ -17,7 +19,7 @@ Both locales are fully translated as of this writing. That won't stay automatica
17
19
  runDomRulesInPage(url, null, { locale: 'fr' }, null);
18
20
  ```
19
21
 
20
- Default is `'en'` if omitted. Any string is accepted — an unrecognized locale (e.g. `'de'`, not yet built) behaves exactly like a locale that's 0% translated: every string falls through to English (see below), not an error.
22
+ Default is `'en'` if omitted. Any string is accepted — an unrecognized locale (e.g. `'ja'`, not yet built) behaves exactly like a locale that's 0% translated: every string falls through to English (see below), not an error.
21
23
 
22
24
  ## Fallback behavior (per-string, not per-locale)
23
25
 
@@ -40,8 +42,9 @@ Both are included in the result alongside the already-resolved text, so you can
40
42
 
41
43
  ## Contributing a translation
42
44
 
43
- 1. Open `src/i18n/en.js` — it's the canonical key list (590 entries, one `module.exports` object of `key: string`).
44
- 2. Add matching keys to `src/i18n/<locale>.js` (create the file if the locale doesn't exist yet — follow `fr.js`'s structure exactly: `'use strict'; module.exports = { ...keys };`).
45
- 3. You don't need every key on day one — the fallback behavior above means a partial translation degrades gracefully to English per-string, exactly like `fr` does today. Ship what you have.
46
- 4. Keep `{{placeholder}}` tokens in translated strings exactly as they appear in the English source — they're substituted verbatim regardless of locale (e.g. `{{element}}`, `{{role}}`).
47
- 5. Run `npm run build && npm test` — there's no locale-completeness test today (a partial locale is valid, not a failure), but this confirms nothing else broke.
45
+ 1. Scaffold the file: `npm run i18n:new <locale>` (e.g. `npm run i18n:new de`) creates `src/i18n/<locale>.js` with all of `en.js`'s keys already present, each seeded with the English text as a placeholder. It refuses to overwrite an existing locale file unless you pass `--force`.
46
+ 2. Replace the placeholder values with real translations, key by key. Leave any you're unsure about as-is for now — a value identical to English is treated as untranslated, not broken (see the fallback behavior above).
47
+ 3. Check progress any time with `npm run i18n:report` — it prints, per locale, how many keys have been translated vs. still match the English placeholder, plus any missing or orphaned keys.
48
+ 4. You don't need every key on day one — the fallback behavior above means a partial translation degrades gracefully to English per-string, exactly like `fr`/`de`/`es` did during their own early stages. Ship what you have. A partial locale is valid and won't fail `i18n-locale-completeness.test.js` unless you also add it to `FULLY_TRANSLATED_LOCALES` in that file — only do that once `npm run i18n:report` shows 100% coverage.
49
+ 5. Keep `{{placeholder}}` tokens (and `{{#foo}}...{{/foo}}` conditional blocks) in translated strings exactly as they appear in the English source — they're substituted/evaluated verbatim regardless of locale (e.g. `{{element}}`, `{{role}}`). Never translate HTML tag/attribute names (`<img>`, `aria-label`, `role="dialog"`, etc.) — they're code identifiers, not prose.
50
+ 6. Run `npm run build && npm test` to confirm nothing broke, including locale-completeness.
@@ -130,7 +130,7 @@ if (failures.length > 0) {
130
130
 
131
131
  Notes for CI specifically:
132
132
  - `cantTell` outcomes are advisory by design (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md#outcome-values)) — most teams log them without failing the build, since they require human judgment the CI run can't make.
133
- - For "only fail on *new* violations," the CLI has a built-in baseline/allowlist mechanism (`--write-baseline`/`--baseline`, see [`BASELINE.md`](./BASELINE.md)). Calling the library directly, the same matching logic is available as `buildBaselineEntries(result)`/`matchBaseline(result, baselineEntries)` from `require('@surea11y/core/src/baseline')` — or diff `checksResults` against a saved prior run yourself if your pages don't fit that model (see `BASELINE.md`'s "known limitation").
133
+ - For "only fail on *new* violations," the CLI has a built-in baseline/allowlist mechanism (`--write-baseline`/`--baseline`, see [`BASELINE.md`](./BASELINE.md)). Calling the library directly, the same matching logic is available as `buildBaselineEntries(result)`/`matchBaseline(result, baselineEntries)` from `require('@surea11y/core/baseline')` — or diff `checksResults` against a saved prior run yourself if your pages don't fit that model (see `BASELINE.md`'s "known limitation").
134
134
  - Prefer Pattern 1 (jsdom) in CI unless you specifically need real-browser layout — it avoids the extra weight of a Puppeteer/Playwright + browser-binary install in your pipeline.
135
135
 
136
136
  ## Browser extension context
@@ -14,7 +14,7 @@ surea11y is a **static DOM scan**: it reads the DOM tree and computed styles at
14
14
 
15
15
  - **jsdom (Node, no real browser) has no CSS layout engine.** Rules needing real geometry — most notably `target-size-minimum` (WCAG 2.5.8, needs real `getBoundingClientRect()`) — report `notApplicable` under plain jsdom rather than guess. Run under a real browser (Puppeteer/Playwright — see [`INTEGRATION.md`](./INTEGRATION.md) Pattern 2) to get real findings from these rules.
16
16
  - **`<dialog>` and other elements hidden by the default UA stylesheet** (no `open` attribute, `display: none` by spec), along with any other subtree hidden via `display:none`, `visibility:hidden`, `[hidden]`, or closed `<details>`, are **excluded from rule evaluation by default** — matching the visibility-aware behavior of other established engines. This is a deliberate default (`engineOptions.includeHiddenElements: false`), not an oversight: hidden content isn't reachable by assistive technology or keyboard until it's shown, so flagging a markup defect inside it by default would often be noise. Set `engineOptions.includeHiddenElements: true` to evaluate hidden/collapsed subtrees anyway — e.g. to catch a markup defect (like a broken ARIA ID reference) before a dialog ever opens. See [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#engineoptions--the-rest) for the option and exactly which hiding mechanisms it covers.
17
- - **Static markup vs. live/post-hydration DOM state.** The rule logic itself is DOM-source-agnostic — it evaluates whatever DOM it's handed, whether that's jsdom-parsed static HTML (Pattern 1) or an already-loaded, already-hydrated real browser tab (Pattern 2, see [`INTEGRATION.md`](./INTEGRATION.md)). But the CLI (`npx @surea11y/core scan <url>`) specifically fetches static HTML only, with no JS execution — see [`CLI.md`](./CLI.md). For a JS-framework-hydrated widget whose server-rendered markup intentionally ships one state before client JS syncs it (e.g. `<input type="checkbox" aria-checked="true">` shipped before client JS sets the native `checked` property to match on hydration — an extremely common, entirely legitimate pattern), a CLI scan only sees the pre-hydration markup. Other engines running inside an actual loaded browser tab see the post-hydration state instead, so the two can disagree on exactly this class of element for reasons that have nothing to do with either engine's rule correctness. `aria-checked-state-mismatch` is deliberately `manual`/`cantTell`-capped for this exact reason rather than a hard `fail`. If you need live-DOM accuracy for hydration-sensitive checks, run the library directly against an already-loaded page via Pattern 2, not the static-fetch CLI.
17
+ - **Static markup vs. live/post-hydration DOM state.** The rule logic itself is DOM-source-agnostic — it evaluates whatever DOM it's handed, whether that's jsdom-parsed static HTML (Pattern 1) or an already-loaded, already-hydrated real browser tab (Pattern 2, see [`INTEGRATION.md`](./INTEGRATION.md)). But the CLI (`npx @surea11y/cli scan <url>`) specifically fetches static HTML only, with no JS execution — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md). For a JS-framework-hydrated widget whose server-rendered markup intentionally ships one state before client JS syncs it (e.g. `<input type="checkbox" aria-checked="true">` shipped before client JS sets the native `checked` property to match on hydration — an extremely common, entirely legitimate pattern), a CLI scan only sees the pre-hydration markup. Other engines running inside an actual loaded browser tab see the post-hydration state instead, so the two can disagree on exactly this class of element for reasons that have nothing to do with either engine's rule correctness. `aria-checked-state-mismatch` is deliberately `manual`/`cantTell`-capped for this exact reason rather than a hard `fail`. If you need live-DOM accuracy for hydration-sensitive checks, run the library directly against an already-loaded page via Pattern 2, not the static-fetch CLI.
18
18
 
19
19
  ## Deliberately not attempted — judgment calls, not automatable safely
20
20
 
package/docs/REPORT.md CHANGED
@@ -18,7 +18,7 @@ Open `report.html` directly from disk. Works alongside any other output mode —
18
18
  ## Library usage
19
19
 
20
20
  ```js
21
- const { renderHtmlReport } = require('@surea11y/core/src/report');
21
+ const { renderHtmlReport } = require('@surea11y/core/report');
22
22
  const { runDomRulesInPage } = require('@surea11y/core');
23
23
 
24
24
  const result = runDomRulesInPage(url, null, {}, null);
@@ -2,11 +2,11 @@
2
2
 
3
3
  Generated from the compiled engine's own catalog (`getChecksCatalog()`/`getRulesCatalog()`) — 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
- **125 rules total: 77 automatic (WCAG-normative, can return `fail`), 48 manual (advisory/judgment-required, capped at `cantTell`). 101 carry at least one formal WCAG Success Criterion mapping.**
5
+ **125 rules total: 76 automatic (WCAG-normative, can return `fail`), 49 manual (advisory/judgment-required, capped at `cantTell`). 101 carry at least one formal WCAG Success Criterion mapping.**
6
6
 
7
7
  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.
8
8
 
9
- ## Automatic rules (77) — can return `fail`
9
+ ## Automatic rules (76) — can return `fail`
10
10
 
11
11
  | Rule ID | Title | WCAG SC | Level | Confidence | Default severity |
12
12
  |---|---|---|---|---|---|
@@ -15,7 +15,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
15
15
  | `aria-allowed-role` | Explicit role must be permitted for its host element | 4.1.2 | A | high | moderate |
16
16
  | `aria-braille-equivalent` | aria-braillelabel/aria-brailleroledescription must have a non-braille equivalent | 4.1.2 | A | high | serious |
17
17
  | `aria-conditional-attr` | aria-errormessage requires aria-invalid to be set to a non-false value | 4.1.2 | A | high | serious |
18
- | `aria-deprecated-role` | role attribute must not use a deprecated or author-prohibited ARIA role | 4.1.2 | A | high | moderate |
18
+ | `aria-deprecated-role` | role attribute should not use a deprecated or author-discouraged ARIA role | 4.1.2 | A | high | moderate |
19
19
  | `aria-hidden-body` | The document &lt;body&gt; must not be aria-hidden | 1.3.1, 4.1.2 | A | high | critical |
20
20
  | `aria-hidden-focus` | ARIA hidden elements must not be focusable | 2.4.7, 4.1.2 | AA | high | serious |
21
21
  | `aria-prohibited-attr` | ARIA naming attributes must not be used on roles that prohibit them | 4.1.2 | A | high | moderate |
@@ -28,10 +28,9 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
28
28
  | `aria-valid-attr` | aria-* attributes must be real, defined ARIA attributes | 4.1.2 | A | high | serious |
29
29
  | `aria-valid-attr-value` | aria-* attribute values must match their declared type | 4.1.2 | A | high | serious |
30
30
  | `autocomplete-valid` | autocomplete attribute must be a valid autofill value | 1.3.5 | AA | high | moderate |
31
- | `avoid-inline-spacing` | Inline style must not force text spacing with !important | 1.4.12 | AA | high | moderate |
31
+ | `avoid-inline-spacing` | Inline style must not force text spacing below the WCAG metric | 1.4.12 | AA | high | moderate |
32
32
  | `binary-control-name-present` | Binary controls have an accessible name | 4.1.2 | A | high | serious |
33
33
  | `button-name-present` | Buttons have an accessible name | 4.1.2 | A | high | serious |
34
- | `bypass-blocks-present` | Page must provide a way to bypass repeated blocks | 2.4.1 | A | medium | serious |
35
34
  | `canvas-text-alternative-present` | &lt;canvas&gt; must provide a text alternative | 1.1.1 | A | high | serious |
36
35
  | `combobox-name-present` | Comboboxes have an accessible name | 4.1.2 | A | high | serious |
37
36
  | `contrast-computable` | Color contrast is computable for rendered text | 1.4.3, 1.4.6 | AAA | high | serious |
@@ -88,7 +87,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
88
87
  | `valid-lang` | Element lang attribute must be syntactically valid | 3.1.2 | AA | high | moderate |
89
88
  | `video-poster-text-alternative-present` | &lt;video&gt; poster must have a text alternative | 1.1.1 | A | medium | serious |
90
89
 
91
- ## Manual rules (48) — advisory, capped at `cantTell`
90
+ ## Manual rules (49) — advisory, capped at `cantTell`
92
91
 
93
92
  | Rule ID | Title | WCAG SC | Level | Confidence | Default severity |
94
93
  |---|---|---|---|---|---|
@@ -97,6 +96,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
97
96
  | `area-alt-quality` | &lt;area&gt; alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
98
97
  | `aria-checked-state-mismatch` | Native checkbox/radio aria-checked should match its actual state | 4.1.2 | A | medium | moderate |
99
98
  | `aria-text` | role="text" elements should have no focusable descendants | — | — | medium | minor |
99
+ | `bypass-blocks-present` | Page must provide a way to bypass repeated blocks | 2.4.1 | A | medium | moderate |
100
100
  | `canvas-text-alternative-quality` | &lt;canvas&gt; text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
101
101
  | `css-hidden-focus` | Focusable elements must not be visually hidden | 2.4.7 | AA | low | serious |
102
102
  | `embed-text-alternative-quality` | &lt;embed&gt; text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
package/package.json CHANGED
@@ -1,20 +1,50 @@
1
1
  {
2
2
  "name": "@surea11y/core",
3
- "version": "1.3.0",
4
- "description": "Lightweight DOM rules accessibility core with modular rules.",
3
+ "version": "1.4.1",
4
+ "description": "Deterministic WCAG 2.2 accessibility engine that tells you what it can't tell you. Zero dependencies.",
5
+ "keywords": [
6
+ "accessibility",
7
+ "a11y",
8
+ "wcag",
9
+ "wcag21",
10
+ "wcag22",
11
+ "accessibility-testing",
12
+ "a11y-testing",
13
+ "audit",
14
+ "aria",
15
+ "contrast",
16
+ "sarif",
17
+ "zero-dependencies",
18
+ "jsdom",
19
+ "playwright",
20
+ "puppeteer",
21
+ "selenium",
22
+ "cypress",
23
+ "webdriverio",
24
+ "jest",
25
+ "vitest"
26
+ ],
5
27
  "main": "src/index.js",
6
28
  "bin": {
7
- "surea11y": "bin/core.js"
29
+ "surea11y-core": "bin/surea11y-core.js"
30
+ },
31
+ "exports": {
32
+ ".": "./src/index.js",
33
+ "./baseline": "./src/baseline.js",
34
+ "./report": "./src/report.js",
35
+ "./sarif": "./src/sarif.js",
36
+ "./browser": "./surea11y.browser.js",
37
+ "./package.json": "./package.json"
8
38
  },
9
39
  "author": "Jorge Rumoroso",
10
40
  "license": "MPL-2.0",
11
41
  "repository": {
12
42
  "type": "git",
13
- "url": "git+https://github.com/rumoroso/surea11y-core.git"
43
+ "url": "git+https://github.com/SureA11y/core.git"
14
44
  },
15
- "homepage": "https://github.com/rumoroso/surea11y-core#readme",
45
+ "homepage": "https://github.com/SureA11y/core#readme",
16
46
  "bugs": {
17
- "url": "https://github.com/rumoroso/surea11y-core/issues"
47
+ "url": "https://github.com/SureA11y/core/issues"
18
48
  },
19
49
  "engines": {
20
50
  "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
@@ -23,8 +53,13 @@
23
53
  "access": "public"
24
54
  },
25
55
  "files": [
26
- "src",
27
- "bin",
56
+ "src/index.js",
57
+ "src/core.js",
58
+ "src/baseline.js",
59
+ "src/report.js",
60
+ "src/sarif.js",
61
+ "src/checks/**/*.js",
62
+ "bin/surea11y-core.js",
28
63
  "surea11y.browser.js",
29
64
  "docs/**/*.md",
30
65
  "README.md",
@@ -33,8 +68,7 @@
33
68
  "!docs/RULE_TEMPLATE.md",
34
69
  "!docs/RULE_TEST_TEMPLATE.md",
35
70
  "!docs/RULE_TEST_AUTHORING.md",
36
- "!docs/TEST_OUTCOME_STABILITY.md",
37
- "!src/explain"
71
+ "!docs/TEST_OUTCOME_STABILITY.md"
38
72
  ],
39
73
  "scripts": {
40
74
  "lint": "eslint .",
@@ -43,8 +77,8 @@
43
77
  "format:check": "prettier --check \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
44
78
  "build": "node scripts/build-core.js && node scripts/build-browser.js",
45
79
  "pretest": "playwright install chromium",
46
- "test": "npm run build && node scripts/run-tests.js",
47
- "test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js --test-coverage-include=bin/**/*.js",
80
+ "test": "npm run format:check && npm run build && node scripts/run-tests.js",
81
+ "test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js",
48
82
  "test:contrast-helpers": "node tests/contrast-helpers.test.js",
49
83
  "helpers-perf-bench": "node --expose-gc scripts/dom-helpers-perf-bench.js",
50
84
  "engine-perf-bench": "node --expose-gc scripts/engine-perf-bench.js --profileRules=true --iters=25 --top=15",
@@ -56,16 +90,18 @@
56
90
  "fixtures:index": "node scripts/generate-fixture-index.js",
57
91
  "docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
58
92
  "validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
59
- "validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual"
60
- },
61
- "dependencies": {
62
- "jsdom": "^29.1.1"
93
+ "validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
94
+ "i18n:new": "node scripts/i18n-scaffold.js",
95
+ "i18n:report": "node scripts/i18n-report.js"
63
96
  },
64
97
  "devDependencies": {
65
98
  "@eslint/js": "^10.0.1",
99
+ "aria-query": "^5.3.2",
66
100
  "eslint": "^10.8.0",
67
101
  "eslint-config-prettier": "^10.1.8",
68
102
  "globals": "^17.8.0",
103
+ "jsdom": "^29.1.1",
104
+ "language-subtag-registry": "^0.4.2",
69
105
  "playwright": "^1.62.1",
70
106
  "prettier": "^3.9.6"
71
107
  }
package/src/baseline.js CHANGED
Binary file
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -199,12 +201,8 @@ function runInPage(ctx) {
199
201
  }
200
202
 
201
203
  // A non-empty title attribute is HTML-AAM's own next fallback naming
202
- // source once alt is entirely absent -- also accepted by a widely-used
203
- // reference engine's equivalent area-alt rule (non-empty-title, same "any" list as
204
- // non-empty-alt/aria-label/aria-labelledby). See img-alt-present's
205
- // sibling fix (2026-07-23, AliExpress's title-only logo <img>) for
206
- // the real page this was found via -- same gap, same fix, different
207
- // element.
204
+ // source once alt is entirely absent. Same gap img-alt-present handles
205
+ // for <img title="..."> with no alt.
208
206
  const titleRaw = (() => {
209
207
  try {
210
208
  return el.getAttribute('title');