## Architecture `attachAxe` is a thin, deterministic wrapper around `@axe-core/playwright`'s `AxeBuilder`. The factory takes a `Page` and per-attach defaults, returns an `AxeHandle` whose two methods (`assertNoViolations`, `getViolations`) build a fresh `AxeBuilder` on each call so callers can scope per-assertion (e.g. exclude a known-noisy third-party widget for one check without leaking the override). ### Adapter choice `@axe-core/playwright` is the canonical Playwright adapter and ships the axe-core engine as a CDN-free script that is `evaluate()`-injected into the page. It matches the rest of the testing-e2e stack (Playwright primitives, no Node-only deps). Alternatives (`axe-playwright`, hand-rolled `page.addScriptTag`) were rejected: the former is community-maintained with weaker semver discipline, the latter duplicates work and forces us to track axe-core releases manually. ### Defaults - **Rule set:** WCAG 2.1 AA (`['wcag2a','wcag2aa','wcag21a','wcag21aa']`). 2.2 tags are MAY-include via `runOnly` override, not default, until baseline runs prove no churn. - **Impact gate:** `serious` + `critical` fail by default. `strict: true` widens to all four (`minor`, `moderate`, `serious`, `critical`). - **Scope:** entire page unless `include`/`exclude` provided. ### Reporter Violations are written to `playwright-report/a11y/.json` keyed by rule id, with `impact`, `nodes[].target`, `nodes[].html`, `helpUrl`. The path is fixed so the release-readiness aggregator (owned by `enterprise-testbed-verification`) can glob a single directory. ### Error model `A11yError extends TestingE2eError` carrying `{ code, cause?, context: { violations, ruleSet, url } }`. Codes: - `TESTING_E2E.A11Y.VIOLATIONS_FOUND` — assertion failed. - `TESTING_E2E.A11Y.AXE_LOAD_FAILED` — axe-core script injection failed (CSP, navigation race). - `TESTING_E2E.A11Y.INVALID_RULESET` — caller passed an unknown WCAG tag. ### Baseline-update workflow There is no baseline file. Either the page passes the rule set or it fails — drift is impossible by construction. To temporarily ignore a rule, `disableRules: ['rule-id']` with a code-comment justification and an issue link is the only mechanism. ### Cross-links - Release-readiness gate: `../enterprise-testbed-verification/proposal.md`. - Audit driver: `/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md` §2 row 1 and §7 row "Inspect browser console". - Project rules: `/mnt/s/web/genesis/CLAUDE.md` "Web App & Page Verification".