From 885f642d358150ebef05849df01a3b24a08095ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Wed, 28 Oct 2026 15:05:23 +0100 Subject: [PATCH 1/6] Replace pa11y-ci with axe-core audit reusing Playwright Closes #241 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../documentation.instructions.md | 13 +- .../github-actions.instructions.md | 3 + .github/skills/documentation.md | 37 +++- .github/skills/feature.md | 3 +- .github/skills/github-actions.md | 26 ++- .github/skills/release.md | 3 +- .github/workflows/a11y.yaml | 66 ++++-- .gitignore | 3 +- .pa11yci.json | 17 -- AGENTS.md | 24 +++ CONTRIBUTING.md | 32 ++- README.md | 19 ++ bun.lock | 190 +++--------------- docs/.vitepress/config.mts | 6 +- docs/.vitepress/theme/custom.css | 14 ++ docs/.vitepress/theme/index.ts | 27 ++- docs/reference/accessibility.md | 123 ++++++++++++ package.json | 7 +- playwright.config.ts | 7 +- scripts/a11y-utils.test.ts | 71 +++++++ scripts/a11y-utils.ts | 49 +++++ scripts/a11y.pw.ts | 129 ++++++++++++ scripts/a11y.ts | 129 ++++++++++++ 23 files changed, 759 insertions(+), 239 deletions(-) delete mode 100644 .pa11yci.json create mode 100644 docs/reference/accessibility.md create mode 100644 scripts/a11y-utils.test.ts create mode 100644 scripts/a11y-utils.ts create mode 100644 scripts/a11y.pw.ts create mode 100644 scripts/a11y.ts diff --git a/.github/instructions/documentation.instructions.md b/.github/instructions/documentation.instructions.md index f66a516..87b4ac7 100644 --- a/.github/instructions/documentation.instructions.md +++ b/.github/instructions/documentation.instructions.md @@ -107,8 +107,17 @@ If the PR modifies any Vue component, CSS, or page layout, also run the accessib ```bash bun run docs:build:a11y bun run docs:preview -- --port 4173 & -bun run docs:a11y # pa11y-ci WCAG 2.1 AA — must report 0 errors +bun run docs:a11y # axe-core WCAG 2.2 AA — must report 0 automated violations +bun run docs:test:a11y # audit failure paths and theme semantics bun run docs:test:responsive # Playwright — 20/20 tests green (4 viewports × 5 pages) ``` -See `.github/skills/documentation.md` for pa11y configuration details, WCAG contrast rules, accessible component patterns, and responsive breakpoint guidance. +Use local Playwright Chromium (`bun run playwright install chromium`) or set +`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to an installed Chrome executable. +CI reuses system Chrome. Inspect both audit reports, including incomplete +findings requiring manual review. Do not disable contrast/ARIA rules or treat +passing automation as WCAG certification. + +See `.github/skills/documentation.md` for the axe runner, WCAG contrast rules, +accessible component patterns and responsive guidance, and +`docs/reference/accessibility.md` for coverage and benchmark methodology. diff --git a/.github/instructions/github-actions.instructions.md b/.github/instructions/github-actions.instructions.md index 02a3d4f..eb54938 100644 --- a/.github/instructions/github-actions.instructions.md +++ b/.github/instructions/github-actions.instructions.md @@ -142,6 +142,9 @@ grep -rn "uses:" .github/workflows/ | grep -v "@[a-f0-9]\{40\}" Also verify: +- [ ] Accessibility uses `@axe-core/playwright` with system Chrome, scans the full sitemap, + preserves failing exit codes, and publishes JSON/Markdown reports without requiring + write permissions on fork PRs (see `.github/skills/github-actions.md`). - [ ] All `.yml` workflow files have been renamed to `.yaml` - [ ] Every new third-party action has a `# vX.Y.Z` comment after its SHA - [ ] New reusable composite actions are under `.github/actions//action.yml` diff --git a/.github/skills/documentation.md b/.github/skills/documentation.md index e6affb1..e3b9d82 100644 --- a/.github/skills/documentation.md +++ b/.github/skills/documentation.md @@ -53,19 +53,45 @@ Dark mode: VitePress applies a `.dark` class on ``. Use `.dark .selector { --- -## Accessibility (WCAG 2.1 AA) +## Accessibility (WCAG 2.2 AA automated checks) -This project maintains **zero pa11y-ci violations** at WCAG 2.1 AA level. +This project requires **zero axe-core automated violations** using WCAG 2.0, +2.1 and 2.2 A/AA tags. A passing automated scan is not WCAG certification. ### Tool ```bash bun run docs:build:a11y # build with VITEPRESS_HOSTNAME=http://localhost:4173 bun run docs:preview -- --port 4173 & -bun run docs:a11y # pa11y-ci via sitemap — must report 0 errors +bun run docs:a11y # axe-core via every sitemap URL — must report 0 violations +bun run docs:test:a11y # passing/failing fixtures and theme regression tests ``` -Config: `.pa11yci.json`. F77 (Mermaid duplicate SVG IDs) is ignored — not blocking for AT users. +Runner: `scripts/a11y.ts`; pure helpers/tests: `scripts/a11y-utils.ts` and its +companion test; browser regression tests: `scripts/a11y.pw.ts`. The official +`@axe-core/playwright` adapter reuses Playwright. One browser, at most two +parallel pages, no fixed sleep. Wait for hydrated VitePress, completed Mermaid +rendering and fonts before scanning. All sitemap URLs must stay within the +preview origin/base; empty, malformed or unreachable sitemaps must fail. + +Install local Chromium with `bun run playwright install chromium`, or set +`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to an installed Chrome. CI uses system +Chrome, with no browser download. `PLAYWRIGHT_BASE_URL` overrides the preview +origin and `VITEPRESS_BASE` overrides the base path for both build and audit. + +Reports: `a11y-report.json` (full violations, incomplete rules and errors) and +`a11y-report.md` (per-page summary). Violations and operational errors fail; +incomplete results require manual review. There are no blanket rule exclusions. +Do not translate obsolete HTML_CodeSniffer F77/contrast suppressions into axe +disables. Check duplicate ARIA references and Mermaid semantics explicitly. + +DocSearch derives its accessible name from visible content, including the +shortcut. Its mobile label is visually hidden, not `display:none`. Mermaid +wrappers with `aria-label` receive `role="group"` via a scoped observer so +asynchronously rendered diagrams and client-side navigation stay valid. + +See `docs/reference/accessibility.md` for the sourced alternatives benchmark, +measurement methodology, engine-coverage differences and manual-review checklist. ### Common patterns @@ -156,7 +182,8 @@ Before opening a PR for any docs change: bun run docs:build # must complete without errors or dead-link warnings bun run docs:build:a11y # build for a11y audit bun run docs:preview -- --port 4173 & -bun run docs:a11y # 0 pa11y violations +bun run docs:a11y # 0 axe-core automated violations +bun run docs:test:a11y # audit harness and accessible theme semantics bun run docs:test:responsive # 20/20 Playwright tests green bun run format:check # bun fmt --check — no formatting diff ``` diff --git a/.github/skills/feature.md b/.github/skills/feature.md index 824d00d..c65c87f 100644 --- a/.github/skills/feature.md +++ b/.github/skills/feature.md @@ -133,6 +133,7 @@ If the feature touches `docs/`: bun run docs:build bun run docs:build:a11y bun run docs:preview -- --port 4173 & -bun run docs:a11y # 0 pa11y violations +bun run docs:a11y # 0 axe-core WCAG 2.2 AA automated violations +bun run docs:test:a11y # audit harness and theme semantics bun run docs:test:responsive # 20/20 Playwright tests green ``` diff --git a/.github/skills/github-actions.md b/.github/skills/github-actions.md index 0a083c6..2f5002d 100644 --- a/.github/skills/github-actions.md +++ b/.github/skills/github-actions.md @@ -16,7 +16,7 @@ repository. Read this before writing or modifying any `.github/workflows/*.yaml` ci.yaml ← unit tests, native format/type checks, knip + coverage PR comment cd.yaml ← binary build + GitHub Release on tag push docs.yaml ← VitePress Pages deploy + versioned snapshot - a11y.yaml ← WCAG 2.1 AA audit via pa11y-ci + a11y.yaml ← WCAG 2.2 AA automated audit via axe-core + Playwright lighthouse.yaml ← Lighthouse CI eco-design budgets + PR comment responsive.yaml ← Playwright mobile viewport checks ``` @@ -155,7 +155,7 @@ should post a **single sticky comment** per PR, updated on every push: if: github.event_name == 'pull_request' uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: - header: # e.g. "lighthouse-ci", "coverage", "pa11y" + header: # e.g. "lighthouse-ci", "coverage", "accessibility-audit" message: ${{ steps.report.outputs.result }} ``` @@ -223,6 +223,28 @@ the Pages site in a broken state; use a more specific group key instead. ## Chrome / Puppeteer on Ubuntu runners +The accessibility workflow uses `@axe-core/playwright` with the already installed +Playwright stack and system Chrome: + +```yaml +env: + PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: /usr/bin/google-chrome-stable +``` + +Do not install/download another browser in this job. Run `docs:test:a11y` after +the preview starts, then `docs:a11y`. The audit scans the entire sitemap with two +workers and explicit WCAG 2.0/2.1/2.2 A/AA tags. Its path filters must include +`scripts/a11y*.ts`, `playwright.config.ts`, `package.json`, `bun.lock` and the shared +preview action as well as docs and the workflow itself. + +Always upload `a11y-report.json` and `a11y-report.md`, publish the Markdown to +`GITHUB_STEP_SUMMARY`, and post the `accessibility-audit` sticky comment only +for same-repository PRs (fork tokens cannot write comments). Preserve a failing +job after reporting violations or operational errors; do not mask scan failures +with `continue-on-error` alone. Incomplete results are manual-review findings, +not proof of compliance. + +Lighthouse still uses Puppeteer-core; do not remove its transitive dependencies. Ubuntu `ubuntu-latest` ships `google-chrome-stable`. Skip the ~200 MB Puppeteer Chromium download and use the system binary: diff --git a/.github/skills/release.md b/.github/skills/release.md index 15e0329..7c3ae9f 100644 --- a/.github/skills/release.md +++ b/.github/skills/release.md @@ -169,6 +169,7 @@ bun run build.ts # binary compiles for current platform bun run docs:build bun run docs:build:a11y bun run docs:preview -- --port 4173 & -bun run docs:a11y # 0 pa11y violations +bun run docs:a11y # 0 axe-core WCAG 2.2 AA automated violations +bun run docs:test:a11y # audit harness and theme semantics bun run docs:test:responsive # 20/20 Playwright tests green ``` diff --git a/.github/workflows/a11y.yaml b/.github/workflows/a11y.yaml index 89b4430..0d2be28 100644 --- a/.github/workflows/a11y.yaml +++ b/.github/workflows/a11y.yaml @@ -1,24 +1,22 @@ -# Accessibility audit — WCAG 2.1 AA +# Accessibility audit — axe-core / WCAG 2.2 AA # -# Runs pa11y-ci against the built VitePress docs to ensure the homepage and -# key pages remain WCAG 2.1 AA compliant. +# Audits every built VitePress sitemap page with axe-core's WCAG A/AA rules. +# Automated findings complement, but never replace, manual accessibility review. # # Triggers: -# - push to main touching docs/** or this workflow / config # - every pull_request touching the same paths # - workflow_dispatch (manual run) # # Strategy: # 1. Build the docs with `bun run docs:build:a11y` (sets VITEPRESS_HOSTNAME= # http://localhost:4173 so the generated sitemap.xml contains localhost -# URLs that pa11y-ci can reach directly) +# URLs that the audit can reach directly) # 2. Start `vitepress preview` via the shared composite action -# 3. Run pa11y-ci configured in .pa11yci.json (WCAG 2.1 AA, errors only) +# 3. Exercise the audit harness, then scan the full sitemap with two workers +# 4. Publish JSON/Markdown artifacts and a sticky same-repository PR summary # -# Chrome: Ubuntu-latest ships google-chrome-stable. We skip Puppeteer's own -# Chromium download (PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1) and point directly to -# the system Chrome via PUPPETEER_EXECUTABLE_PATH at runtime (~200 MB saved). -name: Accessibility audit (WCAG 2.1 AA) +# Chrome: Playwright reuses Ubuntu's system Chrome; no browser download. +name: Accessibility audit (WCAG 2.2 AA) on: pull_request: @@ -28,7 +26,11 @@ on: - "package.json" - "bun.lock" - ".github/workflows/a11y.yaml" - - ".pa11yci.json" + - ".github/actions/start-preview-server/**" + - "scripts/a11y*.ts" + - "playwright.config.ts" + - "package.json" + - "bun.lock" workflow_dispatch: # One audit at a time per branch; cancel stale runs on new push. @@ -41,8 +43,11 @@ permissions: jobs: audit: - name: WCAG 2.1 AA pages audit + name: WCAG 2.2 AA pages audit runs-on: ubuntu-latest + timeout-minutes: 10 + env: + PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: /usr/bin/google-chrome-stable permissions: contents: read pull-requests: write @@ -66,20 +71,37 @@ jobs: - name: Start VitePress preview server uses: ./.github/actions/start-preview-server - # Run the audit. The env vars tell Puppeteer (used by pa11y) to use the - # pre-installed system Chrome instead of downloading a Chromium binary. - - name: Run accessibility audit (pa11y-ci) - env: - PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome-stable - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "1" + - name: Test accessibility audit harness and theme semantics + run: bun run docs:test:a11y --reporter=dot + + - name: Run accessibility audit (axe-core) + id: audit + continue-on-error: true run: bun run docs:a11y - # Upload the pa11y-ci JSON report as an artifact so failures are - # easy to inspect without re-running the workflow. + - name: Publish job summary + if: ${{ !cancelled() && steps.audit.outcome != 'skipped' }} + run: cat a11y-report.md >> "$GITHUB_STEP_SUMMARY" + + - name: Post / update accessibility report on PR + if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.audit.outcome != 'skipped' }} + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 + with: + header: accessibility-audit + path: a11y-report.md + + - name: Fail if accessibility audit failed + if: ${{ !cancelled() && steps.audit.outcome == 'failure' }} + run: | + echo "::error::Accessibility audit failed — see the report for violations or operational errors" + exit 1 + - name: Upload audit report if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: pa11y-ci-report - path: a11y-report.json + name: a11y-report + path: | + a11y-report.json + a11y-report.md if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index d173354..9547e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,9 @@ coverage/ .env *.local -# pa11y accessibility testing results +# Accessibility audit reports a11y-report.json +a11y-report.md # Lighthouse CI .lighthouseci/ diff --git a/.pa11yci.json b/.pa11yci.json deleted file mode 100644 index 2c96b54..0000000 --- a/.pa11yci.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "defaults": { - "standard": "WCAG2AA", - "reporters": ["cli", ["json", { "fileName": "./a11y-report.json" }]], - "level": "error", - "wait": 1500, - "timeout": 60000, - "chromeLaunchConfig": { - "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] - }, - "ignore": [ - "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.BgImage", - "WCAG2AA.Principle1.Guideline1_4.1_4_3.G145.BgImage", - "WCAG2AA.Principle4.Guideline4_1.4_1_1.F77" - ] - } -} diff --git a/AGENTS.md b/AGENTS.md index cf607a2..d308685 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,6 +71,30 @@ installation/contributor documentation consistent. Compiled binaries do not need No repository custom agent definitions are currently shipped; any future agents must use the same native tooling and validation commands. +## Documentation accessibility + +The docs audit uses `@axe-core/playwright` and the existing Playwright dependency. +Run `bun run docs:build:a11y`, start `bun run docs:preview -- --port 4173` in another +terminal, then run `bun run docs:a11y` and `bun run docs:test:a11y`. +Install local Chromium with `bun run playwright install chromium`, or point +`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at an installed Chrome executable. +CI uses system Chrome without downloading another browser. + +`scripts/a11y.ts` scans every sitemap URL with two workers, explicit WCAG +2.0/2.1/2.2 A/AA tags, and readiness checks for Vue hydration, Mermaid and fonts. +Pure URL validation and Markdown formatting live in `scripts/a11y-utils.ts`. +The generated `a11y-report.json` includes full axe results and operational +errors; `a11y-report.md` is used for the job summary and sticky PR comment. +Violations and operational failures must fail CI. Incomplete results require +manual review, not an automatic compliance claim. Never disable contrast/ARIA +rules or carry over HTML_CodeSniffer exclusions as a shortcut. + +Read `.github/skills/documentation.md` for accessible theme patterns and +`.github/skills/github-actions.md` before changing the workflow. The comparison, +measurement caveats and manual-review checklist are in +`docs/reference/accessibility.md`. Repository-local custom agents are not +currently defined; these shared instructions are the agent entry point. + ## Project layout ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a4a67c..9a70db2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,10 +128,25 @@ If your PR touches docs (`docs/**`), also verify: bun run docs:build # no dead links, no build errors bun run docs:build:a11y bun run docs:preview -- --port 4173 & -bun run docs:a11y # 0 pa11y WCAG 2.1 AA violations +bun run docs:a11y # 0 axe-core WCAG 2.2 AA automated violations +bun run docs:test:a11y # audit failure-path fixtures and theme semantics bun run docs:test:responsive # 20/20 Playwright responsive tests green ``` +Install the local test browser once with `bun run playwright install chromium`, +or set `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to an existing Chrome executable. +The accessibility workflow uses `/usr/bin/google-chrome-stable` on Ubuntu, with +no browser download. `PLAYWRIGHT_BASE_URL` overrides the preview origin; +`VITEPRESS_BASE` selects the documentation base path, including versioned builds. +The build and audit must use the same base path. + +Inspect `a11y-report.json` for affected nodes, rule help links, scan errors and +incomplete findings; `a11y-report.md` summarizes every page. Any violation or +operational error fails the command. Do not copy obsolete HTML_CodeSniffer +exceptions into axe or disable rules to make CI pass. Manual keyboard, +screen-reader and interaction checks remain necessary. See the +[audit guide](docs/reference/accessibility.md) for coverage and benchmark details. + ## AI agent tooling This project ships a two-layer system to guide AI coding agents (GitHub Copilot, Claude, etc.): @@ -154,13 +169,14 @@ Each file covers **one task type** — ordered numbered steps, PR conventions, v Deep domain-knowledge files — **read these for reference**, not for step-by-step guidance. Skills provide patterns, checklists, architectural details and examples that are too large for instruction files. -| File | Domain | -| ------------------ | ---------------------------------------------------------------------- | -| `bug-fixing.md` | Extended symptom→module table, test-first patterns, minimal fix rules | -| `feature.md` | Layer map, type-first design, CLI options, render sub-module extension | -| `refactoring.md` | Architectural invariants, safe rename playbook, knip output guide | -| `release.md` | Semver guide, CD pipeline mechanics, blog post format, CHANGELOG rules | -| `documentation.md` | VitePress theme API, CSS variables, WCAG patterns, responsive patterns | +| File | Domain | +| ------------------- | ---------------------------------------------------------------------- | +| `bug-fixing.md` | Extended symptom→module table, test-first patterns, minimal fix rules | +| `feature.md` | Layer map, type-first design, CLI options, render sub-module extension | +| `refactoring.md` | Architectural invariants, safe rename playbook, knip output guide | +| `release.md` | Semver guide, CD pipeline mechanics, blog post format, CHANGELOG rules | +| `documentation.md` | VitePress theme API, CSS variables, WCAG patterns, responsive patterns | +| `github-actions.md` | CI workflows, pinned actions, browser reuse and accessibility reports | **How they fit together:** instruction files say _what steps to follow_; skill files say _what to know deeply_. Instructions reference their companion skill via a `> Skill reference:` note at the top. diff --git a/README.md b/README.md index 18b9ee8..65753e4 100644 --- a/README.md +++ b/README.md @@ -131,3 +131,22 @@ The official [`gh` CLI](https://cli.github.com/) does support `gh search code`, | Pagination (up to 1 000 results) | ✓ | ✓ | `github-code-search` is purpose-built for **org-wide code audits and interactive triage** — not just a search wrapper. + +## Contributing and documentation quality + +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks. +Documentation accessibility uses the maintained Deque `@axe-core/playwright` adapter, +reusing Playwright rather than installing a second browser automation stack: + +```bash +bun run docs:build:a11y +bun run docs:preview -- --port 4173 +# In another terminal, with Playwright Chromium installed: +bun run docs:a11y +``` + +The audit checks every sitemap URL with WCAG 2.0/2.1/2.2 A/AA tags and writes JSON +and Markdown reports. CI reuses system Chrome without downloading a browser. +Automated checks do not certify WCAG compliance: incomplete results, keyboard use, +screen-reader behavior and interactive states still need manual review. +See the [audit guide and alternatives benchmark](https://fulll.github.io/github-code-search/reference/accessibility). diff --git a/bun.lock b/bun.lock index ef2f4e9..cf51f8d 100644 --- a/bun.lock +++ b/bun.lock @@ -7,13 +7,13 @@ "commander": "^15.0.0", }, "devDependencies": { + "@axe-core/playwright": "^4.14.0", "@lhci/cli": "^0.15.1", "@playwright/test": "^1.63.0", "@resvg/resvg-js": "^2.6.2", "bun-types": "^1.4.2", "knip": "^6.39.0", "mermaid": "^12.2.0", - "pa11y-ci": "^5.0.0", "sharp": "^0.35.5", "vitepress": "^1.6.4", "vitepress-mermaid-renderer": "^1.2.2", @@ -59,6 +59,8 @@ "@antfu/install-pkg": ["@antfu/install-pkg@2.1.0", "", { "dependencies": { "package-manager-detector": "^1.8.0", "tinyexec": "^1.3.1" } }, "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw=="], + "@axe-core/playwright": ["@axe-core/playwright@4.14.0", "", { "dependencies": { "axe-core": "~4.14.0" }, "peerDependencies": { "playwright-core": ">= 1.0.0" } }, "sha512-SGUhG5PIHd2eMdpgBF2PyQLOhX+qyDR2PT4cmoS4Qk2ak+jX/rrSvst9l0PGv5WTHolKpUMvVmoHpr4cZgf1RQ=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], @@ -297,13 +299,11 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], - "@pa11y/html_codesniffer": ["@pa11y/html_codesniffer@2.6.0", "", {}, "sha512-BKA7qG8NyaIBdCBDep0hYuYoF/bEyWJprE6EEVJOPiwj80sSiIKDT8LUVd19qKhVqNZZD3QvJIdFZ35p+vAFPg=="], - "@paulirish/trace_engine": ["@paulirish/trace_engine@0.0.53", "", { "dependencies": { "legacy-javascript": "latest", "third-party-web": "latest" } }, "sha512-PUl/vlfo08Oj804VI5nDPeSk9vyslnBlVzDDwFt8SUVxY8+KdGMkra/vrXjEEHe8gb7+RqVTfOIlGw0nyrEelA=="], "@playwright/test": ["@playwright/test@1.63.0", "", { "dependencies": { "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ=="], - "@puppeteer/browsers": ["@puppeteer/browsers@3.2.6", "", { "dependencies": { "modern-tar": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-MJP29gVoHznmqmwhH1tlZqzfeQ3aylTpSSa6WKZXtkwYUH02i9imBmWGTyZrbnAou5paX78SE4jMAUqnEPf8ug=="], + "@puppeteer/browsers": ["@puppeteer/browsers@2.13.2", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw=="], "@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="], @@ -555,15 +555,9 @@ "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], - "array-union": ["array-union@1.0.2", "", { "dependencies": { "array-uniq": "^1.0.1" } }, "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng=="], - - "array-uniq": ["array-uniq@1.0.3", "", {}, "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q=="], - "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - - "axe-core": ["axe-core@4.13.0", "", {}, "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A=="], + "axe-core": ["axe-core@4.14.0", "", {}, "sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ=="], "b4a": ["b4a@1.9.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ=="], @@ -581,14 +575,10 @@ "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], - "bfj": ["bfj@9.1.3", "", { "dependencies": { "check-types": "^11.2.3", "hoopy": "^0.1.4", "tryer": "^1.0.1" } }, "sha512-1ythbcNNAd2UjTYW6M+MAHd9KM/m3g4mQ+3a4Vom16WgmUa4GsisdmXAYfpAjkObY5zdpgzaBh1ctZOEcJipuQ=="], - "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], "body-parser": ["body-parser@1.20.8", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.16.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA=="], - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], @@ -613,17 +603,11 @@ "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], - "check-types": ["check-types@11.2.3", "", {}, "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg=="], - - "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], - - "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], - "chevrotain": ["chevrotain@13.2.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "13.2.0", "@chevrotain/gast": "13.2.0", "@chevrotain/regexp-to-ast": "13.2.0", "@chevrotain/types": "13.2.0", "@chevrotain/utils": "13.2.0" } }, "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA=="], "chrome-launcher": ["chrome-launcher@0.13.4", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^1.0.5", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^0.5.3", "rimraf": "^3.0.2" } }, "sha512-nnzXiDbGKjDSK6t2I+35OAPBy5Pw/39bgkb/ZAFwMhwJbdYBp6aH+vW28ZgtjdU890Q7D+3wN/tB8N66q5Gi2A=="], - "chromium-bidi": ["chromium-bidi@157.0.8090-0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^4.6.5" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-KJw7IQgqXwxpJgJlEIPg5mr2ZoqA+jkTRtc5EABI7sUJ3bu3cpI7NuEBoj4Gn1AsF78CaT9So5WQWspK3/wYag=="], + "chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="], "cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], @@ -663,10 +647,6 @@ "csp_evaluator": ["csp_evaluator@1.1.5", "", {}, "sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw=="], - "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - - "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "cytoscape": ["cytoscape@3.34.3", "", {}, "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ=="], @@ -769,18 +749,8 @@ "devtools-protocol": ["devtools-protocol@0.0.1467305", "", {}, "sha512-LxwMLqBoPPGpMdRL4NkLFRNy3QLp6Uqa7GNp1v6JaBheop2QrB9Q7q0A/q/CYYP9sBfZdHOyszVx4gc9zyk7ow=="], - "dir-glob": ["dir-glob@2.2.2", "", { "dependencies": { "path-type": "^3.0.0" } }, "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw=="], - - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - - "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "dompurify": ["dompurify@3.4.15", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw=="], - "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -795,15 +765,11 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "envinfo": ["envinfo@7.21.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -851,8 +817,6 @@ "figures": ["figures@2.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA=="], - "file-url": ["file-url@3.0.0", "", {}, "sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA=="], - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -873,8 +837,6 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -887,8 +849,6 @@ "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "globby": ["globby@7.1.1", "", { "dependencies": { "array-union": "^1.0.1", "dir-glob": "^2.0.0", "glob": "^7.1.2", "ignore": "^3.3.5", "pify": "^3.0.0", "slash": "^1.0.0" } }, "sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -907,12 +867,8 @@ "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], - "hoopy": ["hoopy@0.1.4", "", {}, "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ=="], - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-link-header": ["http-link-header@1.1.4", "", {}, "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw=="], @@ -923,8 +879,6 @@ "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "ignore": ["ignore@3.3.10", "", {}, "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug=="], - "image-ssim": ["image-ssim@0.2.0", "", {}, "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg=="], "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], @@ -947,8 +901,6 @@ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - "is": ["is@3.3.2", "", {}, "sha512-a2xr4E3s1PjDS8ORcGgXpWx6V+liNs+O3JRD2mb9aeugD7rtkkZ0zgLdYgw0tWsKhsdiezGYptSiMlVazCBTuQ=="], - "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], @@ -987,8 +939,6 @@ "lighthouse-stack-packs": ["lighthouse-stack-packs@1.12.2", "", {}, "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw=="], - "lilconfig": ["lilconfig@3.1.4", "", {}, "sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w=="], - "localforage": ["localforage@1.10.0", "", { "dependencies": { "lie": "3.1.1" } }, "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg=="], "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -1053,12 +1003,8 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "modern-tar": ["modern-tar@0.8.6", "", {}, "sha512-LYSks0tNVqbtez/9OB0YFP6qLRSqkdAxJWOQpVaR67lNTbj2zNLyYpowkRMia5lXLCZKzxwoxkxH2MbbBR2aEg=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], - "mute-stream": ["mute-stream@0.0.7", "", {}, "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ=="], "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], @@ -1069,10 +1015,6 @@ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "node.extend": ["node.extend@2.0.3", "", { "dependencies": { "hasown": "^2.0.0", "is": "^3.3.0" } }, "sha512-xwADg/okH48PvBmRZyoX8i8GJaKuJ1CqlqotlZOhUio8egD1P5trJupHKBzcPjSF9ifK2gPcEICRBnkfPqQXZw=="], - - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -1099,10 +1041,6 @@ "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - "pa11y": ["pa11y@10.0.0", "", { "dependencies": { "@pa11y/html_codesniffer": "^2.6.0", "axe-core": "~4.13.0", "bfj": "~9.1.3", "commander": "~15.0.0", "envinfo": "~7.21.0", "mustache": "~4.2.0", "node.extend": "~2.0.3", "puppeteer": "^25.9.0", "semver": "~7.8.0" }, "bin": { "pa11y": "bin/pa11y.js" } }, "sha512-bgNMoSH/a57sLhlllkKstcI6KRWsHVhBhZyWRbM9Z4uV9+nFsnra213QdO+KPVKD9mRtzQj48510CHLG4BBDZg=="], - - "pa11y-ci": ["pa11y-ci@5.0.0", "", { "dependencies": { "async": "~3.2.6", "cheerio": "~1.2.0", "commander": "~14.0.3", "globby": "~7.1.1", "lodash": "~4.18.1", "node-fetch": "~2.7.0", "pa11y": "^10.0.0", "protocolify": "~3.0.0", "puppeteer": "^25.9.0", "wordwrap": "~1.0.0" }, "bin": { "pa11y-ci": "bin/pa11y-ci.js" } }, "sha512-4izxemb7KKxXISqE92YNULCTxAYN+1LDJXhCTJgkTpOvO94uh6lRNHa0bUpgfiW3nUfaGkqdO536n4u4fjLFNQ=="], - "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], @@ -1111,12 +1049,6 @@ "parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], - - "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], @@ -1127,8 +1059,6 @@ "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], - "path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="], - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], @@ -1137,8 +1067,6 @@ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], - "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], "playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="], @@ -1151,14 +1079,10 @@ "preact": ["preact@10.29.8", "", { "peerDependencies": { "preact-render-to-string": ">=5" }, "optionalPeers": ["preact-render-to-string"] }, "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q=="], - "prepend-http": ["prepend-http@3.0.1", "", {}, "sha512-BLxfZh+m6UiAiCPZFJ4+vYoL7NrRs5XgCTRrjseATAggXhdZKKxn+JUNmuVYWY23bDHgaEHodxw8mnmtVEDtHw=="], - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], - "protocolify": ["protocolify@3.0.0", "", { "dependencies": { "file-url": "^3.0.0", "prepend-http": "^3.0.0" } }, "sha512-PuvDJOkKJMVQx8jSNf8E5g0bJw/UTKm30mTjFHg4N30c8sefgA5Qr/f8INKqYBKfvP/MUSJrj+z1Smjbq4/3rQ=="], - "proxy-addr": ["proxy-addr@2.0.8", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ=="], "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], @@ -1167,8 +1091,6 @@ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "puppeteer": ["puppeteer@25.16.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.6", "chromium-bidi": "157.0.8090-0", "devtools-protocol": "0.0.1704951", "lilconfig": "^3.1.3", "puppeteer-core": "25.16.0", "typed-query-selector": "^2.12.3" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" } }, "sha512-LOr3reYnxAYYfQkpopz1fQnL0LdvqhpR5U2Fu+QnwzTX2gpVTHsn91UETo5NNC1md2/oC54Zu1ChBXgZDNSP1A=="], - "puppeteer-core": ["puppeteer-core@24.43.1", "", { "dependencies": { "@puppeteer/browsers": "2.13.2", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1608973", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.20.0" } }, "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw=="], "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], @@ -1239,8 +1161,6 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "slash": ["slash@1.0.0", "", {}, "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg=="], - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="], @@ -1311,22 +1231,18 @@ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "tryer": ["tryer@1.0.1", "", {}, "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA=="], - "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - "typed-query-selector": ["typed-query-selector@2.12.3", "", {}, "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg=="], + "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], "unbash": ["unbash@4.0.11", "", {}, "sha512-FoSOKV7NEofQSkAefMVHam4ZPKYMxjAydxiV72UFEDNV/YofxjGfiZ2A9pZjdL/lRJzTjcu4PABo1JYJX8N5iQ=="], - "undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], @@ -1367,18 +1283,12 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -1423,12 +1333,10 @@ "@paulirish/trace_engine/third-party-web": ["third-party-web@0.30.0", "", {}, "sha512-p+PfyL5U0ediGzvwPzMSdD9MnC8rHW6nWJJVLIQvqN7RsSqfaZ+7n4c9GILqH36EYR0eXB9CzAeCRnKFr/9uJg=="], - "@puppeteer/browsers/yargs": ["yargs@18.2.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w=="], + "@puppeteer/browsers/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1437,6 +1345,8 @@ "chrome-launcher/lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], + "chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1453,8 +1363,6 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -1463,10 +1371,10 @@ "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "intl-messageformat/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "lighthouse/axe-core": ["axe-core@4.13.0", "", {}, "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A=="], + "lighthouse/chrome-launcher": ["chrome-launcher@1.2.1", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A=="], "lighthouse/lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="], @@ -1487,22 +1395,8 @@ "mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "pa11y-ci/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "puppeteer/devtools-protocol": ["devtools-protocol@0.0.1704951", "", {}, "sha512-GQhQOs9u/Mvm1zMtJIH872JS4AskuZdUSsPNPk4ChahtWAUqgz6atEtIsMt+nO7MqF1Ic4HxII/k8Cu36JivNg=="], - - "puppeteer/puppeteer-core": ["puppeteer-core@25.16.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.6", "chromium-bidi": "157.0.8090-0", "devtools-protocol": "0.0.1704951", "typed-query-selector": "^2.12.3", "webdriver-bidi-protocol": "1.0.1", "ws": "^8.22.0" } }, "sha512-VA5Pf54N8shfopNkZZXCdiiwfnnY8ZJYAMqFQRhlH5mGUhfwf53Ar770GPtdpTDLUqvQeYpIBtC9/G2Jij2IJw=="], - - "puppeteer-core/@puppeteer/browsers": ["@puppeteer/browsers@2.13.2", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw=="], - - "puppeteer-core/chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="], - "puppeteer-core/devtools-protocol": ["devtools-protocol@0.0.1608973", "", {}, "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ=="], - "puppeteer-core/typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], - "puppeteer-core/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -1511,8 +1405,6 @@ "tmp/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], - "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1525,13 +1417,13 @@ "@img/sharp-wasm32/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@puppeteer/browsers/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + "@puppeteer/browsers/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "@puppeteer/browsers/yargs/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + "@puppeteer/browsers/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@puppeteer/browsers/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + "@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -1565,14 +1457,6 @@ "lighthouse/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "puppeteer-core/@puppeteer/browsers/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], - - "puppeteer-core/chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "puppeteer/puppeteer-core/webdriver-bidi-protocol": ["webdriver-bidi-protocol@1.0.1", "", {}, "sha512-WXWs/aXF2a6Al1Z2/V/7Gp2oPZOAAmHjJOTmv4KCXRJ122IMj3tSSMc6a4q3K4QOmhQbse/Ahr+oTVJOU2dUiw=="], - - "puppeteer/puppeteer-core/ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "string-width/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], @@ -1587,13 +1471,13 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@puppeteer/browsers/yargs/cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "@puppeteer/browsers/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "chrome-launcher/lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -1605,25 +1489,15 @@ "lighthouse/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "puppeteer-core/@puppeteer/browsers/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "puppeteer-core/@puppeteer/browsers/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "puppeteer-core/@puppeteer/browsers/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "puppeteer-core/@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@puppeteer/browsers/yargs/cliui/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "@puppeteer/browsers/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="], + "@puppeteer/browsers/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "@puppeteer/browsers/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="], + "@puppeteer/browsers/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "lighthouse/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1631,26 +1505,12 @@ "lighthouse/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "puppeteer-core/@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "puppeteer-core/@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "puppeteer-core/@puppeteer/browsers/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "puppeteer-core/@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "lighthouse/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "puppeteer-core/@puppeteer/browsers/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "puppeteer-core/@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "puppeteer-core/@puppeteer/browsers/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "lighthouse/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "puppeteer-core/@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "puppeteer-core/@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 6d34789..1d0d91a 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -359,6 +359,7 @@ export default defineConfig({ link: "/reference/github-api-limits", }, { text: "Environment variables", link: "/reference/environment" }, + { text: "Accessibility audits", link: "/reference/accessibility" }, ], }, ], @@ -417,9 +418,10 @@ export default defineConfig({ // ── Sitemap ─────────────────────────────────────────────────────────────── // VITEPRESS_HOSTNAME overrides the default for local/CI a11y audits: // VITEPRESS_HOSTNAME=http://localhost:4173 vitepress build docs - // → sitemap.xml contains localhost URLs that pa11y-ci can reach directly. + // → sitemap.xml contains localhost URLs that the accessibility audit can reach. sitemap: { hostname: - (process.env.VITEPRESS_HOSTNAME ?? "https://fulll.github.io") + "/github-code-search/", + (process.env.VITEPRESS_HOSTNAME ?? "https://fulll.github.io") + + (process.env.VITEPRESS_BASE ?? "/github-code-search/"), }, }); diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index bae204d..f1bb47a 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -485,3 +485,17 @@ html.dark .vp-doc table code { background-color: #2d333b; color: #c9d1d9; } +/* Keep the icon-only mobile search button named without overriding its desktop label. */ +@media (max-width: 767px) { + .DocSearch-Button .DocSearch-Button-Placeholder { + display: block; + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + } +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 5607698..91610eb 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,4 +1,4 @@ -import { h, nextTick, onMounted, watch } from "vue"; +import { h, nextTick, onMounted, onUnmounted, watch } from "vue"; import type { App } from "vue"; import type { Theme } from "vitepress"; import DefaultTheme from "vitepress/theme"; @@ -16,6 +16,13 @@ import VersionBadge from "./VersionBadge.vue"; import TestimonialsSection from "./TestimonialsSection.vue"; import "./custom.css"; +// aria-label is only permitted on elements with a role; Mermaid containers lack one. +function labelDiagrams() { + for (const diagram of document.querySelectorAll(".mermaid[aria-label]:not([role])")) { + diagram.setAttribute("role", "group"); + } +} + export default { extends: DefaultTheme, enhanceApp({ app }: { app: App }) { @@ -28,15 +35,19 @@ export default { app.component("TestimonialsSection", TestimonialsSection); }, setup() { - // Fix: DocSearch button has aria-label="Search" but its visible text includes - // the keyboard-shortcut keys ("⌘ K"), which triggers a label-content-name- - // mismatch accessibility failure. Marking the keys container aria-hidden hides - // it from assistive technology so the button's accessible name ("Search") and - // its visible label agree. + let diagramObserver: MutationObserver | undefined; onMounted(() => { - const keys = document.querySelector(".DocSearch-Button-Keys"); - if (keys) keys.setAttribute("aria-hidden", "true"); + // Derive the name from visible content, including the shortcut. The label + // stays screen-reader-visible on mobile via custom.css. + document.querySelector(".DocSearch")?.removeAttribute("aria-label"); + labelDiagrams(); + const content = document.getElementById("VPContent"); + if (content) { + diagramObserver = new MutationObserver(labelDiagrams); + diagramObserver.observe(content, { childList: true, subtree: true }); + } }); + onUnmounted(() => diagramObserver?.disconnect()); }, Layout: () => { const { isDark, frontmatter } = useData(); diff --git a/docs/reference/accessibility.md b/docs/reference/accessibility.md new file mode 100644 index 0000000..53a73f4 --- /dev/null +++ b/docs/reference/accessibility.md @@ -0,0 +1,123 @@ +# Documentation accessibility audits + +Run the full-sitemap audit when changing documentation, theme components or the audit harness. + +```bash +bun run docs:build:a11y +bun run docs:preview -- --port 4173 +``` + +In another terminal: + +```bash +# One-time local browser setup; skip when using an existing Chrome executable. +bun run playwright install chromium +bun run docs:a11y +bun run docs:test:a11y +bun run docs:test:responsive +``` + +## Browser and preview configuration + +`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` selects an existing Chrome/Chromium executable. +The accessibility CI job uses `/usr/bin/google-chrome-stable` on Ubuntu; it does not +download another browser. Without this variable, local runs use Playwright's installed +Chromium. The browser is separate from the `node_modules` footprint. + +`PLAYWRIGHT_BASE_URL` defaults to `http://localhost:4173`. `VITEPRESS_BASE` defaults to +`/github-code-search/` and must end with `/`. Set the same base during the build and +audit for versioned documentation. `docs:build:a11y` sets the sitemap hostname to +localhost. For another preview origin, build with `VITEPRESS_HOSTNAME` explicitly: + +```bash +VITEPRESS_HOSTNAME=http://localhost:4174 VITEPRESS_BASE=/github-code-search/v2/ bun run docs:build +``` + +## Coverage and reports + +The official Deque `@axe-core/playwright` adapter runs axe-core in a real browser. +`scripts/a11y.ts` scans every distinct sitemap URL with two workers sharing one +browser. It waits for Vue hydration, Mermaid rendering and fonts, rather than a fixed +sleep. Pages have isolated browser contexts and a 60-second audit deadline. + +Enabled tags are `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa` and `wcag22aa`: WCAG +2.0/2.1/2.2 A/AA rules supported by axe. No contrast or ARIA rules are disabled. +Empty, malformed, out-of-base or unreachable sitemaps, broken pages and scanner +errors fail the audit, as do violations of any impact level. + +| Report | Contents | +| ------------------ | ---------------------------------------------------------------------------------------- | +| `a11y-report.json` | Engine results, affected nodes, remediation links, incomplete rules, errors and duration | +| `a11y-report.md` | Per-page violation/manual-review counts and status | + +CI uploads both reports, publishes a job summary and updates a sticky comment on +same-repository PRs. Fork PRs still receive artifacts and a job summary without +requiring comment-write permissions. + +::: warning Automated checks are not WCAG certification +Review `incomplete` results manually. Also test keyboard navigation, focus order, +screen-reader announcements, zoom/reflow, both color schemes, and interactive +states such as search dialogs, tabs and expanded menus. The default automated audit +checks the initial desktop page state; it does not automatically exercise all +themes, viewports or interactions. See the +[Playwright accessibility guide](https://playwright.dev/docs/accessibility-testing) +and [W3C guidance on evaluating accessibility](https://www.w3.org/WAI/test-evaluate/). +::: + +## Alternatives benchmark + +Evaluated on 2026-10-28. All shortlisted projects showed recent maintenance; +pa11y-ci was not replaced because it was abandoned. + +The sizes below are registry `dist.unpackedSize` values for the named package, +**not** its full dependency tree, browser binary or download size. + +| Alternative | Version | Unpacked package | Integration trade-off | +| ------------------------- | ---------- | ---------------: | --------------------------------------------------------------------------- | +| pa11y-ci | 5.0.0 | 42,330 bytes | Adds Pa11y, HTML_CodeSniffer and a separate Puppeteer stack | +| **@axe-core/playwright** | **4.14.0** | **47,795 bytes** | **Reuses existing Playwright; only runtime dependency is axe-core** | +| Direct axe-core | 4.14.0 | 3,430,531 bytes | No dependencies, but requires maintaining browser/frame injection ourselves | +| @axe-core/cli | 4.14.0 | 71,507 bytes | Also adds Selenium, ChromeDriver and the WebDriver adapter | +| IBM accessibility-checker | 4.0.34 | 6,042,998 bytes | Adds Puppeteer, ChromeDriver, telemetry and spreadsheet reporting | + +The selected adapter also needs axe-core (3,430,531 bytes). The engine is already +used by Lighthouse; the migration moves the shared engine from 4.13 to 4.14. +Only the adapter and that engine version were introduced, with no unrelated +package upgrades. Removing pa11y-ci does **not** remove Lighthouse's Puppeteer-core. + +### Measurements and limitations + +Initial same-machine comparison: macOS arm64, Bun 1.4.4 canary, system Chrome +156.0.8078.48, the same built 36-page sitemap and preview server. Browser downloads, +dependency installation and the documentation build are excluded from audit timing. + +| Metric | Before | After | +| ------------------------------------------------------ | ----------: | ----------: | +| Audit wall time, single exploratory run | 294.76 s | 112.69 s | +| Lockfile package entries, including nested resolutions | 857 | 787 | +| Installed `node_modules`, `du -sk` | 610,532 KiB | 575,500 KiB | + +The timing compares the original configuration (one worker, 1,500 ms fixed wait, +HTML_CodeSniffer) with axe (two workers, readiness checks). It is **not** an +engine-only comparison or a hosted-CI speed guarantee. The first axe run audited +all 36 pages but failed on newly detected search-label and Mermaid-role issues; +these were repaired rather than suppressed. Adding this guide expands the final +sitemap, so subsequent audit durations are not the same 36-page workload. +The installed-tree reduction is about 34.2 MiB (5.7%); lockfile entries are not +equivalent to unique packages. Measurements exclude browser caches. + +The old `WCAG2AA` HTML_CodeSniffer configuration did not explicitly add WCAG 2.1 +rules despite the workflow title. It also ignored background-image contrast +findings and F77 duplicate IDs. Different engines have different rule coverage: +axe's explicit 2.1/2.2 tags do not establish rule-for-rule HTML_CodeSniffer parity. +Static DOM-only checks cannot replace rendered contrast checks, and Lighthouse's +sampled performance audits are complementary rather than a full-sitemap substitute. + +### Maintenance and sources + +- [axe-core 4.14.0](https://github.com/dequelabs/axe-core/releases/tag/v4.14.0): released 2026-10-05; Deque-maintained, MPL-2.0. +- [Official Playwright adapter](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright): 4.14.0 published 2026-10-15; repository activity observed 2026-10-27. +- [pa11y-ci 5.0.0](https://github.com/pa11y/pa11y-ci/releases/tag/5.0.0): released 2026-10-05; LGPL-3.0-only. +- [Pa11y standards and runners](https://github.com/pa11y/pa11y#runners): default HTML_CodeSniffer versus optional axe. +- [IBM Equal Access](https://github.com/IBMa/equal-access): checker 4.0.34 published 2026-09-08; repository activity observed 2026-10-16; Apache-2.0. +- Package metadata and sizes: [pa11y-ci registry](https://registry.npmjs.org/pa11y-ci/5.0.0), [Playwright adapter registry](https://registry.npmjs.org/@axe-core/playwright/4.14.0), [axe-core registry](https://registry.npmjs.org/axe-core/4.14.0), [axe CLI registry](https://registry.npmjs.org/@axe-core/cli/4.14.0), [IBM checker registry](https://registry.npmjs.org/accessibility-checker/4.0.34). diff --git a/package.json b/package.json index 0245b56..b36f3e7 100644 --- a/package.json +++ b/package.json @@ -51,21 +51,22 @@ "docs:build:og": "bun scripts/generate-og.ts", "generate-ico": "bun scripts/generate-ico.ts", "docs:preview": "vitepress preview docs", - "docs:a11y": "pa11y-ci --config .pa11yci.json --sitemap http://localhost:4173${VITEPRESS_BASE:-/github-code-search/}sitemap.xml", + "docs:a11y": "bun scripts/a11y.ts", "docs:lhci": "lhci autorun --config .lhci.config.cjs", - "docs:test:responsive": "playwright test" + "docs:test:responsive": "playwright test responsive.pw.ts", + "docs:test:a11y": "playwright test a11y.pw.ts" }, "dependencies": { "commander": "^15.0.0" }, "devDependencies": { + "@axe-core/playwright": "^4.14.0", "@lhci/cli": "^0.15.1", "@playwright/test": "^1.63.0", "@resvg/resvg-js": "^2.6.2", "bun-types": "^1.4.2", "knip": "^6.39.0", "mermaid": "^12.2.0", - "pa11y-ci": "^5.0.0", "sharp": "^0.35.5", "vitepress": "^1.6.4", "vitepress-mermaid-renderer": "^1.2.2" diff --git a/playwright.config.ts b/playwright.config.ts index d594412..da09d07 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,5 @@ /** - * Playwright configuration — responsive tests only. + * Playwright configuration — responsive and accessibility harness tests. * * Run: bun run docs:test:responsive * @@ -13,7 +13,7 @@ const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:4173"; export default defineConfig({ testDir: "./scripts", - testMatch: ["responsive.pw.ts"], + testMatch: ["responsive.pw.ts", "a11y.pw.ts"], /* Run all (viewport × page) pairs concurrently — they are fully independent */ fullyParallel: true, @@ -31,6 +31,9 @@ export default defineConfig({ baseURL: BASE_URL, /* Disable JS animations — they don't affect layout but speed up page load */ reducedMotion: "reduce", + launchOptions: { + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, + }, }, /* Up to 4 workers locally; CI defaults to 2 (GitHub-hosted runners are 2-core) */ diff --git a/scripts/a11y-utils.test.ts b/scripts/a11y-utils.test.ts new file mode 100644 index 0000000..d6856e4 --- /dev/null +++ b/scripts/a11y-utils.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "bun:test"; +import { formatSummary, validateSitemapUrls, WCAG_TAGS } from "./a11y-utils.ts"; + +describe("accessibility sitemap scope", () => { + const base = "http://localhost:4173/github-code-search/"; + + it("keeps all distinct pages, including versioned paths and encoded URLs", () => { + const urls = [base, `${base}reference/cli.html`, `${base}v1/a%20b.html?x=1&y=2`]; + expect(validateSitemapUrls([...urls, base], base)).toEqual(urls); + }); + + it("supports a versioned documentation base", () => { + expect(validateSitemapUrls([`${base}v2/page.html`], `${base}v2/`)).toEqual([ + `${base}v2/page.html`, + ]); + }); + + it("fails closed on an empty sitemap", () => { + expect(() => validateSitemapUrls([], base)).toThrow("no page URLs"); + }); + + it.each([ + "https://example.com/github-code-search/page.html", + "http://localhost:4173/github-code-search-other/page.html", + "http://localhost:4173/github-code-search/../page.html", + "http://user:password@localhost:4173/github-code-search/", + "file:///github-code-search/page.html", + "not a URL", + ])("rejects out-of-scope or invalid sitemap URL %s", (url) => { + expect(() => validateSitemapUrls([url], base)).toThrow(); + }); + + it.each(["file:///docs/", "http://localhost:4173/docs"])("rejects invalid base %s", (url) => { + expect(() => validateSitemapUrls([url], url)).toThrow(); + }); + + it("runs WCAG A/AA tags through 2.2 without disabling contrast or ARIA", () => { + expect(WCAG_TAGS).toEqual(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]); + }); +}); + +describe("accessibility summary", () => { + it("distinguishes violations, manual review and operational errors", () => { + const report = formatSummary( + [ + { url: "http://localhost/pass", violations: 0, incomplete: 1, error: null }, + { url: "http://localhost/fail", violations: 2, incomplete: 0, error: null }, + { url: "http://localhost/error", violations: 0, incomplete: 0, error: "HTTP 404" }, + ], + null, + ); + expect(report).toContain("| `/pass` | 0 | 1 | PASS |"); + expect(report).toContain("| `/fail` | 2 | 0 | FAIL |"); + expect(report).toContain("| `/error` | 0 | 0 | ERROR |"); + expect(report).toContain("not WCAG certification"); + expect(report).toContain("a11y-report.json"); + }); + + it("reports startup failures without presenting an empty audit as success", () => { + expect(formatSummary([], "Malformed sitemap")).toContain("**Audit failed:"); + }); + + it("escapes table delimiters in URL paths", () => { + expect( + formatSummary( + [{ url: "http://localhost/a|b", violations: 0, incomplete: 0, error: null }], + null, + ), + ).toContain("`/a%7Cb`"); + }); +}); diff --git a/scripts/a11y-utils.ts b/scripts/a11y-utils.ts new file mode 100644 index 0000000..afab0d3 --- /dev/null +++ b/scripts/a11y-utils.ts @@ -0,0 +1,49 @@ +export const WCAG_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]; + +export function validateSitemapUrls(locations: string[], base: string): string[] { + const root = new URL(base); + if (!["http:", "https:"].includes(root.protocol) || !root.pathname.endsWith("/")) { + throw new Error(`Expected an HTTP(S) base URL ending in /, got ${base}`); + } + if (locations.length === 0) throw new Error("The sitemap contains no page URLs"); + + return [ + ...new Set( + locations.map((location) => { + const url = new URL(location); + if ( + url.origin !== root.origin || + !url.pathname.startsWith(root.pathname) || + url.username || + url.password + ) { + throw new Error(`Sitemap URL is outside the preview base: ${location}`); + } + return url.href; + }), + ), + ]; +} + +export function formatSummary( + pages: { url: string; violations: number; incomplete: number; error: string | null }[], + error: string | null, +): string { + const rows = pages.map((page) => { + const pathname = new URL(page.url).pathname.replaceAll("|", "%7C"); + return `| \`${pathname}\` | ${page.violations} | ${page.incomplete} | ${page.error ? "ERROR" : page.violations ? "FAIL" : "PASS"} |`; + }); + return [ + "## Accessibility audit — axe-core / WCAG 2.2 AA", + "", + `Pages scanned: ${pages.length}. ${error ? "**Audit failed: see the JSON report and job logs.**" : ""}`, + "", + "| Page | Violations | Incomplete rules | Status |", + "| --- | ---: | ---: | --- |", + ...rows, + "", + "Incomplete results require manual review; zero violations is not WCAG certification.", + "Full rule details, affected nodes and operational errors are in `a11y-report.json`.", + "", + ].join("\n"); +} diff --git a/scripts/a11y.pw.ts b/scripts/a11y.pw.ts new file mode 100644 index 0000000..ebdf4e7 --- /dev/null +++ b/scripts/a11y.pw.ts @@ -0,0 +1,129 @@ +import { test, expect } from "@playwright/test"; +import { spawn } from "node:child_process"; +import { createServer } from "node:http"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const script = resolve("scripts/a11y.ts"); + +for (const scenario of [ + "pass", + "violation", + "empty", + "malformed", + "missing-loc", + "outside", + "http-error", + "sitemap-error", +]) { + test(`audit harness: ${scenario}`, async () => { + const directory = await mkdtemp(join(tmpdir(), "gcs-a11y-")); + const server = createServer((request, response) => { + if (request.url === "/docs/sitemap.xml") { + response.setHeader("Content-Type", "application/xml"); + if (scenario === "sitemap-error") { + response.writeHead(500).end("Unavailable"); + } else if (scenario === "malformed") { + response.end(""); + } else if (scenario === "empty") { + response.end(""); + } else if (scenario === "missing-loc") { + response.end(""); + } else { + const origin = `http://${request.headers.host}`; + const urls = + scenario === "outside" + ? [`${origin}/outside.html`] + : [`${origin}/docs/page.html`, `${origin}/docs/second.html`]; + response.end( + `${urls.map((url) => `${url}`).join("")}`, + ); + } + } else if (scenario === "http-error") { + response.writeHead(404).end("Not found"); + } else { + response.setHeader("Content-Type", "text/html"); + response.end(`Audit fixture +

Audit fixture

+ ${scenario === "violation" ? '' : "

Accessible content

"} +
`); + } + }); + + try { + await new Promise((ready) => server.listen(0, "127.0.0.1", ready)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Missing fixture server port"); + const logs: string[] = []; + const child = spawn("bun", [script], { + cwd: directory, + env: { + ...process.env, + PLAYWRIGHT_BASE_URL: `http://127.0.0.1:${address.port}`, + VITEPRESS_BASE: "/docs/", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + child.stdout.on("data", (chunk) => logs.push(chunk.toString())); + child.stderr.on("data", (chunk) => logs.push(chunk.toString())); + const code = await new Promise((done, reject) => { + child.once("error", reject); + child.once("close", done); + }); + expect(code, logs.join("")).toBe(scenario === "pass" ? 0 : 1); + const report = JSON.parse(await readFile(join(directory, "a11y-report.json"), "utf8")); + const summary = await readFile(join(directory, "a11y-report.md"), "utf8"); + expect(summary).toContain("not WCAG certification"); + if (scenario === "pass" || scenario === "violation" || scenario === "http-error") { + expect(report.pages).toHaveLength(2); + expect(report.error).toBeNull(); + if (scenario === "violation") { + expect( + report.pages[0].results.violations.map((rule: { id: string }) => rule.id), + ).toContain("image-alt"); + expect(summary).toContain("FAIL"); + } else if (scenario === "http-error") { + expect(report.pages[0].error).toContain("HTTP 404"); + expect(summary).toContain("ERROR"); + } else { + expect( + report.pages.every( + (page: { results: { violations: unknown[] } }) => + page.results.violations.length === 0, + ), + ).toBe(true); + expect(summary).toContain("PASS"); + } + } else { + expect(report.pages).toHaveLength(0); + expect(report.error).toBeTruthy(); + expect(summary).toContain("**Audit failed:"); + } + } finally { + await new Promise((done, reject) => + server.close((error) => (error ? reject(error) : done())), + ); + await rm(directory, { recursive: true, force: true }); + } + }); +} + +test("search remains named at desktop and mobile widths", async ({ page }) => { + const base = process.env.VITEPRESS_BASE ?? "/github-code-search/"; + await page.goto(base); + for (const width of [1280, 375, 1280]) { + await page.setViewportSize({ width, height: 800 }); + await expect(page.locator(".DocSearch")).toHaveAccessibleName(/Search/); + await expect(page.locator(".DocSearch")).not.toHaveAttribute("aria-label"); + } +}); + +test("Mermaid containers retain permitted labels after client navigation", async ({ page }) => { + const base = process.env.VITEPRESS_BASE ?? "/github-code-search/"; + await page.goto(`${base}architecture/overview.html`); + const diagram = page.locator(".mermaid[aria-label]").first(); + await expect(diagram).toHaveAttribute("role", "group"); + await page.getByRole("link", { name: "Containers (C4 L2)", exact: true }).first().click(); + await expect(page.locator(".mermaid[aria-label]").first()).toHaveAttribute("role", "group"); +}); diff --git a/scripts/a11y.ts b/scripts/a11y.ts new file mode 100644 index 0000000..fd55abd --- /dev/null +++ b/scripts/a11y.ts @@ -0,0 +1,129 @@ +import AxeBuilder from "@axe-core/playwright"; +import { chromium, type Browser, type Page } from "@playwright/test"; +import { formatSummary, validateSitemapUrls, WCAG_TAGS } from "./a11y-utils.ts"; + +async function readSitemap(page: Page, base: string): Promise { + const response = await page.request.get(new URL("sitemap.xml", base).href, { + failOnStatusCode: true, + timeout: 30_000, + }); + const locations = await page.evaluate( + (xml) => { + const doc = new DOMParser().parseFromString(xml, "application/xml"); + if (doc.querySelector("parsererror") || doc.documentElement.localName !== "urlset") { + throw new Error("Expected a valid sitemap urlset"); + } + return Array.from(doc.documentElement.children).map((entry) => { + const location = entry.querySelector("loc")?.textContent?.trim(); + if (entry.localName !== "url" || !location) throw new Error("Invalid sitemap URL entry"); + return location; + }); + }, + await response.text(), + ); + return validateSitemapUrls(locations, base); +} + +async function auditPage(browser: Browser, url: string) { + const context = await browser.newContext({ reducedMotion: "reduce" }); + const page = await context.newPage(); + let timer: ReturnType | undefined; + try { + const results = await Promise.race([ + (async () => { + const response = await page.goto(url, { waitUntil: "load", timeout: 30_000 }); + if (!response?.ok()) throw new Error(`HTTP ${response?.status()} on ${url}`); + if (page.url() !== url) throw new Error(`Unexpected redirect: ${url} -> ${page.url()}`); + await page.locator("#VPContent").waitFor(); + await page.waitForFunction(() => { + const app = document.getElementById("app"); + return app && "__vue_app__" in app; + }); + await page.waitForFunction(() => !document.querySelector(".mermaid-rendering")); + await page.evaluate(() => document.fonts.ready); + return new AxeBuilder({ page }).withTags(WCAG_TAGS).analyze(); + })(), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Audit timed out on ${url}`)), 60_000); + }), + ]); + return { url, results, error: null }; + } catch (error) { + return { url, results: null, error: String(error) }; + } finally { + clearTimeout(timer); + await context.close(); + } +} + +async function runAudit(): Promise { + const started = performance.now(); + const pages: Awaited>[] = []; + let browser: Browser | undefined; + let error: string | null = null; + try { + const origin = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:4173"; + const base = new URL(process.env.VITEPRESS_BASE ?? "/github-code-search/", origin).href; + // Validate before requesting the sitemap, including the trailing slash. + validateSitemapUrls([base], base); + const activeBrowser = await chromium.launch({ + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, + }); + browser = activeBrowser; + const discovery = await browser.newPage(); + const urls = await readSitemap(discovery, base); + await discovery.close(); + let next = 0; + const worker = async () => { + while (next < urls.length) { + const url = urls[next++]; + const result = await auditPage(activeBrowser, url); + pages.push(result); + console.log( + `${result.error ? "ERROR" : result.results?.violations.length ? "FAIL" : "PASS"} ${url}`, + ); + if (result.error) console.error(result.error); + for (const violation of result.results?.violations ?? []) { + console.error(`${violation.id}: ${violation.help} (${violation.helpUrl})`); + for (const node of violation.nodes) + console.error(` ${node.target}: ${node.failureSummary}`); + } + } + }; + await Promise.all(Array.from({ length: Math.min(2, urls.length) }, worker)); + } catch (failure) { + error = String(failure); + console.error(error); + } finally { + try { + await browser?.close(); + } catch (failure) { + error = String(failure); + console.error(error); + } + pages.sort((a, b) => a.url.localeCompare(b.url)); + const durationMs = Math.round(performance.now() - started); + const summary = formatSummary( + pages.map((page) => ({ + url: page.url, + violations: page.results?.violations.length ?? 0, + incomplete: page.results?.incomplete.length ?? 0, + error: page.error, + })), + error, + ); + await Bun.write( + "a11y-report.json", + JSON.stringify({ tags: WCAG_TAGS, durationMs, error, pages }, null, 2), + ); + await Bun.write("a11y-report.md", summary); + console.log(`Audited ${pages.length} pages in ${(durationMs / 1000).toFixed(2)}s`); + } + return error || + pages.length === 0 || + pages.some((page) => page.error || page.results?.violations.length) + ? 1 + : 0; +} + +if (import.meta.main) process.exitCode = await runAudit(); From ed7da49d3f8faaa92026da6a55bd14eaf66e788a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Wed, 28 Oct 2026 15:17:53 +0100 Subject: [PATCH 2/6] Speed up accessibility audit by skipping preview compression vitepress preview Brotli-compresses the 4 MB Mermaid chunk on every request (~5 s per page). Request identity encoding in the audit context and scale workers with available CPUs (up to four). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/a11y.yaml | 2 +- AGENTS.md | 2 +- docs/reference/accessibility.md | 12 ++++++++++-- scripts/a11y.ts | 12 ++++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/a11y.yaml b/.github/workflows/a11y.yaml index 0d2be28..ab47f59 100644 --- a/.github/workflows/a11y.yaml +++ b/.github/workflows/a11y.yaml @@ -12,7 +12,7 @@ # http://localhost:4173 so the generated sitemap.xml contains localhost # URLs that the audit can reach directly) # 2. Start `vitepress preview` via the shared composite action -# 3. Exercise the audit harness, then scan the full sitemap with two workers +# 3. Exercise the audit harness, then scan the full sitemap with up to four workers (one per CPU) # 4. Publish JSON/Markdown artifacts and a sticky same-repository PR summary # # Chrome: Playwright reuses Ubuntu's system Chrome; no browser download. diff --git a/AGENTS.md b/AGENTS.md index d308685..3ed86a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,7 +80,7 @@ Install local Chromium with `bun run playwright install chromium`, or point `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at an installed Chrome executable. CI uses system Chrome without downloading another browser. -`scripts/a11y.ts` scans every sitemap URL with two workers, explicit WCAG +`scripts/a11y.ts` scans every sitemap URL with up to four workers (one per CPU), explicit WCAG 2.0/2.1/2.2 A/AA tags, and readiness checks for Vue hydration, Mermaid and fonts. Pure URL validation and Markdown formatting live in `scripts/a11y-utils.ts`. The generated `a11y-report.json` includes full axe results and operational diff --git a/docs/reference/accessibility.md b/docs/reference/accessibility.md index 53a73f4..01056e8 100644 --- a/docs/reference/accessibility.md +++ b/docs/reference/accessibility.md @@ -36,7 +36,7 @@ VITEPRESS_HOSTNAME=http://localhost:4174 VITEPRESS_BASE=/github-code-search/v2/ ## Coverage and reports The official Deque `@axe-core/playwright` adapter runs axe-core in a real browser. -`scripts/a11y.ts` scans every distinct sitemap URL with two workers sharing one +`scripts/a11y.ts` scans every distinct sitemap URL with up to four workers (one per CPU) sharing one browser. It waits for Vue hydration, Mermaid rendering and fonts, rather than a fixed sleep. Pages have isolated browser contexts and a 60-second audit deadline. @@ -98,7 +98,7 @@ dependency installation and the documentation build are excluded from audit timi | Installed `node_modules`, `du -sk` | 610,532 KiB | 575,500 KiB | The timing compares the original configuration (one worker, 1,500 ms fixed wait, -HTML_CodeSniffer) with axe (two workers, readiness checks). It is **not** an +HTML_CodeSniffer) with the first axe version (two workers, readiness checks). It is **not** an engine-only comparison or a hosted-CI speed guarantee. The first axe run audited all 36 pages but failed on newly detected search-label and Mermaid-role issues; these were repaired rather than suppressed. Adding this guide expands the final @@ -106,6 +106,14 @@ sitemap, so subsequent audit durations are not the same 36-page workload. The installed-tree reduction is about 34.2 MiB (5.7%); lockfile entries are not equivalent to unique packages. Measurements exclude browser caches. +Profiling then showed ~5 s of every page load spent waiting for `vitepress preview` +to Brotli-compress the 4 MB Mermaid chunk on each request; axe itself takes ~0.4 s. +The audit context therefore sends `Accept-Encoding: identity` (encoding does not +affect axe results; Lighthouse keeps compression) and uses one worker per CPU, up +to four. On the 37-page sitemap this brought the local audit from 116 s to 14–20 s +and the hosted CI audit step from 220 s (two workers) to the duration reported on +the pull request. + The old `WCAG2AA` HTML_CodeSniffer configuration did not explicitly add WCAG 2.1 rules despite the workflow title. It also ignored background-image contrast findings and F77 duplicate IDs. Different engines have different rule coverage: diff --git a/scripts/a11y.ts b/scripts/a11y.ts index fd55abd..f72ed67 100644 --- a/scripts/a11y.ts +++ b/scripts/a11y.ts @@ -1,6 +1,7 @@ import AxeBuilder from "@axe-core/playwright"; import { chromium, type Browser, type Page } from "@playwright/test"; import { formatSummary, validateSitemapUrls, WCAG_TAGS } from "./a11y-utils.ts"; +import { availableParallelism } from "node:os"; async function readSitemap(page: Page, base: string): Promise { const response = await page.request.get(new URL("sitemap.xml", base).href, { @@ -25,7 +26,12 @@ async function readSitemap(page: Page, base: string): Promise { } async function auditPage(browser: Browser, url: string) { - const context = await browser.newContext({ reducedMotion: "reduce" }); + // `vitepress preview` Brotli-compresses on every request: ~5 s per page for the + // 4 MB Mermaid chunk. Encoding is irrelevant to axe, so request raw bytes. + const context = await browser.newContext({ + reducedMotion: "reduce", + extraHTTPHeaders: { "Accept-Encoding": "identity" }, + }); const page = await context.newPage(); let timer: ReturnType | undefined; try { @@ -90,7 +96,9 @@ async function runAudit(): Promise { } } }; - await Promise.all(Array.from({ length: Math.min(2, urls.length) }, worker)); + await Promise.all( + Array.from({ length: Math.min(4, availableParallelism(), urls.length) }, worker), + ); } catch (failure) { error = String(failure); console.error(error); From 705aaf07a21e0da3e2916ee8605f580cc6b77a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Wed, 28 Oct 2026 15:42:16 +0100 Subject: [PATCH 3/6] Lazy-load Mermaid and reuse audit contexts to speed up a11y CI Keep the Vue runtime and Vite helpers in the framework chunk so pages without diagrams no longer download the 4 MB Mermaid chunk, import the Mermaid renderer only when a diagram is present, reuse one browser context per audit worker, and run the fixture harness in a parallel job. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../actions/start-preview-server/action.yml | 3 +- .github/skills/documentation.md | 8 ++-- .github/skills/github-actions.md | 9 ++-- .github/workflows/a11y.yaml | 32 +++++++++++-- AGENTS.md | 9 +++- docs/.vitepress/config.mts | 33 ++++++++----- docs/.vitepress/theme/index.ts | 47 ++++++++++++------- docs/reference/accessibility.md | 15 +++++- playwright.config.ts | 3 ++ scripts/a11y.ts | 43 +++++++++++++---- 10 files changed, 149 insertions(+), 53 deletions(-) diff --git a/.github/actions/start-preview-server/action.yml b/.github/actions/start-preview-server/action.yml index 401230f..8b0b2a0 100644 --- a/.github/actions/start-preview-server/action.yml +++ b/.github/actions/start-preview-server/action.yml @@ -71,7 +71,6 @@ runs: echo "::error::Preview server did not respond within ${TIMEOUT} s on ${URL}." exit 1 fi - echo "… retrying in 2 s" - sleep 2 + sleep 0.5 done echo "Preview server is ready." diff --git a/.github/skills/documentation.md b/.github/skills/documentation.md index e3b9d82..006919d 100644 --- a/.github/skills/documentation.md +++ b/.github/skills/documentation.md @@ -69,9 +69,11 @@ bun run docs:test:a11y # passing/failing fixtures and theme regression tests Runner: `scripts/a11y.ts`; pure helpers/tests: `scripts/a11y-utils.ts` and its companion test; browser regression tests: `scripts/a11y.pw.ts`. The official -`@axe-core/playwright` adapter reuses Playwright. One browser, at most two -parallel pages, no fixed sleep. Wait for hydrated VitePress, completed Mermaid -rendering and fonts before scanning. All sitemap URLs must stay within the +`@axe-core/playwright` adapter reuses Playwright. One browser, up to four workers +(one per CPU) each reusing a browser context, no fixed sleep. Wait for hydrated +VitePress, rendered visible Mermaid diagrams and fonts before scanning. Keep the +Mermaid renderer lazily imported by the theme and the Vue runtime in the `framework` +chunk so pages without diagrams never download Mermaid. All sitemap URLs must stay within the preview origin/base; empty, malformed or unreachable sitemaps must fail. Install local Chromium with `bun run playwright install chromium`, or set diff --git a/.github/skills/github-actions.md b/.github/skills/github-actions.md index 2f5002d..3786cf1 100644 --- a/.github/skills/github-actions.md +++ b/.github/skills/github-actions.md @@ -231,9 +231,12 @@ env: PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: /usr/bin/google-chrome-stable ``` -Do not install/download another browser in this job. Run `docs:test:a11y` after -the preview starts, then `docs:a11y`. The audit scans the entire sitemap with two -workers and explicit WCAG 2.0/2.1/2.2 A/AA tags. Its path filters must include +Do not install/download another browser in this job. The audit-harness fixture +tests (`docs:test:a11y --grep "audit harness"`) need no docs build and run in a +parallel `harness` job. The `audit` job runs the theme-semantics tests +(`--grep-invert "audit harness"`) after the preview starts, then `docs:a11y`. The +audit scans the entire sitemap with up to four workers and explicit WCAG +2.0/2.1/2.2 A/AA tags. Its path filters must include `scripts/a11y*.ts`, `playwright.config.ts`, `package.json`, `bun.lock` and the shared preview action as well as docs and the workflow itself. diff --git a/.github/workflows/a11y.yaml b/.github/workflows/a11y.yaml index ab47f59..2470be1 100644 --- a/.github/workflows/a11y.yaml +++ b/.github/workflows/a11y.yaml @@ -12,9 +12,13 @@ # http://localhost:4173 so the generated sitemap.xml contains localhost # URLs that the audit can reach directly) # 2. Start `vitepress preview` via the shared composite action -# 3. Exercise the audit harness, then scan the full sitemap with up to four workers (one per CPU) +# 3. Check theme semantics, then scan the full sitemap with up to four workers +# (one per CPU, each reusing a single browser context) # 4. Publish JSON/Markdown artifacts and a sticky same-repository PR summary # +# The audit-harness fixture tests need no docs build, so they run in a parallel +# `harness` job instead of extending the critical path of the audit job. +# # Chrome: Playwright reuses Ubuntu's system Chrome; no browser download. name: Accessibility audit (WCAG 2.2 AA) @@ -42,6 +46,28 @@ permissions: contents: read jobs: + harness: + name: Audit harness (fixtures) + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: /usr/bin/google-chrome-stable + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: .bun-version + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Test accessibility audit harness + run: bun run docs:test:a11y --grep "audit harness" --reporter=dot + audit: name: WCAG 2.2 AA pages audit runs-on: ubuntu-latest @@ -71,8 +97,8 @@ jobs: - name: Start VitePress preview server uses: ./.github/actions/start-preview-server - - name: Test accessibility audit harness and theme semantics - run: bun run docs:test:a11y --reporter=dot + - name: Test theme accessibility semantics + run: bun run docs:test:a11y --grep-invert "audit harness" --reporter=dot - name: Run accessibility audit (axe-core) id: audit diff --git a/AGENTS.md b/AGENTS.md index 3ed86a4..fa3b1fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,8 +80,13 @@ Install local Chromium with `bun run playwright install chromium`, or point `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at an installed Chrome executable. CI uses system Chrome without downloading another browser. -`scripts/a11y.ts` scans every sitemap URL with up to four workers (one per CPU), explicit WCAG -2.0/2.1/2.2 A/AA tags, and readiness checks for Vue hydration, Mermaid and fonts. +`scripts/a11y.ts` scans every sitemap URL with up to four workers (one per CPU), each +reusing one browser context, explicit WCAG 2.0/2.1/2.2 A/AA tags, and readiness +checks for Vue hydration, visible Mermaid diagrams and fonts. Call axe's `options()` +before `withTags()`: `options()` replaces the `runOnly` tag filter. +The theme lazy-loads the Mermaid renderer only on pages with a diagram, and the +custom `manualChunks` in `docs/.vitepress/config.mts` must keep the Vue runtime and +Vite helpers in the `framework` chunk; otherwise every page loads ~5 MB of Mermaid. Pure URL validation and Markdown formatting live in `scripts/a11y-utils.ts`. The generated `a11y-report.json` includes full axe results and operational errors; `a11y-report.md` is used for the job summary and sticky PR comment. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 1d0d91a..8ea259c 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -203,24 +203,36 @@ export default defineConfig({ }, ], // ── Chunk splitting ────────────────────────────────────────────────────── - // Four async chunk groups — all loaded lazily via vitepress-mermaid-renderer, - // so none affect initial page load. Target: keep every chunk under 500 kB. + // The theme imports vitepress-mermaid-renderer dynamically, only on pages + // that contain a diagram, so the Mermaid chunk groups below never weigh on + // other pages (~200 kB of JS modulepreloaded instead of ~5.4 MB). // // Approximate minified sizes (actual, measured): - // mermaid ~1 769 kB (mermaid core + d3, circular-dep locked together) - // mermaid-graph ~442 kB (cytoscape core) - // mermaid-layout ~201 kB (cytoscape-fcose + cytoscape-cose-bilkent) - // mermaid-utils ~163 kB (lodash-es, khroma, dayjs, roughjs, …) - // katex ~261 kB (own chunk, handled by vitepress-mermaid-renderer) + // mermaid ~4 040 kB (mermaid core + d3, circular-dep locked together) + // mermaid-graph ~433 kB (cytoscape core) + // mermaid-layout ~196 kB (cytoscape-fcose + cytoscape-cose-bilkent) + // mermaid-utils ~162 kB (lodash-es, khroma, dayjs, roughjs, …) + // katex ~255 kB (own chunk, handled by vitepress-mermaid-renderer) // - // mermaid+d3 is the one inescapable large chunk (~1 769 kB). The 1 800 kB - // limit silences the Rollup warning for that known bundle without masking - // real bloat in any other chunk (next largest is mermaid-graph at ~442 kB). + // mermaid+d3 is the one inescapable large chunk, so it still triggers the + // Rollup size warning; the 1 800 kB limit keeps every other chunk in check + // (next largest is mermaid-graph at ~433 kB). build: { chunkSizeWarningLimit: 1800, rollupOptions: { output: { manualChunks(id: string) { + // This function replaces VitePress's own chunking. Rollup pulls the + // unassigned dependencies of a manual chunk into it, so keep the + // Vue runtime in VitePress's "framework" chunk: otherwise every page + // statically imports the multi-MB Mermaid chunk just to get Vue. + if ( + id.startsWith("\0vite") || + id.includes("plugin-vue:export-helper") || + /node_modules\/(@vue\/|vue\/)/.test(id) + ) + return "framework"; + // cytoscape-fcose + cytoscape-cose-bilkent: layout algorithm extensions // (~200 kB combined). Must be checked before the bare "cytoscape" catch. if ( @@ -254,7 +266,6 @@ export default defineConfig({ // circular dependencies that Rollup cannot untangle further. if ( id.includes("node_modules/mermaid") || - id.includes("node_modules/vitepress-mermaid-renderer") || id.includes("node_modules/d3") || id.includes("node_modules/dagre-d3-es") || id.includes("node_modules/internmap") || diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 91610eb..7c568b0 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -3,7 +3,6 @@ import type { App } from "vue"; import type { Theme } from "vitepress"; import DefaultTheme from "vitepress/theme"; import { useData } from "vitepress"; -import { createMermaidRenderer } from "vitepress-mermaid-renderer"; import TerminalDemo from "./TerminalDemo.vue"; import UseCaseTabs from "./UseCaseTabs.vue"; import ComparisonTable from "./ComparisonTable.vue"; @@ -23,6 +22,28 @@ function labelDiagrams() { } } +// Mermaid and its renderer weigh ~5 MB of JS: fetch them only once a page +// actually contains a diagram (first render or client-side navigation). +const MERMAID_SOURCE = ".language-mermaid, code.mermaid"; +let mermaidRenderer: Promise | undefined; +let mermaidTheme: "dark" | "base" = "base"; + +function initMermaid(theme = mermaidTheme) { + mermaidTheme = theme; + if (typeof document === "undefined") return; + if (!mermaidRenderer && !document.querySelector(MERMAID_SOURCE)) return; + mermaidRenderer ??= import("vitepress-mermaid-renderer"); + void mermaidRenderer.then(({ createMermaidRenderer }) => + createMermaidRenderer({ + startOnLoad: false, + theme: mermaidTheme, + securityLevel: "strict", + // Fulll brand theme variables applied via %%{init}%% in each diagram. + // The renderer handles re-render on theme toggle automatically. + }), + ); +} + export default { extends: DefaultTheme, enhanceApp({ app }: { app: App }) { @@ -43,7 +64,10 @@ export default { labelDiagrams(); const content = document.getElementById("VPContent"); if (content) { - diagramObserver = new MutationObserver(labelDiagrams); + diagramObserver = new MutationObserver(() => { + labelDiagrams(); + if (!mermaidRenderer) initMermaid(); + }); diagramObserver.observe(content, { childList: true, subtree: true }); } }); @@ -52,22 +76,9 @@ export default { Layout: () => { const { isDark, frontmatter } = useData(); - const initMermaid = () => { - createMermaidRenderer({ - startOnLoad: false, - theme: isDark.value ? "dark" : "base", - securityLevel: "strict", - // Fulll brand theme variables applied via %%{init}%% in each diagram. - // The renderer handles re-render on theme toggle automatically. - }); - }; - - nextTick(() => initMermaid()); - - watch( - () => isDark.value, - () => initMermaid(), - ); + const theme = () => (isDark.value ? "dark" : "base"); + nextTick(() => initMermaid(theme())); + watch(isDark, () => initMermaid(theme())); return h(DefaultTheme.Layout, null, { // Version badge pill ("v1.x is here!") — only on the homepage, above hero name diff --git a/docs/reference/accessibility.md b/docs/reference/accessibility.md index 01056e8..9cd37f5 100644 --- a/docs/reference/accessibility.md +++ b/docs/reference/accessibility.md @@ -37,8 +37,10 @@ VITEPRESS_HOSTNAME=http://localhost:4174 VITEPRESS_BASE=/github-code-search/v2/ The official Deque `@axe-core/playwright` adapter runs axe-core in a real browser. `scripts/a11y.ts` scans every distinct sitemap URL with up to four workers (one per CPU) sharing one -browser. It waits for Vue hydration, Mermaid rendering and fonts, rather than a fixed -sleep. Pages have isolated browser contexts and a 60-second audit deadline. +browser. It waits for Vue hydration, rendering of every visible Mermaid diagram and +fonts, rather than a fixed sleep. Each page is a fresh document with a 60-second audit +deadline; a worker reuses its browser context (HTTP and compiled-code caches) and +replaces it after any error. axe only collects `violations` and `incomplete` results. Enabled tags are `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa` and `wcag22aa`: WCAG 2.0/2.1/2.2 A/AA rules supported by axe. No contrast or ARIA rules are disabled. @@ -114,6 +116,15 @@ to four. On the 37-page sitemap this brought the local audit from 116 s to 14– and the hosted CI audit step from 220 s (two workers) to the duration reported on the pull request. +A second profiling pass found that every page, even without a diagram, downloaded +~5.4 MB of JavaScript: the custom `manualChunks` replaced VitePress's own chunking, +so Rollup bundled the Vue runtime into the Mermaid chunk, and the theme imported +the Mermaid renderer statically. The Vue runtime and Vite helpers now go to the +`framework` chunk, and the theme only imports the renderer on pages that contain a +diagram, so other pages modulepreload ~200 kB. Combined with per-worker context reuse, +this brought the local audit from 14 s to 8 s. In CI, the audit-harness fixture tests +(which need no docs build) now run in a parallel job. + The old `WCAG2AA` HTML_CodeSniffer configuration did not explicitly add WCAG 2.1 rules despite the workflow title. It also ignored background-image contrast findings and F77 duplicate IDs. Different engines have different rule coverage: diff --git a/playwright.config.ts b/playwright.config.ts index da09d07..fbecf8c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -31,6 +31,9 @@ export default defineConfig({ baseURL: BASE_URL, /* Disable JS animations — they don't affect layout but speed up page load */ reducedMotion: "reduce", + /* `vitepress preview` Brotli-compresses every response on the fly (~5 s for + the 4 MB Mermaid chunk); encoding is irrelevant to these tests. */ + extraHTTPHeaders: { "Accept-Encoding": "identity" }, launchOptions: { executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, }, diff --git a/scripts/a11y.ts b/scripts/a11y.ts index f72ed67..5ea52a8 100644 --- a/scripts/a11y.ts +++ b/scripts/a11y.ts @@ -25,14 +25,15 @@ async function readSitemap(page: Page, base: string): Promise { return validateSitemapUrls(locations, base); } -async function auditPage(browser: Browser, url: string) { - // `vitepress preview` Brotli-compresses on every request: ~5 s per page for the - // 4 MB Mermaid chunk. Encoding is irrelevant to axe, so request raw bytes. - const context = await browser.newContext({ +// `vitepress preview` Brotli-compresses on every request (~5 s for the 4 MB Mermaid +// chunk). Encoding is irrelevant to axe, so request raw bytes. +const newAuditContext = (browser: Browser) => + browser.newContext({ reducedMotion: "reduce", extraHTTPHeaders: { "Accept-Encoding": "identity" }, }); - const page = await context.newPage(); + +async function auditPage(page: Page, url: string) { let timer: ReturnType | undefined; try { const results = await Promise.race([ @@ -45,9 +46,24 @@ async function auditPage(browser: Browser, url: string) { const app = document.getElementById("app"); return app && "__vue_app__" in app; }); - await page.waitForFunction(() => !document.querySelector(".mermaid-rendering")); + // Mermaid is lazy-loaded: wait until every visible diagram is rendered. + // Off-screen blocks stay as source until scrolled into view (200px margin). + await page.waitForFunction( + () => + !document.querySelector(".mermaid-rendering") && + Array.from(document.querySelectorAll("div.language-mermaid")).every( + (block) => + block.querySelector(".mermaid svg") || + (block.querySelector("pre[data-mermaid-processed]") && + block.getBoundingClientRect().top >= innerHeight + 200), + ), + ); await page.evaluate(() => document.fonts.ready); - return new AxeBuilder({ page }).withTags(WCAG_TAGS).analyze(); + // options() replaces runOnly, so it must come before withTags(). + return new AxeBuilder({ page }) + .options({ resultTypes: ["violations", "incomplete"] }) + .withTags(WCAG_TAGS) + .analyze(); })(), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`Audit timed out on ${url}`)), 60_000); @@ -58,7 +74,6 @@ async function auditPage(browser: Browser, url: string) { return { url, results: null, error: String(error) }; } finally { clearTimeout(timer); - await context.close(); } } @@ -80,11 +95,20 @@ async function runAudit(): Promise { const urls = await readSitemap(discovery, base); await discovery.close(); let next = 0; + // One context per worker: navigations reuse the HTTP and V8 code caches + // (~4x faster page loads). A fresh context replaces it after any error. const worker = async () => { + let context = await newAuditContext(activeBrowser); + let page = await context.newPage(); while (next < urls.length) { const url = urls[next++]; - const result = await auditPage(activeBrowser, url); + const result = await auditPage(page, url); pages.push(result); + if (result.error) { + await context.close().catch(() => {}); + context = await newAuditContext(activeBrowser); + page = await context.newPage(); + } console.log( `${result.error ? "ERROR" : result.results?.violations.length ? "FAIL" : "PASS"} ${url}`, ); @@ -95,6 +119,7 @@ async function runAudit(): Promise { console.error(` ${node.target}: ${node.failureSummary}`); } } + await context.close(); }; await Promise.all( Array.from({ length: Math.min(4, availableParallelism(), urls.length) }, worker), From dacf01a46c69f7bcc8795ce6311e54097ce21e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Wed, 28 Oct 2026 15:45:48 +0100 Subject: [PATCH 4/6] Run theme a11y tests alongside the audit and unblock preview start Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../actions/start-preview-server/action.yml | 5 +++- .github/skills/github-actions.md | 7 ++++-- .github/workflows/a11y.yaml | 24 +++++++++++++++---- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.github/actions/start-preview-server/action.yml b/.github/actions/start-preview-server/action.yml index 8b0b2a0..6e27f1c 100644 --- a/.github/actions/start-preview-server/action.yml +++ b/.github/actions/start-preview-server/action.yml @@ -34,7 +34,9 @@ runs: shell: bash env: PORT: ${{ inputs.port }} - run: bun run docs:preview -- --port "$PORT" & + # Redirect output: a background process holding the step's stdout open + # makes the runner wait ~5 s before moving on. + run: bun run docs:preview -- --port "$PORT" > "$RUNNER_TEMP/vitepress-preview.log" 2>&1 & - name: Wait for preview server to be ready shell: bash @@ -69,6 +71,7 @@ runs: until curl -sf "${URL}" > /dev/null 2>&1; do if [ "$(date +%s)" -ge "${DEADLINE}" ]; then echo "::error::Preview server did not respond within ${TIMEOUT} s on ${URL}." + cat "$RUNNER_TEMP/vitepress-preview.log" || true exit 1 fi sleep 0.5 diff --git a/.github/skills/github-actions.md b/.github/skills/github-actions.md index 3786cf1..a681e5a 100644 --- a/.github/skills/github-actions.md +++ b/.github/skills/github-actions.md @@ -233,8 +233,11 @@ env: Do not install/download another browser in this job. The audit-harness fixture tests (`docs:test:a11y --grep "audit harness"`) need no docs build and run in a -parallel `harness` job. The `audit` job runs the theme-semantics tests -(`--grep-invert "audit harness"`) after the preview starts, then `docs:a11y`. The +parallel `harness` job. Once the preview starts, the `audit` job runs the +theme-semantics tests (`--grep-invert "audit harness"`) in the background while +`docs:a11y` runs, then checks their exit code in a later step. Composite actions +that start background servers must redirect their output, otherwise the runner +waits ~5 s for the step's stdout to close. The audit scans the entire sitemap with up to four workers and explicit WCAG 2.0/2.1/2.2 A/AA tags. Its path filters must include `scripts/a11y*.ts`, `playwright.config.ts`, `package.json`, `bun.lock` and the shared diff --git a/.github/workflows/a11y.yaml b/.github/workflows/a11y.yaml index 2470be1..847ce87 100644 --- a/.github/workflows/a11y.yaml +++ b/.github/workflows/a11y.yaml @@ -12,8 +12,8 @@ # http://localhost:4173 so the generated sitemap.xml contains localhost # URLs that the audit can reach directly) # 2. Start `vitepress preview` via the shared composite action -# 3. Check theme semantics, then scan the full sitemap with up to four workers -# (one per CPU, each reusing a single browser context) +# 3. Scan the full sitemap with up to four workers (one per CPU, each reusing +# a single browser context) while the theme-semantics tests run alongside # 4. Publish JSON/Markdown artifacts and a sticky same-repository PR summary # # The audit-harness fixture tests need no docs build, so they run in a parallel @@ -97,14 +97,30 @@ jobs: - name: Start VitePress preview server uses: ./.github/actions/start-preview-server - - name: Test theme accessibility semantics - run: bun run docs:test:a11y --grep-invert "audit harness" --reporter=dot + # Theme-semantics tests run concurrently with the audit (same preview); + # their result is checked after the audit step. + - name: Start theme accessibility semantics tests + id: theme + run: | + ( + rc=0 + bun run docs:test:a11y --grep-invert "audit harness" --reporter=dot || rc=$? + echo "$rc" > "$RUNNER_TEMP/theme-tests.exit" + ) > "$RUNNER_TEMP/theme-tests.log" 2>&1 & - name: Run accessibility audit (axe-core) id: audit continue-on-error: true run: bun run docs:a11y + - name: Check theme accessibility semantics tests + if: ${{ !cancelled() && steps.theme.outcome == 'success' }} + timeout-minutes: 3 + run: | + until [ -f "$RUNNER_TEMP/theme-tests.exit" ]; do sleep 0.5; done + cat "$RUNNER_TEMP/theme-tests.log" + exit "$(cat "$RUNNER_TEMP/theme-tests.exit")" + - name: Publish job summary if: ${{ !cancelled() && steps.audit.outcome != 'skipped' }} run: cat a11y-report.md >> "$GITHUB_STEP_SUMMARY" From a29e8437b5d79d15c04b093bf75d15e7cee9841e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Wed, 28 Oct 2026 15:47:58 +0100 Subject: [PATCH 5/6] Document accessibility CI timings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/reference/accessibility.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/reference/accessibility.md b/docs/reference/accessibility.md index 9cd37f5..2319aaf 100644 --- a/docs/reference/accessibility.md +++ b/docs/reference/accessibility.md @@ -123,7 +123,10 @@ the Mermaid renderer statically. The Vue runtime and Vite helpers now go to the `framework` chunk, and the theme only imports the renderer on pages that contain a diagram, so other pages modulepreload ~200 kB. Combined with per-worker context reuse, this brought the local audit from 14 s to 8 s. In CI, the audit-harness fixture tests -(which need no docs build) now run in a parallel job. +(which need no docs build) now run in a parallel job, the theme-semantics tests run +alongside the audit, and the preview action no longer blocks ~5 s on its background +server's output: the audit step went from 41 s to 17 s and the audit job from 108 s +to 45 s. The old `WCAG2AA` HTML_CodeSniffer configuration did not explicitly add WCAG 2.1 rules despite the workflow title. It also ignored background-image contrast From eb43819ed6ce0ca25fb642317e6bf1e371e285b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Wed, 28 Oct 2026 16:27:31 +0100 Subject: [PATCH 6/6] Address review feedback and move audit guide out of product docs Reject redirected sitemaps, skip the sticky comment on Dependabot runs, wait for the destination heading in the Mermaid navigation test, and remove the reference page: the audit covers the docs site, not the CLI. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../documentation.instructions.md | 5 +- .github/skills/documentation.md | 5 +- .github/skills/github-actions.md | 3 +- .github/workflows/a11y.yaml | 2 +- AGENTS.md | 7 +- CONTRIBUTING.md | 7 +- README.md | 3 +- docs/.vitepress/config.mts | 1 - docs/reference/accessibility.md | 145 ------------------ scripts/a11y.pw.ts | 6 +- scripts/a11y.ts | 8 +- 11 files changed, 29 insertions(+), 163 deletions(-) delete mode 100644 docs/reference/accessibility.md diff --git a/.github/instructions/documentation.instructions.md b/.github/instructions/documentation.instructions.md index 87b4ac7..3b8f7e2 100644 --- a/.github/instructions/documentation.instructions.md +++ b/.github/instructions/documentation.instructions.md @@ -119,5 +119,6 @@ findings requiring manual review. Do not disable contrast/ARIA rules or treat passing automation as WCAG certification. See `.github/skills/documentation.md` for the axe runner, WCAG contrast rules, -accessible component patterns and responsive guidance, and -`docs/reference/accessibility.md` for coverage and benchmark methodology. +accessible component patterns and responsive guidance, and `CONTRIBUTING.md` for +the manual-review checklist. The audit covers the documentation site only, so do +not document it in the product docs (`docs/`); the benchmark lives in issue #241. diff --git a/.github/skills/documentation.md b/.github/skills/documentation.md index 006919d..7383793 100644 --- a/.github/skills/documentation.md +++ b/.github/skills/documentation.md @@ -92,8 +92,9 @@ shortcut. Its mobile label is visually hidden, not `display:none`. Mermaid wrappers with `aria-label` receive `role="group"` via a scoped observer so asynchronously rendered diagrams and client-side navigation stay valid. -See `docs/reference/accessibility.md` for the sourced alternatives benchmark, -measurement methodology, engine-coverage differences and manual-review checklist. +The audit only covers the documentation site, not the product: keep it out of the +product docs (`docs/`). The manual-review checklist is in `CONTRIBUTING.md`; the +sourced alternatives benchmark and engine-coverage differences are in issue #241. ### Common patterns diff --git a/.github/skills/github-actions.md b/.github/skills/github-actions.md index a681e5a..4ab4736 100644 --- a/.github/skills/github-actions.md +++ b/.github/skills/github-actions.md @@ -245,7 +245,8 @@ preview action as well as docs and the workflow itself. Always upload `a11y-report.json` and `a11y-report.md`, publish the Markdown to `GITHUB_STEP_SUMMARY`, and post the `accessibility-audit` sticky comment only -for same-repository PRs (fork tokens cannot write comments). Preserve a failing +for same-repository PRs not triggered by `dependabot[bot]` (fork and Dependabot +tokens are read-only and cannot write comments). Preserve a failing job after reporting violations or operational errors; do not mask scan failures with `continue-on-error` alone. Incomplete results are manual-review findings, not proof of compliance. diff --git a/.github/workflows/a11y.yaml b/.github/workflows/a11y.yaml index 847ce87..d60220a 100644 --- a/.github/workflows/a11y.yaml +++ b/.github/workflows/a11y.yaml @@ -126,7 +126,7 @@ jobs: run: cat a11y-report.md >> "$GITHUB_STEP_SUMMARY" - name: Post / update accessibility report on PR - if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.audit.outcome != 'skipped' }} + if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && steps.audit.outcome != 'skipped' }} uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: accessibility-audit diff --git a/AGENTS.md b/AGENTS.md index fa3b1fb..3fa85c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,9 +95,10 @@ manual review, not an automatic compliance claim. Never disable contrast/ARIA rules or carry over HTML_CodeSniffer exclusions as a shortcut. Read `.github/skills/documentation.md` for accessible theme patterns and -`.github/skills/github-actions.md` before changing the workflow. The comparison, -measurement caveats and manual-review checklist are in -`docs/reference/accessibility.md`. Repository-local custom agents are not +`.github/skills/github-actions.md` before changing the workflow. The audit only +guarantees the accessibility of the documentation site, not of the CLI, so it is +documented for contributors in `CONTRIBUTING.md` (manual-review checklist), not in +the product docs. The alternatives benchmark is in issue #241. Repository-local custom agents are not currently defined; these shared instructions are the agent entry point. ## Project layout diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a70db2..1fc6ac6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,8 +144,11 @@ Inspect `a11y-report.json` for affected nodes, rule help links, scan errors and incomplete findings; `a11y-report.md` summarizes every page. Any violation or operational error fails the command. Do not copy obsolete HTML_CodeSniffer exceptions into axe or disable rules to make CI pass. Manual keyboard, -screen-reader and interaction checks remain necessary. See the -[audit guide](docs/reference/accessibility.md) for coverage and benchmark details. +screen-reader and interaction checks remain necessary: review `incomplete` results, +keyboard navigation, focus order, screen-reader announcements, zoom/reflow, both +color schemes and interactive states (search dialog, tabs, expanded menus). The audit +only checks the initial desktop state of each page. The alternatives benchmark that +led to `@axe-core/playwright` is in [#241](https://github.com/fulll/github-code-search/issues/241). ## AI agent tooling diff --git a/README.md b/README.md index 65753e4..ddd57bf 100644 --- a/README.md +++ b/README.md @@ -149,4 +149,5 @@ The audit checks every sitemap URL with WCAG 2.0/2.1/2.2 A/AA tags and writes JS and Markdown reports. CI reuses system Chrome without downloading a browser. Automated checks do not certify WCAG compliance: incomplete results, keyboard use, screen-reader behavior and interactive states still need manual review. -See the [audit guide and alternatives benchmark](https://fulll.github.io/github-code-search/reference/accessibility). +See [CONTRIBUTING.md](CONTRIBUTING.md#contributing) for the audit workflow and +[#241](https://github.com/fulll/github-code-search/issues/241) for the alternatives benchmark. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 8ea259c..6d5f10f 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -370,7 +370,6 @@ export default defineConfig({ link: "/reference/github-api-limits", }, { text: "Environment variables", link: "/reference/environment" }, - { text: "Accessibility audits", link: "/reference/accessibility" }, ], }, ], diff --git a/docs/reference/accessibility.md b/docs/reference/accessibility.md deleted file mode 100644 index 2319aaf..0000000 --- a/docs/reference/accessibility.md +++ /dev/null @@ -1,145 +0,0 @@ -# Documentation accessibility audits - -Run the full-sitemap audit when changing documentation, theme components or the audit harness. - -```bash -bun run docs:build:a11y -bun run docs:preview -- --port 4173 -``` - -In another terminal: - -```bash -# One-time local browser setup; skip when using an existing Chrome executable. -bun run playwright install chromium -bun run docs:a11y -bun run docs:test:a11y -bun run docs:test:responsive -``` - -## Browser and preview configuration - -`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` selects an existing Chrome/Chromium executable. -The accessibility CI job uses `/usr/bin/google-chrome-stable` on Ubuntu; it does not -download another browser. Without this variable, local runs use Playwright's installed -Chromium. The browser is separate from the `node_modules` footprint. - -`PLAYWRIGHT_BASE_URL` defaults to `http://localhost:4173`. `VITEPRESS_BASE` defaults to -`/github-code-search/` and must end with `/`. Set the same base during the build and -audit for versioned documentation. `docs:build:a11y` sets the sitemap hostname to -localhost. For another preview origin, build with `VITEPRESS_HOSTNAME` explicitly: - -```bash -VITEPRESS_HOSTNAME=http://localhost:4174 VITEPRESS_BASE=/github-code-search/v2/ bun run docs:build -``` - -## Coverage and reports - -The official Deque `@axe-core/playwright` adapter runs axe-core in a real browser. -`scripts/a11y.ts` scans every distinct sitemap URL with up to four workers (one per CPU) sharing one -browser. It waits for Vue hydration, rendering of every visible Mermaid diagram and -fonts, rather than a fixed sleep. Each page is a fresh document with a 60-second audit -deadline; a worker reuses its browser context (HTTP and compiled-code caches) and -replaces it after any error. axe only collects `violations` and `incomplete` results. - -Enabled tags are `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa` and `wcag22aa`: WCAG -2.0/2.1/2.2 A/AA rules supported by axe. No contrast or ARIA rules are disabled. -Empty, malformed, out-of-base or unreachable sitemaps, broken pages and scanner -errors fail the audit, as do violations of any impact level. - -| Report | Contents | -| ------------------ | ---------------------------------------------------------------------------------------- | -| `a11y-report.json` | Engine results, affected nodes, remediation links, incomplete rules, errors and duration | -| `a11y-report.md` | Per-page violation/manual-review counts and status | - -CI uploads both reports, publishes a job summary and updates a sticky comment on -same-repository PRs. Fork PRs still receive artifacts and a job summary without -requiring comment-write permissions. - -::: warning Automated checks are not WCAG certification -Review `incomplete` results manually. Also test keyboard navigation, focus order, -screen-reader announcements, zoom/reflow, both color schemes, and interactive -states such as search dialogs, tabs and expanded menus. The default automated audit -checks the initial desktop page state; it does not automatically exercise all -themes, viewports or interactions. See the -[Playwright accessibility guide](https://playwright.dev/docs/accessibility-testing) -and [W3C guidance on evaluating accessibility](https://www.w3.org/WAI/test-evaluate/). -::: - -## Alternatives benchmark - -Evaluated on 2026-10-28. All shortlisted projects showed recent maintenance; -pa11y-ci was not replaced because it was abandoned. - -The sizes below are registry `dist.unpackedSize` values for the named package, -**not** its full dependency tree, browser binary or download size. - -| Alternative | Version | Unpacked package | Integration trade-off | -| ------------------------- | ---------- | ---------------: | --------------------------------------------------------------------------- | -| pa11y-ci | 5.0.0 | 42,330 bytes | Adds Pa11y, HTML_CodeSniffer and a separate Puppeteer stack | -| **@axe-core/playwright** | **4.14.0** | **47,795 bytes** | **Reuses existing Playwright; only runtime dependency is axe-core** | -| Direct axe-core | 4.14.0 | 3,430,531 bytes | No dependencies, but requires maintaining browser/frame injection ourselves | -| @axe-core/cli | 4.14.0 | 71,507 bytes | Also adds Selenium, ChromeDriver and the WebDriver adapter | -| IBM accessibility-checker | 4.0.34 | 6,042,998 bytes | Adds Puppeteer, ChromeDriver, telemetry and spreadsheet reporting | - -The selected adapter also needs axe-core (3,430,531 bytes). The engine is already -used by Lighthouse; the migration moves the shared engine from 4.13 to 4.14. -Only the adapter and that engine version were introduced, with no unrelated -package upgrades. Removing pa11y-ci does **not** remove Lighthouse's Puppeteer-core. - -### Measurements and limitations - -Initial same-machine comparison: macOS arm64, Bun 1.4.4 canary, system Chrome -156.0.8078.48, the same built 36-page sitemap and preview server. Browser downloads, -dependency installation and the documentation build are excluded from audit timing. - -| Metric | Before | After | -| ------------------------------------------------------ | ----------: | ----------: | -| Audit wall time, single exploratory run | 294.76 s | 112.69 s | -| Lockfile package entries, including nested resolutions | 857 | 787 | -| Installed `node_modules`, `du -sk` | 610,532 KiB | 575,500 KiB | - -The timing compares the original configuration (one worker, 1,500 ms fixed wait, -HTML_CodeSniffer) with the first axe version (two workers, readiness checks). It is **not** an -engine-only comparison or a hosted-CI speed guarantee. The first axe run audited -all 36 pages but failed on newly detected search-label and Mermaid-role issues; -these were repaired rather than suppressed. Adding this guide expands the final -sitemap, so subsequent audit durations are not the same 36-page workload. -The installed-tree reduction is about 34.2 MiB (5.7%); lockfile entries are not -equivalent to unique packages. Measurements exclude browser caches. - -Profiling then showed ~5 s of every page load spent waiting for `vitepress preview` -to Brotli-compress the 4 MB Mermaid chunk on each request; axe itself takes ~0.4 s. -The audit context therefore sends `Accept-Encoding: identity` (encoding does not -affect axe results; Lighthouse keeps compression) and uses one worker per CPU, up -to four. On the 37-page sitemap this brought the local audit from 116 s to 14–20 s -and the hosted CI audit step from 220 s (two workers) to the duration reported on -the pull request. - -A second profiling pass found that every page, even without a diagram, downloaded -~5.4 MB of JavaScript: the custom `manualChunks` replaced VitePress's own chunking, -so Rollup bundled the Vue runtime into the Mermaid chunk, and the theme imported -the Mermaid renderer statically. The Vue runtime and Vite helpers now go to the -`framework` chunk, and the theme only imports the renderer on pages that contain a -diagram, so other pages modulepreload ~200 kB. Combined with per-worker context reuse, -this brought the local audit from 14 s to 8 s. In CI, the audit-harness fixture tests -(which need no docs build) now run in a parallel job, the theme-semantics tests run -alongside the audit, and the preview action no longer blocks ~5 s on its background -server's output: the audit step went from 41 s to 17 s and the audit job from 108 s -to 45 s. - -The old `WCAG2AA` HTML_CodeSniffer configuration did not explicitly add WCAG 2.1 -rules despite the workflow title. It also ignored background-image contrast -findings and F77 duplicate IDs. Different engines have different rule coverage: -axe's explicit 2.1/2.2 tags do not establish rule-for-rule HTML_CodeSniffer parity. -Static DOM-only checks cannot replace rendered contrast checks, and Lighthouse's -sampled performance audits are complementary rather than a full-sitemap substitute. - -### Maintenance and sources - -- [axe-core 4.14.0](https://github.com/dequelabs/axe-core/releases/tag/v4.14.0): released 2026-10-05; Deque-maintained, MPL-2.0. -- [Official Playwright adapter](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright): 4.14.0 published 2026-10-15; repository activity observed 2026-10-27. -- [pa11y-ci 5.0.0](https://github.com/pa11y/pa11y-ci/releases/tag/5.0.0): released 2026-10-05; LGPL-3.0-only. -- [Pa11y standards and runners](https://github.com/pa11y/pa11y#runners): default HTML_CodeSniffer versus optional axe. -- [IBM Equal Access](https://github.com/IBMa/equal-access): checker 4.0.34 published 2026-09-08; repository activity observed 2026-10-16; Apache-2.0. -- Package metadata and sizes: [pa11y-ci registry](https://registry.npmjs.org/pa11y-ci/5.0.0), [Playwright adapter registry](https://registry.npmjs.org/@axe-core/playwright/4.14.0), [axe-core registry](https://registry.npmjs.org/axe-core/4.14.0), [axe CLI registry](https://registry.npmjs.org/@axe-core/cli/4.14.0), [IBM checker registry](https://registry.npmjs.org/accessibility-checker/4.0.34). diff --git a/scripts/a11y.pw.ts b/scripts/a11y.pw.ts index ebdf4e7..a177f47 100644 --- a/scripts/a11y.pw.ts +++ b/scripts/a11y.pw.ts @@ -16,11 +16,14 @@ for (const scenario of [ "outside", "http-error", "sitemap-error", + "sitemap-redirect", ]) { test(`audit harness: ${scenario}`, async () => { const directory = await mkdtemp(join(tmpdir(), "gcs-a11y-")); const server = createServer((request, response) => { - if (request.url === "/docs/sitemap.xml") { + if (request.url === "/docs/sitemap.xml" && scenario === "sitemap-redirect") { + response.writeHead(301, { Location: "/docs/moved-sitemap.xml" }).end(); + } else if (request.url === "/docs/sitemap.xml" || request.url === "/docs/moved-sitemap.xml") { response.setHeader("Content-Type", "application/xml"); if (scenario === "sitemap-error") { response.writeHead(500).end("Unavailable"); @@ -125,5 +128,6 @@ test("Mermaid containers retain permitted labels after client navigation", async const diagram = page.locator(".mermaid[aria-label]").first(); await expect(diagram).toHaveAttribute("role", "group"); await page.getByRole("link", { name: "Containers (C4 L2)", exact: true }).first().click(); + await expect(page.getByRole("heading", { level: 1, name: "Level 2: Containers" })).toBeVisible(); await expect(page.locator(".mermaid[aria-label]").first()).toHaveAttribute("role", "group"); }); diff --git a/scripts/a11y.ts b/scripts/a11y.ts index 5ea52a8..1535629 100644 --- a/scripts/a11y.ts +++ b/scripts/a11y.ts @@ -4,10 +4,10 @@ import { formatSummary, validateSitemapUrls, WCAG_TAGS } from "./a11y-utils.ts"; import { availableParallelism } from "node:os"; async function readSitemap(page: Page, base: string): Promise { - const response = await page.request.get(new URL("sitemap.xml", base).href, { - failOnStatusCode: true, - timeout: 30_000, - }); + const url = new URL("sitemap.xml", base).href; + // Redirects are not followed: a moved sitemap must fail like any other non-2xx. + const response = await page.request.get(url, { maxRedirects: 0, timeout: 30_000 }); + if (!response.ok()) throw new Error(`HTTP ${response.status()} on ${url}`); const locations = await page.evaluate( (xml) => { const doc = new DOMParser().parseFromString(xml, "application/xml");