@surea11y/core 1.0.0 → 1.0.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.
package/docs/I18N.md CHANGED
@@ -6,10 +6,10 @@ 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` | 590 | 100% (the canonical/fallback set) |
10
- | `fr` (French) | `src/i18n/fr.js` | 287 | ~49% — **partial**, not every string is translated yet |
9
+ | `en` (English) | `src/i18n/en.js` | 600 | 100% (the canonical/fallback set) |
10
+ | `fr` (French) | `src/i18n/fr.js` | 600 | 100% |
11
11
 
12
- That `fr` number is real and worth being honest about: about half of the engine's translatable strings don't have a French entry yet. That's not a bug — see the fallback behavior below, which means a missing `fr` key never produces broken or missing text, just an English string in the middle of otherwise-French output.
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.
13
13
 
14
14
  ## Selecting a locale
15
15
 
@@ -103,7 +103,7 @@ Notes:
103
103
 
104
104
  ## An occurrence (`occurrences[i]`)
105
105
 
106
- Only present when `outcome` is `fail` or `cantTell` (a `pass`/`notApplicable` result has `occurrences: []` — this engine does not enumerate the elements it passed, only the ones it flagged; see the note in `docs/RULE_AUTHORING.md` on why "silence" from a `pass` rule is not the same as an enumerated list of passing elements).
106
+ Only present when `outcome` is `fail` or `cantTell` (a `pass`/`notApplicable` result has `occurrences: []` — this engine does not enumerate the elements it passed, only the ones it flagged).
107
107
 
108
108
  ```ts
109
109
  {
@@ -114,7 +114,7 @@ Only present when `outcome` is `fail` or `cantTell` (a `pass`/`notApplicable` re
114
114
  hint: string,
115
115
  i18n: { summaryKey: string, hintKey: string, params: object } | null,
116
116
  data: {
117
- visibilityFilter?: { targetSet: string, accEligible: boolean | null, reasons: string[] },
117
+ visibilityFilter?: { eligible: boolean, targetSet: string, accEligible: boolean | null, reasons: string[] },
118
118
  details?: object // rule-specific, non-normative — see below
119
119
  }
120
120
  }
@@ -128,7 +128,7 @@ Only present when `outcome` is `fail` or `cantTell` (a `pass`/`notApplicable` re
128
128
  | `summary` | Human-readable, already localized ("This button has no accessible name."). |
129
129
  | `hint` | Human-readable remediation guidance, already localized. |
130
130
  | `i18n` | The raw translation keys behind `summary`/`hint`, if you want to re-render them in a different locale yourself without re-running the scan. `null` if the occurrence didn't use key-based i18n. |
131
- | `data.visibilityFilter` | Present on most occurrences: why the engine considered this element eligible for accessibility-tree evaluation (or not). `reasons` is a list of machine-readable exclusion codes when `accEligible: false`. |
131
+ | `data.visibilityFilter` | Present on most occurrences: why the engine considered this element eligible (or not) under whichever eligibility model the rule used. `eligible` is that result; `targetSet` says which model produced it (`'dom'`: raw DOM/CSS visibility — most rules; `'acc'`: accessibility-tree eligibility). `accEligible` mirrors `eligible` only when `targetSet` is `'acc'`, otherwise `null` — don't read it as a second, independent signal. `reasons` is a list of machine-readable exclusion codes when `eligible: false`. |
132
132
  | `data.details` | Rule-specific structured data (e.g. `reasonCode`, computed metrics, resolved references) — **non-normative**: useful for building richer UI or debugging, but never changes what `outcome`/`severity` mean. Shape varies per rule; treat as best-effort extra context, not a stable contract. |
133
133
 
134
134
  ## A composite result (`rulesResults[i]`)
@@ -202,7 +202,7 @@ const result = runDomRulesInPage(
202
202
  {
203
203
  "selector": "html > body > button",
204
204
  "html": "<button></button>",
205
- "structuralPath": [1, 0],
205
+ "structuralPath": [1, 1],
206
206
  "summary": "This button has no accessible name.",
207
207
  "hint": "Provide visible button text or a programmatic accessible-name mechanism (for example aria-label) so assistive technologies can identify the button.",
208
208
  "data": {
@@ -222,7 +222,7 @@ const result = runDomRulesInPage(
222
222
  {
223
223
  "selector": "html > body > img",
224
224
  "html": "<img src=\"logo.png\">",
225
- "structuralPath": [1, 1],
225
+ "structuralPath": [1, 0],
226
226
  "summary": "Missing alt attribute on <img>.",
227
227
  "hint": "Add an alt attribute (use alt=\"\" only for decorative images)."
228
228
  }
@@ -12,7 +12,7 @@ Encoded by: `meta.type`
12
12
 
13
13
  - `automatic`
14
14
  - Rule makes a **normative decision**
15
- - Allowed outcomes: `pass`, `fail`, `notApplicable`
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
16
16
  - `manual`
17
17
  - Rule signals **human review required**
18
18
  - Allowed outcomes: `cantTell`, `notApplicable`
@@ -24,7 +24,7 @@ Manual rules MUST NOT make normative failure decisions.
24
24
  ### 1.2 Intent
25
25
  Encoded by: **rule id suffix**
26
26
 
27
- Current intents:
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):
28
28
 
29
29
  - `present`
30
30
  - Verifies that a required **mechanism exists**
@@ -43,7 +43,7 @@ Intent determines whether a rule can be automatic.
43
43
  ### 1.3 Target Family
44
44
  Encoded by: **rule id prefix**
45
45
 
46
- Current families include:
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:
47
47
 
48
48
  - `img`
49
49
  - `area`
@@ -67,12 +67,13 @@ Rules MUST NOT mix families.
67
67
  ### 1.4 Target Set (Tree Scope)
68
68
  Encoded by: `data.visibilityFilter.targetSet`
69
69
 
70
- Current value used by this ruleset:
71
- - `acc` (accessibility tree)
70
+ Values used by this ruleset:
71
+ - `acc` (accessibility tree) — most rules
72
+ - `dom` (raw DOM/CSS visibility, no accessibility-tree computation) — e.g. `label-in-name.js`
72
73
 
73
- Rules explicitly log eligibility against the accessibility tree using:
74
- - `helpers.isAccTreeEligible`
75
- - `helpers.getEligibilityInfo(..., { targetSet: "acc" })`
74
+ Rules log eligibility against whichever tree scope they target using:
75
+ - `helpers.isAccTreeEligible` / `helpers.isDomVisibleEligible`
76
+ - `helpers.getEligibilityInfo(..., { targetSet: "acc" | "dom" })`
76
77
 
77
78
  This is a semantic constraint, not logging noise.
78
79
 
@@ -39,9 +39,9 @@ No — see [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md). A `pass` means every
39
39
 
40
40
  Treat it as "needs a human to look" — it's neither pass nor fail by design. Most teams log `cantTell` findings without failing CI on them, since failing a build on something the engine explicitly couldn't determine tends to train people to ignore the gate. See [`POLICY.md`](./POLICY.md) if you want to reshape this behavior (e.g. via a custom policy contract), and [`INTEGRATION.md`](./INTEGRATION.md#ci-gating-a-build-on-the-result) for a concrete CI-gating example.
41
41
 
42
- ## "Why is French only partially translated?"
42
+ ## "What happens if a locale is only partially translated?"
43
43
 
44
- It genuinely is — see [`I18N.md`](./I18N.md) for the exact current coverage. Missing keys fall back to English per-string (never a blank or broken result), so a partial locale degrades gracefully rather than failing outright.
44
+ Missing keys fall back to English per-string (never a blank or broken result), so a partial locale degrades gracefully rather than failing outright — see [`I18N.md`](./I18N.md) for the mechanism and current coverage. Both shipped locales (`en`, `fr`) are at full parity as of this writing, but that's not guaranteed to stay true automatically: adding a new rule adds a new key to `en.js`, and unless the same key is added to `fr.js` (or any other locale file you maintain), that string falls back to English until it is.
45
45
 
46
46
  ## "`runDomRulesInPage` vs `runa11yCoreInPage` — which one do I want?"
47
47
 
@@ -4,7 +4,7 @@ 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 123.
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.
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
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).
10
10
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@surea11y/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Lightweight DOM rules accessibility core with modular rules.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
- "surea11y": "./bin/core.js"
7
+ "surea11y": "bin/core.js"
8
8
  },
9
9
  "author": "Jorge Rumoroso",
10
10
  "license": "MIT",
@@ -22,12 +22,7 @@
22
22
  * - SUPPORTED_ATTRS_BY_ROLE widened 2026-07-21 (7 new roles), cross-checked
23
23
  * against a widely-used reference engine's own per-role `allowedAttrs`
24
24
  * table AND verified each addition is an unambiguous, well-established ARIA
25
- * fact rather than a blind import — that engine's own table has ~68 roles,
26
- * far more than were reconciled here; this pass deliberately took only the
27
- * additions with clear, specific supported attributes (not just the
28
- * near-universal, thin `aria-expanded` that engine allows on most roles), and
29
- * left the rest for a dedicated future full-reconciliation pass rather
30
- * than rushing all 68 through at lower confidence:
25
+ * fact rather than a blind import:
31
26
  * - `searchbox`: identical to the already-covered `textbox` (ARIA
32
27
  * explicitly defines searchbox as textbox's subclass, same supported
33
28
  * set).
@@ -47,6 +42,34 @@
47
42
  * - `menu`/`menubar`/`toolbar`: activedescendant/orientation — standard
48
43
  * composite-widget properties, same family as the already-covered
49
44
  * `tablist`.
45
+ * - SUPPORTED_ATTRS_BY_ROLE full-reconciliation pass 2026-07-28: the
46
+ * 2026-07-21 pass deliberately deferred ~61 roles where the reference
47
+ * engine (axe-core) allows `aria-expanded`, treating it as too thin/
48
+ * near-universal to import blindly. Checked against `aria-query`
49
+ * (tracks the published WAI-ARIA 1.2 Recommendation, 6 June 2023 — the
50
+ * latest actually-published version, as opposed to the in-progress 1.3
51
+ * Editor's Draft) instead of axe-core directly, because axe-core's own
52
+ * source comments (`// Spec difference: Aria-expanded removed in 1.2`)
53
+ * show that most of its `aria-expanded` allowances are deliberate
54
+ * ARIA-1.1 legacy/AT-compat carryovers axe-core keeps on purpose, not
55
+ * current-spec facts — importing them wholesale would have re-added
56
+ * exactly the kind of unverified allowance the 07-21 pass was avoiding.
57
+ * `aria-query`'s per-role tables (with superclass inheritance resolved,
58
+ * e.g. `aria-activedescendant` via the abstract `composite` role) gave a
59
+ * spec-grounded diff instead. Net changes from that diff:
60
+ * - `aria-expanded` added to: checkbox, columnheader, gridcell, listbox,
61
+ * menuitemcheckbox, menuitemradio, row, rowheader, switch, tab —
62
+ * confirmed present in current spec for these roles specifically
63
+ * (unlike e.g. `listitem`, `dialog`, `alertdialog`, `heading`, which
64
+ * the spec does NOT list it for — those stay excluded).
65
+ * - `aria-activedescendant` added to: combobox, grid, listbox,
66
+ * radiogroup, row, spinbutton, tablist, treegrid (inherited from the
67
+ * abstract `composite` role for any composite/managed-focus widget).
68
+ * - `aria-readonly`/`aria-required` added to: switch, menuitemcheckbox,
69
+ * menuitemradio. `aria-posinset`/`aria-setsize` added to: tab, radio,
70
+ * menuitemcheckbox, menuitemradio. `aria-level` added to: tablist.
71
+ * - `tree`'s `aria-readonly` removed: not in aria-query's resolved
72
+ * props for `tree` (was an unverified carryover, not spec-backed).
50
73
  * - Not gated on isAccTreeEligible: this is a static markup property.
51
74
  */
52
75
 
@@ -101,39 +124,39 @@ function runInPage(ctx) {
101
124
  // role/attribute pairings from the WAI-ARIA role definitions are listed.
102
125
  const SUPPORTED_ATTRS_BY_ROLE = {
103
126
  alertdialog: ['aria-modal'],
104
- checkbox: ['aria-checked', 'aria-readonly', 'aria-required'],
105
- columnheader: ['aria-sort', 'aria-colindex', 'aria-colspan', 'aria-readonly', 'aria-required', 'aria-rowindex', 'aria-rowspan', 'aria-selected'],
106
- combobox: ['aria-expanded', 'aria-autocomplete', 'aria-readonly', 'aria-required'],
127
+ checkbox: ['aria-checked', 'aria-readonly', 'aria-required', 'aria-expanded'],
128
+ columnheader: ['aria-sort', 'aria-colindex', 'aria-colspan', 'aria-readonly', 'aria-required', 'aria-rowindex', 'aria-rowspan', 'aria-selected', 'aria-expanded'],
129
+ combobox: ['aria-expanded', 'aria-autocomplete', 'aria-readonly', 'aria-required', 'aria-activedescendant'],
107
130
  dialog: ['aria-modal'],
108
- grid: ['aria-multiselectable', 'aria-readonly', 'aria-colcount', 'aria-rowcount'],
109
- gridcell: ['aria-selected', 'aria-readonly', 'aria-required', 'aria-colindex', 'aria-colspan', 'aria-rowindex', 'aria-rowspan'],
131
+ grid: ['aria-multiselectable', 'aria-readonly', 'aria-colcount', 'aria-rowcount', 'aria-activedescendant'],
132
+ gridcell: ['aria-selected', 'aria-readonly', 'aria-required', 'aria-colindex', 'aria-colspan', 'aria-rowindex', 'aria-rowspan', 'aria-expanded'],
110
133
  heading: ['aria-level'],
111
- listbox: ['aria-multiselectable', 'aria-readonly', 'aria-required', 'aria-orientation'],
134
+ listbox: ['aria-multiselectable', 'aria-readonly', 'aria-required', 'aria-orientation', 'aria-expanded', 'aria-activedescendant'],
112
135
  listitem: ['aria-level', 'aria-posinset', 'aria-setsize'],
113
136
  menu: ['aria-activedescendant', 'aria-orientation'],
114
137
  menubar: ['aria-activedescendant', 'aria-orientation'],
115
- menuitemcheckbox: ['aria-checked'],
116
- menuitemradio: ['aria-checked'],
138
+ menuitemcheckbox: ['aria-checked', 'aria-expanded', 'aria-readonly', 'aria-required', 'aria-posinset', 'aria-setsize'],
139
+ menuitemradio: ['aria-checked', 'aria-expanded', 'aria-readonly', 'aria-required', 'aria-posinset', 'aria-setsize'],
117
140
  meter: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext'],
118
141
  option: ['aria-selected', 'aria-checked', 'aria-posinset', 'aria-setsize'],
119
142
  progressbar: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext'],
120
- radio: ['aria-checked'],
121
- radiogroup: ['aria-readonly', 'aria-required', 'aria-orientation'],
122
- row: ['aria-selected', 'aria-level', 'aria-posinset', 'aria-setsize', 'aria-colindex', 'aria-rowindex'],
123
- rowheader: ['aria-sort', 'aria-colindex', 'aria-colspan', 'aria-readonly', 'aria-required', 'aria-rowindex', 'aria-rowspan', 'aria-selected'],
143
+ radio: ['aria-checked', 'aria-posinset', 'aria-setsize'],
144
+ radiogroup: ['aria-readonly', 'aria-required', 'aria-orientation', 'aria-activedescendant'],
145
+ row: ['aria-selected', 'aria-level', 'aria-posinset', 'aria-setsize', 'aria-colindex', 'aria-rowindex', 'aria-expanded', 'aria-activedescendant'],
146
+ rowheader: ['aria-sort', 'aria-colindex', 'aria-colspan', 'aria-readonly', 'aria-required', 'aria-rowindex', 'aria-rowspan', 'aria-selected', 'aria-expanded'],
124
147
  scrollbar: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-orientation', 'aria-controls'],
125
148
  searchbox: ['aria-activedescendant', 'aria-autocomplete', 'aria-multiline', 'aria-placeholder', 'aria-readonly', 'aria-required'],
126
149
  separator: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-orientation'],
127
150
  slider: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-orientation', 'aria-readonly'],
128
- spinbutton: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-readonly', 'aria-required'],
129
- switch: ['aria-checked'],
130
- tab: ['aria-selected'],
151
+ spinbutton: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-readonly', 'aria-required', 'aria-activedescendant'],
152
+ switch: ['aria-checked', 'aria-expanded', 'aria-readonly', 'aria-required'],
153
+ tab: ['aria-selected', 'aria-expanded', 'aria-posinset', 'aria-setsize'],
131
154
  table: ['aria-colcount', 'aria-rowcount'],
132
- tablist: ['aria-multiselectable', 'aria-orientation'],
155
+ tablist: ['aria-multiselectable', 'aria-orientation', 'aria-level', 'aria-activedescendant'],
133
156
  textbox: ['aria-activedescendant', 'aria-autocomplete', 'aria-multiline', 'aria-placeholder', 'aria-readonly', 'aria-required'],
134
157
  toolbar: ['aria-activedescendant', 'aria-orientation'],
135
- tree: ['aria-multiselectable', 'aria-readonly', 'aria-required', 'aria-orientation'],
136
- treegrid: ['aria-multiselectable', 'aria-readonly', 'aria-required', 'aria-orientation', 'aria-colcount', 'aria-rowcount'],
158
+ tree: ['aria-multiselectable', 'aria-required', 'aria-orientation', 'aria-activedescendant'],
159
+ treegrid: ['aria-multiselectable', 'aria-readonly', 'aria-required', 'aria-orientation', 'aria-colcount', 'aria-rowcount', 'aria-activedescendant'],
137
160
  treeitem: ['aria-checked', 'aria-selected', 'aria-expanded', 'aria-level', 'aria-posinset', 'aria-setsize']
138
161
  };
139
162