Skip to content

feat(axe): add axe-core a11y gate (serious/critical, config-driven)#6

Merged
bdelanghe merged 2 commits into
mainfrom
conformance/axe-gate
Jun 29, 2026
Merged

feat(axe): add axe-core a11y gate (serious/critical, config-driven)#6
bdelanghe merged 2 commits into
mainfrom
conformance/axe-gate

Conversation

@bdelanghe

Copy link
Copy Markdown
Contributor

Adds gates/axe-gate.mjs — a generic, config-driven axe-core accessibility gate so the conformance model's a11y.axe-serious-critical criterion becomes a continuously-ENFORCED, gate-backed check instead of not-assessed.

What it does

  • Loads each built page in a real browser and runs axe-core with the WCAG 2.x A/AA ruleset, failing closed on any violation at/above a configurable impact threshold (default serious).
  • Config-driven, nothing hardcoded: $DIST / argv (dist dir), $AXE_PAGES (default: every *.html), $AXE_TAGS (default wcag2a,wcag2aa,wcag21a,wcag21aa,wcag22aa), $AXE_IMPACT_THRESHOLD, $AXE_RUNNER, $AXE_REPORT.
  • Serves dist over an ephemeral localhost origin first, so absolute asset paths resolve (running file:// would strip styles and fabricate layout-dependent violations).
  • Two interchangeable engines: playwright (default, CI — @axe-core/playwright + bundled Chromium) and tezcatl (local, macOS WebKit — injects axe.min.js, reads results back).
  • Emits a machine-readable report: violations grouped by impact, per page, plus the axe: { serious, critical } envelope that conformance-report's evaluator consumes verbatim.

Tests

  • Pure classification/threshold/report logic asserted deterministically (no browser).
  • fixtures/axe/{bad,good}.html exercised end-to-end when a browser engine is on PATH (skipped, like the cosign step, when none is). Local run: known-bad = 1 critical / 2 serious blocking, known-good = clean. 13/13 pass.

Packaging

  • axe-core added as a dependency (small; used by the tezcatl injection path).
  • @axe-core/playwright + playwright are consumer-supplied (dynamically imported with a clear error) to keep npm ci light — sites install them in their axe.yml.

Independent PR; based on main. Sites (bdelanghe/site, bounded-systems/site) re-vendor this gate + wire an axe.yml workflow in companion PRs.

🤖 Generated with Claude Code

bdelanghe and others added 2 commits June 28, 2026 18:35
gates/axe-gate.mjs runs axe-core over each built page in a real browser and
fails closed on any violation at or above a configurable impact threshold
(default: serious). Everything is config-driven (dist dir, page list, ruleset
tags, threshold, runner) with neutral defaults — nothing site-specific is
hardcoded. It serves dist over an ephemeral origin so absolute asset paths
resolve (running file:// would strip styles and fabricate layout-dependent
findings), then drives one of two interchangeable engines:

  - playwright (default, CI): @axe-core/playwright + bundled Chromium
  - tezcatl (local, macOS WebKit): injects axe.min.js and reads results back

The emitted machine-readable report groups violations by impact per page and
exposes an `axe: { serious, critical }` envelope — exactly what
conformance-report's `a11y.axe-serious-critical` criterion consumes, so a clean
run is what lets a site honestly assert it and a regression turns CI red.

Pure classification/threshold/report logic is exported and unit-tested
deterministically; a fixtures/axe/ known-bad + known-good pair is exercised
end-to-end when a browser engine is on PATH (skipped, like the cosign step,
when none is). axe-core added as a dependency; @axe-core/playwright + playwright
are consumer-supplied (dynamically imported) to keep `npm ci` light.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uU1XvPggEPNMiXBUrV8hy
@bdelanghe bdelanghe marked this pull request as ready for review June 29, 2026 00:06
@bdelanghe bdelanghe merged commit dbc2c54 into main Jun 29, 2026
1 check passed
@bdelanghe bdelanghe deleted the conformance/axe-gate branch June 29, 2026 00:06
bdelanghe added a commit to bdelanghe/site that referenced this pull request Jun 29, 2026
…it gate via tezcatl locally (#161)

* refactor(conformance): consume the kit's axe gate (real browser) instead of the bespoke jsdom one

Re-vendor conformance-kit to current main (dbc2c54) and replace bd-site's ad-hoc
jsdom axe gate with the kit's reusable, dual-runner gate (bounded-systems/conformance-kit#6).

Why it's a strict upgrade: the kit gate loads each built page in a REAL browser and
runs axe's full WCAG 2.x A/AA ruleset, so it covers the layout-dependent rules
(color-contrast, target-size) that jsdom forced us to disable. Verified clean over
all 7 pages via tezcatl (macOS WebKit, no Chromium): 0 violations at any level —
confirming the jsdom gate hid nothing, and axe now corroborates lone's static
color_contrast check rather than splitting coverage.

- scripts/vendor-integrity.mjs — add gates/axe-gate.mjs to the vendored FILES.
- .github/workflows/conformance.yml — axe step now runs the vendored kit gate; CI
  uses the Playwright/Chromium runner (`npx playwright install`), local dev uses
  tezcatl (`npm run check:axe`, no Chromium download).
- package.json — drop jsdom; add @axe-core/playwright + playwright; check:axe →
  kit gate via tezcatl. Remove scripts/axe-gate.mjs.
- data/conformance-evidence.json — _gates/_source updated (real browser, full
  coverage). evidence.axe {serious:0, critical:0} unchanged.

Side effect of the re-vendor: the kit's external-grader criteria (#5: scorecard,
HSTS-preload, SLSA level) now appear on /conformance as not-assessed (bd-site
supplies no evidence for them) — additive and honest. structure.json baseline
regenerated for the new /conformance rows. SBOM self-heals (352 pkgs, check:sbom green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: keep axe CI browser-free (jsdom), add the kit gate for local full-coverage via tezcatl

Reworks the kit-gate adoption per the no-macOS / no-Chromium constraint. tezcatl is
native macOS WebKit (links AppKit/CoreGraphics) — it cannot run on a Linux CI runner
— so CI stays BROWSER-FREE on the bespoke jsdom gate, and the vendored kit gate is
the DEEPER LOCAL pass (real browser via tezcatl, full WCAG 2.2 A/AA incl. contrast).

- CI (conformance.yml): axe runs `scripts/axe-gate.mjs --check` (axe-core in jsdom,
  no Chromium, no macOS). color-contrast / target-size deferred to lone's static
  color_contrast check, as before.
- Local: `npm run check:axe` runs the vendored kit gate via tezcatl — verified clean
  over all 7 pages incl. contrast.
- Drop @axe-core/playwright + playwright (unused on this path); keep axe-core + jsdom.
- _gates/_source document the two layers honestly.

Net change vs main is just the re-vendor (kit→dbc2c54: brings the #5 external-grader
criteria — now not-assessed on /conformance — and vendors the kit axe gate for local
use) + pointing check:axe at it. structure.json regenerated for the new rows;
SBOM self-heals (check:sbom green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bdelanghe added a commit that referenced this pull request Jun 29, 2026
… evidence) (#7)

Promotes bd-site's bespoke vuln gate into the kit as a reusable, config-driven gate
(the axe-gate #6 pattern): pure parse/evaluate core (unit-tested over synthetic
npm-audit payloads) + a config-driven CLI + the ck-vuln-gate bin.

Runs `npm audit` over a project's lockfile and FAILS CLOSED when the known
critical/high count exceeds a threshold (default 0). Production-scoped by default
($VULN_OMIT_DEV, since a static site ships no runtime deps); $VULN_THRESHOLD,
$VULN_ROOT, $VULN_REPORT configurable. The report's `vulns: {knownCriticalOrHighVulns}`
envelope is exactly what lone's conformance() consumes for `security.no-critical-vulns`.

Tests: pure parse/threshold logic + a best-effort real `npm audit` e2e (tolerated
skip when offline). node test/run.mjs → 14 passed, 0 failed.

NOTE: the e2e surfaced 1 high advisory in the kit's OWN production deps — a real
supply-chain finding for the kit, tracked separately.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bdelanghe added a commit that referenced this pull request Jun 29, 2026
…mance evidence) (#10)

Third and final gate-promotion (epic prx-em8h). Promotes bd-site's bespoke baseline
gate into the kit, same pattern as vuln (#7), vnu (#9), axe (#6).

Maps the SHIPPED CSS to web-features Baseline data via stylelint-plugin-use-baseline
(headless, no browser) and FAILS CLOSED when the site-wide status is below target.
HONEST: reports the MEASURED status (widely/newly/limited = the worst feature used);
a feature guarded behind an @supports query is a tested fallback and doesn't count.
Config-driven ($BASELINE_CSS/$BASELINE_TARGET/$BASELINE_REPORT); no site coupling.
Emits the `baseline: {status, fallbackTested}` envelope lone's conformance() consumes
for compatibility.baseline.

Unlike vnu/axe, the engine (stylelint) is pure npm, so the fixture e2e is
DETERMINISTIC and runs in CI (no skip). Tests: pure classify/threshold logic + e2e
over good (widely) / bad (:has + ::selection → limited) fixtures. node test/run.mjs → 16/0.

With this, all three bd-site gates are reusable in the kit; bd-site can now consume
them vendored (prx-v5ry), and the kit is ready to publish (prx-47qm).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant