@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/CHANGELOG.md CHANGED
@@ -4,9 +4,26 @@ All notable changes to this project are documented here, in [Keep a Changelog](h
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Added
8
+ - Completed French (`fr`) localization: translated the 313 remaining `src/i18n/fr.js` keys, bringing French to full parity with English (600/600 keys, up from 287/600). Verified against a live scan (`locale: 'fr'`) and confirmed no key/placeholder mismatches against `src/i18n/en.js`. Landmark terminology uses "point de repère" per MDN's French ARIA documentation.
9
+
10
+ ### Fixed
11
+ - `docs/RULE_TAXONOMY.md`: automatic rules' allowed outcomes was missing `cantTell` (4 rules use it as a defensive fallback); the "current intents"/"current families" lists were stale and read as exhaustive when the ruleset actually spans 54 suffixes/68 prefixes — reframed as illustrative with a pointer to the generated `RULE_CATALOG.md`; `data.visibilityFilter.targetSet` was missing the `'dom'` value (only `'acc'` was listed).
12
+ - `docs/OUTPUT_SCHEMA.md`: the `visibilityFilter` type was missing its always-present `eligible` field; the worked example's `structuralPath` values for the button/img pair were swapped; removed a citation to a note in `RULE_AUTHORING.md` that doesn't exist there.
13
+ - `docs/I18N.md`: stale key counts (`en` listed as 590, actual 600; `fr` coverage listed as ~49%, actual ~48% at the time) — now updated to reflect full parity.
14
+
15
+ ## [1.0.1] - 2026-07-28
16
+
7
17
  ### Changed
8
18
  - Trimmed the published npm package: rule-authoring scaffolding (`docs/RULE_TEMPLATE.*`, `docs/RULE_TEST_TEMPLATE.md`, `docs/RULE_TEST_AUTHORING.md`, `docs/TEST_OUTCOME_STABILITY.md`) and the not-yet-documented `src/explain` module no longer ship in the tarball — both stay in the git repo for contributors.
9
19
  - README rewritten for clarity: corrected the install command and `require()` examples to the actual package name (`@surea11y/core`), and updated the rule count to 125.
20
+ - `docs/ENGINE_OPTIONS.md`: documented the previously-undocumented `visibilityMode` option (`'styleOnly'`/`'styleAndGeometry'`, scoped to the three contrast rules), and added a "Recipes" section with composed, runnable examples for common scenarios (CI gating, auditor-mode contrast passes, scoped re-scans, reproducible snapshots, custom rules).
21
+
22
+ ### Fixed
23
+ - `aria-allowed-attr`'s `SUPPORTED_ATTRS_BY_ROLE` table reconciled against the published WAI-ARIA 1.2 Recommendation (via `aria-query`, not axe-core's table — axe-core's own source comments confirm many of its `aria-expanded` allowances are deliberate ARIA 1.1 legacy carryovers, not current-spec facts). Added `aria-expanded` to 10 roles (checkbox, columnheader, gridcell, listbox, menuitemcheckbox, menuitemradio, row, rowheader, switch, tab) and `aria-activedescendant` to 8 composite-widget roles (combobox, grid, listbox, radiogroup, row, spinbutton, tablist, treegrid), plus smaller posinset/setsize/readonly/required/level gaps; removed `tree`'s unverified `aria-readonly`. `listitem` was already correct and is unchanged.
24
+ - README: a "Real browser execution" code sample passed four positional arguments to `page.evaluate()` and claimed it worked with "any" automation framework — Playwright's `page.evaluate()` only accepts one argument alongside the function and throws on this exact pattern. Now shown as Puppeteer-specific, with a pointer to `INTEGRATION.md`'s wrapper for Playwright.
25
+ - README: the JSON output example referenced a nonexistent rule id (`link-name-quality`); corrected to the real id, `link-name-quality-manual`.
26
+ - README: Quick Start code samples labeled the runner's four positional arguments as `url, ruleFilter, options, policy`; corrected to the actual names (`pageUrl, contextSelector, engineOptions, runOnly`) used consistently elsewhere in the docs.
10
27
 
11
28
  ## [1.1.1] - 2026-07-24
12
29
 
package/README.md CHANGED
@@ -1,75 +1,216 @@
1
- # surea11y
1
+ # @surea11y/core
2
2
 
3
- **surea11y runs automated accessibility tests against a real DOM** — 125 rules, standards-traceable, safe by default. Point it at a static HTML file or a URL, or run it inside a real rendered page — via Puppeteer, Playwright, Selenium, Cypress, or any other browser-automation driver — and it tells you exactly what fails a WCAG Success Criterion, where, and why — no browser extension, no dashboard sign-up, just a function call or a CLI command.
3
+ > **Reliable accessibility testing for real web applications.**
4
4
 
5
- What makes it trustworthy enough to gate a build on:
5
+ surea11y is an accessibility engine designed to help development teams
6
+ identify objective accessibility issues early in the software lifecycle.
7
+ It runs against either static HTML or fully rendered browser pages,
8
+ producing deterministic, standards-traceable results that are suitable
9
+ for local development, automated testing and CI/CD pipelines.
6
10
 
7
- - **No false alarms by design.** `fail` is reserved for deterministic, high-confidence, normative violations — never a heuristic guess. Where certainty isn't possible, the result says so (`cantTell`) instead of forcing a guess into a `pass`/`fail`.
8
- - **Deterministic, every time.** Same input, same result — no built-in clock, no randomness, nothing that can make a CI run flaky.
9
- - **Every rule is atomic and traceable.** One normative decision per rule, mapped to a WCAG Success Criterion where one applies, with a machine-readable rule ID and a human-readable hint for fixing it.
10
- - **Zero runtime dependencies in the library itself** (see [`SECURITY.md`](./SECURITY.md)) — the CLI is the only part that pulls in `jsdom`, and only if you use it.
11
- - **Runs anywhere your DOM does — jsdom or a real browser, each with real tradeoffs.** See "Static HTML vs. a live browser" below before picking one.
12
- - **Fully localized output** (English, French, more on the way) without sacrificing stable, locale-independent rule IDs and machine-readable keys.
13
- - **Extensible.** Register custom rules at scan time, filter by rule ID/tag/WCAG version, or scan several regions of a page in a single pass.
11
+ Unlike browser extensions or cloud-based services, surea11y is a
12
+ library-first project. You install it, run it where your code runs, and
13
+ receive structured results that can be consumed by people, scripts or
14
+ reporting tools.
14
15
 
15
- ## Static HTML vs. a live browser
16
+ ## Why surea11y?
16
17
 
17
- This is the decision that determines what content the engine can actually see — pick wrong and you'll get clean results on a page that isn't really accessible.
18
+ Accessibility automation is only valuable if developers can trust its
19
+ results.
18
20
 
19
- - **CLI (`scan ./file.html` / `scan <url>`) and jsdom (`runDomRulesInPage`) — no JavaScript execution, no real CSS layout engine.** These parse markup as text/fetch it as-is; a URL scan sees exactly what the server sent, not what the page looks like after client JS runs. Fine for static or server-rendered HTML. Blind to anything a client-side framework adds after load — SPA content, hydration-driven attribute changes, modals/dropdowns that only exist post-interaction. Layout-dependent rules (`target-size-minimum`, most notably) report `notApplicable` rather than guess, since jsdom has no real `getBoundingClientRect()`.
20
- - **A real browser (`runa11yCoreInPage`, run inside a live page — Puppeteer/Playwright's `page.evaluate`, Selenium's `execute_script`, Cypress's in-browser test code, a browser extension, or any other driver that can run code in the page) — sees the fully rendered, post-JS, post-hydration DOM and real computed layout.** Required for client-rendered apps, and for accurate results on layout-dependent rules.
21
+ surea11y is built around a conservative philosophy: **never report
22
+ certainty when certainty cannot be established objectively.**
21
23
 
22
- Full detail on both: [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md) (everything each mode structurally can't see) and [`docs/INTEGRATION.md`](./docs/INTEGRATION.md) (the two patterns, in depth, with working code).
24
+ Instead of relying on heuristics that may generate false positives, each
25
+ rule makes a single deterministic decision. If a violation can be
26
+ proven, the outcome is `fail`. If human judgement is required, the
27
+ engine reports `cantTell` instead of guessing.
23
28
 
24
- ## Install
29
+ This makes the engine predictable, reproducible and suitable for
30
+ automated quality gates.
25
31
 
26
- ```sh
32
+ ### Key principles
33
+
34
+ - **Deterministic execution.** The same input always produces the same
35
+ output.
36
+ - **Conservative by design.** `fail` is reserved for objective,
37
+ normative violations, minimizing false alarms.
38
+ - **Standards traceability.** Rules map to the applicable WCAG Success
39
+ Criterion whenever appropriate.
40
+ - **Stable machine-readable output.** Rule identifiers remain stable
41
+ regardless of language.
42
+ - **Framework independent.** Run inside jsdom or any browser
43
+ automation framework.
44
+ - **Extensible.** Add custom rules, register policies and filter scans
45
+ by rule IDs, tags or WCAG version.
46
+ - **Localized reporting.** Human-readable messages can be translated
47
+ without affecting machine-readable data.
48
+
49
+ ---
50
+
51
+ ## Choosing the right execution model
52
+
53
+ surea11y supports two complementary execution models. Choosing the
54
+ correct one is essential because it determines which parts of the page
55
+ the engine can inspect.
56
+
57
+ ### Static HTML
58
+
59
+ The CLI (`scan file.html` or `scan https://example.com`) and
60
+ `runDomRulesInPage()` analyse HTML without executing page JavaScript.
61
+
62
+ This approach is ideal for:
63
+
64
+ - static websites;
65
+ - server-side rendered applications;
66
+ - generated documentation;
67
+ - pre-rendered HTML.
68
+
69
+ Because JavaScript is never executed, the engine cannot inspect content
70
+ added after page load. Likewise, rules requiring real layout information
71
+ cannot be evaluated and will report `notApplicable` instead of making
72
+ assumptions.
73
+
74
+ ### Fully rendered browser
75
+
76
+ `runa11yCoreInPage()` executes directly inside a live browser page.
77
+
78
+ This allows the engine to inspect:
79
+
80
+ - client-rendered applications;
81
+ - hydrated pages;
82
+ - computed styles;
83
+ - runtime DOM changes;
84
+ - layout-dependent rules.
85
+
86
+ The function is browser-agnostic and can be executed through Puppeteer,
87
+ Playwright, Selenium, Cypress or any environment capable of evaluating
88
+ JavaScript inside the page.
89
+
90
+ For a detailed comparison of both execution models, see
91
+ `docs/LIMITATIONS.md` and `docs/INTEGRATION.md`.
92
+
93
+ ---
94
+
95
+ ## Installation
96
+
97
+ Install the core package from npm:
98
+
99
+ ```bash
27
100
  npm install @surea11y/core
28
101
  ```
29
102
 
30
- ## Quickstart
103
+ The core engine has no runtime dependencies — requiring the library
104
+ itself never loads `jsdom`. Only the bundled CLI loads it, and only
105
+ when you actually run a scan, keeping the library lightweight and
106
+ suitable for embedding into your own tooling.
107
+
108
+ ---
31
109
 
32
- ### CLI — fastest way to just try it
110
+ ## Quick Start
33
111
 
34
- ```sh
112
+ ### CLI
113
+
114
+ The CLI is the fastest way to analyse a page without writing any code.
115
+
116
+ ```bash
35
117
  npx @surea11y/core scan ./index.html
36
118
  npx @surea11y/core scan https://example.com/
37
119
  ```
38
120
 
39
- Static HTML only (no page JavaScript execution) — see [`docs/CLI.md`](./docs/CLI.md) for every flag, exit codes, and when you need the library instead (client-rendered content, real-browser geometry).
121
+ The CLI analyses static HTML. It does not execute client-side
122
+ JavaScript, making it ideal for static sites and server-rendered
123
+ applications.
124
+
125
+ For available options, exit codes and advanced usage, see `docs/CLI.md`.
126
+
127
+ ---
128
+
129
+ ### Library
40
130
 
41
- ### Library — for your own scripts, test suites, or browser-automation code
131
+ surea11y exposes two entry points depending on where your code executes.
42
132
 
43
- Two runner functions, depending on where you run it (see [`docs/INTEGRATION.md`](./docs/INTEGRATION.md) for the full explanation of the difference and more patterns):
133
+ #### Node.js + jsdom
134
+
135
+ Use `runDomRulesInPage()` when your application already has a DOM
136
+ available through jsdom.
44
137
 
45
138
  ```js
46
- // Node + jsdom (no real browser needed)
47
- const { JSDOM } = require('jsdom');
48
- const { runDomRulesInPage } = require('@surea11y/core');
139
+ const { JSDOM } = require("jsdom");
140
+ const { runDomRulesInPage } = require("@surea11y/core");
141
+
142
+ const dom = new JSDOM('<img src="logo.png">', {
143
+ url: "https://example.com/",
144
+ pretendToBeVisual: true
145
+ });
49
146
 
50
- const dom = new JSDOM('<img src="logo.png">', { url: 'https://example.com/', pretendToBeVisual: true });
51
147
  global.window = dom.window;
52
148
  global.document = dom.window.document;
53
149
 
54
- const result = runDomRulesInPage('https://example.com/', null, {}, null);
55
- console.log(result.checksResults.filter((r) => r.outcome === 'fail'));
56
- // -> [{ ruleId: 'img-alt-present', outcome: 'fail', occurrences: [...] }]
150
+ const result = runDomRulesInPage(
151
+ "https://example.com/", // pageUrl
152
+ null, // contextSelector
153
+ {}, // engineOptions
154
+ null // runOnly
155
+ );
156
+
157
+ console.log(
158
+ result.checksResults.filter(r => r.outcome === "fail")
159
+ );
57
160
  ```
58
161
 
59
- ```js
60
- // Puppeteer / Playwright, against a real rendered page
61
- const { runa11yCoreInPage } = require('@surea11y/core');
162
+ This approach is fast, simple and requires no browser installation. It
163
+ is best suited for static HTML and server-rendered pages.
164
+
165
+ ---
62
166
 
63
- const result = await page.evaluate(runa11yCoreInPage, 'https://example.com/', null, {}, null);
167
+ #### Real browser execution
168
+
169
+ For client-rendered applications, execute the engine inside the page
170
+ itself.
171
+
172
+ ```js
173
+ // Puppeteer
174
+ const { runa11yCoreInPage } = require("@surea11y/core");
175
+
176
+ const result = await page.evaluate(
177
+ runa11yCoreInPage,
178
+ "https://example.com/", // pageUrl
179
+ null, // contextSelector
180
+ {}, // engineOptions
181
+ null // runOnly
182
+ );
64
183
  ```
65
184
 
66
- `runa11yCoreInPage` is fully self-contained (see [`docs/INTEGRATION.md`](./docs/INTEGRATION.md)), so it isn't Puppeteer/Playwright-specific — the same function works with Selenium's `execute_script`, runs natively in Cypress's in-browser test code, or in a browser extension content script. Puppeteer/Playwright are just the two shown here.
185
+ `runa11yCoreInPage` is self-contained (its whole rule catalog is
186
+ inlined), so it works unmodified with any framework capable of
187
+ evaluating a function inside the page — Puppeteer, Selenium, Cypress,
188
+ browser extensions and custom drivers can all call it exactly as
189
+ shown above. **Playwright is the one exception**: its `page.evaluate()`
190
+ only accepts a single argument alongside the function, so
191
+ `runa11yCoreInPage` needs a one-argument wrapper instead of four
192
+ positional arguments — see `docs/INTEGRATION.md` for the exact pattern.
193
+
194
+ This allows the engine to inspect the fully rendered DOM exactly as
195
+ users experience it.
196
+
197
+ For complete integration examples, see `docs/INTEGRATION.md`.
67
198
 
68
- ## What you get back
199
+ ---
200
+
201
+ ## Understanding the Results
202
+
203
+ Every scan returns a structured JSON document designed for both
204
+ developers and automated tooling.
205
+
206
+ A simplified example looks like this:
69
207
 
70
208
  ```json
71
209
  {
72
- "engine": { "tag": "a11ycore", "schemaVersion": "1.0.0" },
210
+ "engine": {
211
+ "tag": "a11ycore",
212
+ "schemaVersion": "1.0.0"
213
+ },
73
214
  "url": "https://example.com/",
74
215
  "checksResults": [
75
216
  {
@@ -78,68 +219,228 @@ const result = await page.evaluate(runa11yCoreInPage, 'https://example.com/', nu
78
219
  "severity": "serious",
79
220
  "confidence": "high",
80
221
  "occurrences": [
81
- { "selector": "html > body > img", "html": "<img src=\"logo.png\">", "summary": "Missing alt attribute on <img>.", "hint": "Add an alt attribute (use alt=\"\" only for decorative images)." }
222
+ {
223
+ "selector": "html > body > img",
224
+ "summary": "Missing alt attribute on <img>.",
225
+ "hint": "Add an alt attribute or use alt=\"\" for decorative images."
226
+ }
227
+ ]
228
+ },
229
+ {
230
+ "ruleId": "link-name-quality-manual",
231
+ "outcome": "cantTell",
232
+ "severity": "minor",
233
+ "confidence": "medium",
234
+ "occurrences": [
235
+ {
236
+ "selector": "html > body > a:nth-child(3)",
237
+ "summary": "Link text may not be descriptive enough out of context.",
238
+ "hint": "Confirm the link text clearly describes its destination or purpose."
239
+ }
82
240
  ]
83
241
  }
84
- ],
85
- "rulesResults": []
242
+ ]
86
243
  }
87
244
  ```
88
245
 
89
- Full field-by-field reference (what every field means, what `cantTell` vs `notApplicable` means, how composite WCAG-SC rollups work): [`docs/OUTPUT_SCHEMA.md`](./docs/OUTPUT_SCHEMA.md).
246
+ The second result illustrates the engine's conservative stance: it can
247
+ confirm a link has text, but whether that text is genuinely descriptive
248
+ requires human judgement, so it reports `cantTell` instead of guessing.
249
+
250
+ Each finding contains enough information to answer four questions:
251
+
252
+ - **What failed?** (`ruleId`)
253
+ - **Why did it fail?** (`summary`)
254
+ - **Where did it fail?** (`selector` and occurrences)
255
+ - **How can it be fixed?** (`hint`)
256
+
257
+ The complete schema also includes confidence, severity, WCAG
258
+ traceability, composite rule results and other metadata intended for
259
+ reporting and automation.
260
+
261
+ For a complete field-by-field reference, see `docs/OUTPUT_SCHEMA.md`.
262
+
263
+ ---
90
264
 
91
265
  ## Documentation
92
266
 
93
- | Doc | What's in it |
94
- |---|---|
95
- | [`docs/OUTPUT_SCHEMA.md`](./docs/OUTPUT_SCHEMA.md) | The full result shape — every field, every outcome value, worked examples. Start here. |
96
- | [`docs/CLI.md`](./docs/CLI.md) | The `surea11y scan` command — flags, exit codes, what it can and can't scan. |
97
- | [`docs/ENGINE_OPTIONS.md`](./docs/ENGINE_OPTIONS.md) | Every `engineOptions`/`runOnly` field: selecting rules, locale, shadow DOM, contrast mode, policy, and the common `runOnly` gotcha. |
98
- | [`docs/INTEGRATION.md`](./docs/INTEGRATION.md) | Node/jsdom vs. real-browser (Puppeteer, Playwright, Selenium, Cypress, or any driver) usage, CI gating, browser-extension context. |
99
- | [`docs/BINDING_AUTHORS_GUIDE.md`](./docs/BINDING_AUTHORS_GUIDE.md) | Building a *new* framework binding (Puppeteer, Cypress, ...)? What the engine gives you for free vs. what every binding has to build itself, checked against what `surea11y-playwright` actually needed. |
100
- | [`docs/RULE_CATALOG.md`](./docs/RULE_CATALOG.md) | All 125 rules — id, WCAG SC, level, confidence, severity. Generated; run `npm run docs:rule-catalog` to refresh. |
101
- | [`docs/WCAG_CONFORMANCE.md`](./docs/WCAG_CONFORMANCE.md) | How rule outcomes roll up to an SC-level / A-AA-AAA conformance picture, and what that picture does and doesn't claim. |
102
- | [`docs/POLICY.md`](./docs/POLICY.md) | The `a11y`/`generic` policy contracts — what they control and how to customize. |
103
- | [`docs/I18N.md`](./docs/I18N.md) | Current locale coverage and how to contribute a translation. |
104
- | [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md) | What this engine cannot do, and why — stated upfront. |
105
- | [`docs/TROUBLESHOOTING.md`](./docs/TROUBLESHOOTING.md) | Common gotchas, FAQ. |
106
- | [`docs/RULE_AUTHORING.md`](./docs/RULE_AUTHORING.md) | How to write a new rule — the exact module contract, a critical footgun to avoid, fixture requirements. |
107
- | [`docs/RULE_TAXONOMY.md`](./docs/RULE_TAXONOMY.md) | How rules are categorized. |
108
- | [`CONTRIBUTING.md`](./CONTRIBUTING.md) | How to add/change a rule, commit conventions, required checks before a PR. |
109
- | [`SECURITY.md`](./SECURITY.md) | Scope, threat model, how to report a vulnerability. |
110
- | [`CHANGELOG.md`](./CHANGELOG.md) | What changed, release to release. |
111
-
112
- ## Folder layout
267
+ The project documentation is organized by topic so you can start quickly
268
+ and progressively explore more advanced features.
113
269
 
114
- ```
270
+ | Document | Description |
271
+ |---|---|
272
+ | `docs/OUTPUT_SCHEMA.md` | Complete description of every field returned by the engine. |
273
+ | `docs/CLI.md` | CLI commands, options, exit codes and examples. |
274
+ | `docs/ENGINE_OPTIONS.md` | Configuration, filtering, policies and localization. |
275
+ | `docs/INTEGRATION.md` | Using surea11y with jsdom, Playwright, Puppeteer, Selenium, Cypress and other drivers. |
276
+ | `docs/BINDING_AUTHORS_GUIDE.md` | Building new framework integrations on top of the engine. |
277
+ | `docs/RULE_CATALOG.md` | Reference of every built-in accessibility rule. |
278
+ | `docs/WCAG_CONFORMANCE.md` | Understanding WCAG rollups and conformance reporting. |
279
+ | `docs/POLICY.md` | Built-in policy contracts and customization. |
280
+ | `docs/I18N.md` | Translation support and localization. |
281
+ | `docs/LIMITATIONS.md` | Structural limitations of automated accessibility testing. |
282
+ | `docs/TROUBLESHOOTING.md` | Frequently asked questions and common issues. |
283
+ | `docs/RULE_AUTHORING.md` | Writing custom accessibility rules. |
284
+ | `docs/RULE_TAXONOMY.md` | Rule categorization model. |
285
+ | `CONTRIBUTING.md` | Contributing guidelines. |
286
+ | `SECURITY.md` | Security policy and vulnerability reporting. |
287
+ | `CHANGELOG.md` | Release history. |
288
+
289
+ ---
290
+
291
+ ## Philosophy
292
+
293
+ surea11y is built on a simple principle:
294
+
295
+ > Automate what can be determined objectively. Never pretend to automate
296
+ > what cannot.
297
+
298
+ Accessibility is not something that can be reduced to a single score or
299
+ a binary pass/fail result. Some WCAG requirements can be evaluated with
300
+ complete confidence, while others require human judgement, knowledge of
301
+ context or usability evaluation.
302
+
303
+ Rather than hiding that distinction, surea11y makes it explicit.
304
+
305
+ That philosophy influences every rule in the engine and is the reason
306
+ outcomes such as `cantTell` and `notApplicable` exist. They communicate
307
+ uncertainty honestly instead of encouraging misleading conclusions.
308
+
309
+ ### What surea11y won't catch
310
+
311
+ Being explicit about the boundaries of automation is part of the same
312
+ philosophy. For example, surea11y will not:
313
+
314
+ - confirm that alt text is *meaningful*, only that it is present
315
+ (an alt attribute of `"image123.png"` passes the objective check);
316
+ - judge whether a color contrast choice is aesthetically appropriate,
317
+ only whether it meets the applicable contrast ratio;
318
+ - determine whether an error message actually *explains* the problem,
319
+ since that depends on validation logic a static scan can't see;
320
+ - detect a keyboard focus trap or content clipped at 400% zoom, since
321
+ both require simulating real user interaction over time, not just
322
+ reading the DOM at one instant.
323
+
324
+ These are the cases where the engine reports `cantTell`, and where a
325
+ human reviewer's judgement remains necessary. See
326
+ `docs/LIMITATIONS.md` for the complete list of structural limitations.
327
+
328
+ The objective of the project is not to replace accessibility experts. It
329
+ is to remove repetitive verification work, provide reliable automated
330
+ feedback to developers and help teams integrate accessibility into their
331
+ normal development process.
332
+
333
+ ---
334
+
335
+ ## Project Structure
336
+
337
+ The repository is organised so that the accessibility engine, rule
338
+ implementations and supporting infrastructure remain clearly separated.
339
+
340
+ ```text
115
341
  bin/
116
- core.js # CLI entry point (`npx @surea11y/core scan ...`) — see docs/CLI.md
342
+ core.js # CLI entry point
343
+
117
344
  src/
118
- index.js # public entry point (re-exports src/core.js)
119
- core.js # GENERATED — do not edit directly, see "Build" below
345
+ index.js # Public API
346
+ core.js # Generated runtime bundle
347
+
120
348
  checks/
121
- automatic/ # type: 'automatic' rules — can return `fail`
122
- manual/ # type: 'manual' rules — advisory, capped at `cantTell`
123
- core/ # shared runtime: dom-runner, dom-helpers, aria-helpers, contrast-helpers
124
- policy/ # policy contracts (a11y / generic)
125
- i18n/ # locale dictionaries (en.js, fr.js)
126
- coverage/ # WCAG facet definitions
127
- catalogs/ # composite (WCAG-SC rollup) rule definitions
349
+ automatic/ # Deterministic automated rules
350
+ manual/ # Advisory rules (maximum outcome: cantTell)
351
+
352
+ core/ # Shared engine runtime
353
+ policy/ # Policy implementations
354
+ i18n/ # Localized messages
355
+ coverage/ # WCAG coverage definitions
356
+ catalogs/ # Composite rule catalogs
357
+
128
358
  scripts/
129
- build-core.js # bundles src/checks/**, src/core/**, src/i18n/** into src/core.js
130
- docs/ # everything in the table above
359
+ build-core.js # Generates the runtime bundle
360
+
361
+ docs/ # Project documentation
362
+
131
363
  tests/
132
- fixtures/ # one *-all-scenarios.html scenario page per rule
133
- engine-checks/ # per-rule unit + fixture-coverage tests
364
+ fixtures/ # Rule fixtures
365
+ engine-checks/ # Engine and rule tests
366
+ ```
367
+
368
+ This separation allows the engine to evolve independently from framework
369
+ integrations while keeping the rule authoring experience consistent.
370
+
371
+ ---
372
+
373
+ ## Building the Project
374
+
375
+ After cloning the repository, regenerate the bundled runtime:
376
+
377
+ ```bash
378
+ npm run build
134
379
  ```
135
380
 
136
- ## Build & test
381
+ To execute the complete test suite:
137
382
 
138
- ```sh
139
- npm run build # regenerate src/core.js from source
140
- npm test # build + run the full test suite
383
+ ```bash
384
+ npm test
141
385
  ```
142
386
 
387
+ The test suite validates rule behaviour, fixture coverage and overall
388
+ engine consistency to ensure deterministic results across releases.
389
+
390
+ ---
391
+
392
+ ## Contributing
393
+
394
+ Contributions are welcome.
395
+
396
+ Whether you are fixing a bug, improving documentation or implementing a
397
+ new accessibility rule, please keep the project's core principles in
398
+ mind:
399
+
400
+ - deterministic behaviour;
401
+ - objective rule evaluation;
402
+ - conservative outcome reporting;
403
+ - stable public APIs;
404
+ - standards traceability;
405
+ - comprehensive test coverage.
406
+
407
+ Before opening a pull request, please review `CONTRIBUTING.md` for the
408
+ complete development workflow and coding conventions.
409
+
410
+ ---
411
+
412
+ ## Security
413
+
414
+ Security issues should be reported responsibly.
415
+
416
+ Please refer to `SECURITY.md` for the project's security policy,
417
+ supported versions and the preferred disclosure process.
418
+
419
+ ---
420
+
143
421
  ## License
144
422
 
145
- [MIT](./LICENSE)
423
+ This project is released under the MIT License.
424
+
425
+ See the accompanying `LICENSE` file for the complete license text.
426
+
427
+ ---
428
+
429
+ ## Final Notes
430
+
431
+ surea11y was created with a simple goal: make accessibility testing
432
+ trustworthy enough to become part of everyday software engineering.
433
+
434
+ It does not attempt to replace manual accessibility reviews, usability
435
+ testing or expert judgement. Instead, it focuses on providing reliable
436
+ automated verification for the parts of accessibility that can be
437
+ evaluated objectively.
438
+
439
+ By combining deterministic rules, standards traceability, stable
440
+ machine-readable output and honest reporting of uncertainty, surea11y
441
+ enables teams to detect accessibility issues earlier, reduce regressions
442
+ and build more accessible products with confidence.
443
+
444
+ Accessibility is not a checkbox performed before release. It is an
445
+ engineering practice that benefits from continuous feedback, and
446
+ surea11y is designed to become one of those feedback loops.
@@ -82,6 +82,7 @@ const engineOptions = {
82
82
  mode: 'strictConformance', // 'strictConformance' (default) | 'auditorAssist'
83
83
  rootCanvasFallback: '#ffffff' // background assumed when the true root background isn't computable
84
84
  },
85
+ visibilityMode: 'styleOnly', // 'styleOnly' (default) | 'styleAndGeometry' — see below; scoped to the contrast rules only
85
86
 
86
87
  policyContract: 'a11y', // 'a11y' (default) | 'generic' | inline contract object — see POLICY.md
87
88
  policy: { // optional overrides on top of policyContract
@@ -116,13 +117,77 @@ const engineOptions = {
116
117
  | `timestamp` | Passed straight through to the result's top-level `timestamp` field; the engine does not generate one itself (deterministic-by-design). |
117
118
  | `contrast.mode` | `strictConformance` (default): contrast rules stay silent (`notApplicable`/skip) whenever the true rendered background isn't confidently computable, to protect against false `fail`s. `auditorAssist`: trades some of that safety margin for more findings, intended for a human auditor who will double-check flagged cases, not for unattended CI gating. |
118
119
  | `contrast.rootCanvasFallback` | The assumed page background color when it's not computable at all — only matters in `auditorAssist` mode. |
120
+ | `visibilityMode` | Controls how strict the three contrast rules (`contrast-minimum`, `contrast-enhanced`, `contrast-computable`) are about deciding a text node is actually eligible to check. **Not read by any other rule.** `'styleOnly'` (default): eligibility is CSS-only — `display`, `visibility`, `opacity`, ancestor-hiding, etc. `'styleAndGeometry'`: adds real layout checks (`getClientRects()`/`getBoundingClientRect()`) on top of that — text with no client rects, or zero width/height, is excluded too. Reach for `'styleAndGeometry'` when running under a real browser/Playwright-Puppeteer (`runa11yCoreInPage`) and you want contrast findings to reflect actual rendered layout rather than just computed style; under plain jsdom (`runDomRulesInPage`) there's no real layout engine, so `'styleAndGeometry'` mostly just adds `getBoundingClientRect()` zero-size checks, not true clipping/overflow detection — see [`LIMITATIONS.md`](./LIMITATIONS.md). |
119
121
  | `policyContract` / `policy` | See [`POLICY.md`](./POLICY.md) — controls which outcomes/confidence values are allowed and whether manual rules' would-be `fail`s get coerced to `cantTell`. |
120
- | `output.includeSelector` / `.includeHtml` | Only affects the small number of rules (currently 4 of 123) that rely on the engine's automatic selector/HTML fill-in rather than building their own — most rules set `selector`/`html` themselves inside `runInPage` and are unaffected by this option. Not a reliable way to strip selectors/HTML from all output. |
122
+ | `output.includeSelector` / `.includeHtml` | Only affects the small number of rules (currently 4 of 125) that rely on the engine's automatic selector/HTML fill-in rather than building their own — most rules set `selector`/`html` themselves inside `runInPage` and are unaffected by this option. Not a reliable way to strip selectors/HTML from all output. |
121
123
  | `rules[ruleId]` | Passed through to that rule as `ctx.config`. The plumbing exists end-to-end, but **no shipped rule currently reads `ctx.config`** — this is infrastructure for future per-rule configurability, not a lever that changes any of today's 123 rules' behavior. |
122
124
  | `probes` | An optional, JSON-safe evidence object your host application can supply (depth- and size-capped by the engine before rules see it, via `ctx.inputs.probes`) — for future rules that might accept externally-supplied signals (e.g. real layout measurements a static DOM scan can't compute itself). Not consumed by any current rule. |
123
125
  | `perfStats` / `profileRules` | Debug-only. `perfStats: true` returns internal counters on the result's `perfStats` field; `profileRules: true` additionally adds a per-rule timing breakdown. Shape is not part of the stable output contract — don't build on it. |
124
126
  | `pingWaitTime` / `frameWaitTime` | Only read by `runa11yCoreAcrossFrames` (see [`INTEGRATION.md`](./INTEGRATION.md#cross-frame-scanning-including-cross-origin)) — how long to wait for a child frame to answer a ping (default `500`ms) and a full run request (default `60000`ms) before treating it as unreachable. Ignored by `runDomRulesInPage`/`runa11yCoreInPage`. |
125
127
 
128
+ ## Recipes — composing options for real scenarios
129
+
130
+ The reference above documents each option in isolation. These combine several at once, for scenarios you're likely to actually hit.
131
+
132
+ **CI gate: WCAG 2.2 AA only, ignore a third-party widget you don't control**
133
+
134
+ ```js
135
+ runDomRulesInPage(url, null, {
136
+ excludeSelectors: ['#cookie-banner', '.intercom-launcher'],
137
+ tags: { include: 'wcag2a,wcag2aa,wcag21a,wcag21aa,wcag22a,wcag22aa' }
138
+ }, null);
139
+ ```
140
+
141
+ **Human auditor doing a deep contrast pass in a real browser** — trade some false-positive protection for more findings, and check real layout (not just computed style) since a real page is being driven. Shown with Puppeteer's `page.evaluate` (accepts multiple args); if you're on Playwright, wrap the four positional args into a single object first — see [`INTEGRATION.md`](./INTEGRATION.md):
142
+
143
+ ```js
144
+ const result = await page.evaluate(runa11yCoreInPage, url, null, {
145
+ contrast: { mode: 'auditorAssist' },
146
+ visibilityMode: 'styleAndGeometry'
147
+ }, null);
148
+ ```
149
+
150
+ **Scoped re-scan of one region after a UI change, skipping shadow DOM** — useful in a component-level test where you only care about the widget you just changed:
151
+
152
+ ```js
153
+ runDomRulesInPage(url, '#checkout-form', {
154
+ includeShadowDom: false
155
+ }, { includeRuleIds: ['form-control-programmatic-label-present', 'button-name-present'] });
156
+ ```
157
+
158
+ **Reproducible output for snapshot testing** — pin a `timestamp` so two runs of the same HTML produce byte-identical JSON, and request the debug timing breakdown:
159
+
160
+ ```js
161
+ runDomRulesInPage(url, null, {
162
+ timestamp: '2026-01-01T00:00:00Z',
163
+ perfStats: true,
164
+ profileRules: true
165
+ }, null);
166
+ ```
167
+
168
+ **A custom, org-specific rule alongside the built-ins**, only for this one call:
169
+
170
+ ```js
171
+ runDomRulesInPage(url, null, {
172
+ customRules: [{
173
+ id: 'org-no-inline-onclick',
174
+ meta: { title: 'No inline onclick handlers', defaultSeverity: 'moderate' },
175
+ runInPage(ctx) {
176
+ const els = ctx.helpers.queryAll('[onclick]');
177
+ const occurrences = els.map((el) => ({
178
+ selector: ctx.helpers.buildSelector(el),
179
+ html: el.outerHTML,
180
+ summary: 'Inline onclick handler found.',
181
+ hint: 'Move event handling into an external script.'
182
+ }));
183
+ return { ruleId: ctx.rule.ruleId, outcome: occurrences.length ? 'fail' : 'pass', severity: 'moderate', occurrences };
184
+ }
185
+ }]
186
+ }, null);
187
+ ```
188
+
189
+ See the option-by-option table above for anything not shown here, and the `customRules` section immediately below for the full descriptor contract.
190
+
126
191
  ## `customRules` — runtime-registered rules (equivalent to the rule/check registration pattern used by other engines)
127
192
 
128
193
  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.