@misar/a11y-guards (0.1.1)
Installation
@misar:registry=npm install @misar/a11y-guards@0.1.1"@misar/a11y-guards": "0.1.1"About this package
@misar/a11y-guards
Real-browser WCAG contrast auditor for the Misar AI monorepo. Sibling to
@misar/guards and @misar/seo-guards in
naming and CLI shape, but not zero-dependency — contrast cannot be
measured from source text alone, so this drives a real headless Chromium via
Playwright (the same way this org's e2e.yml workflows already do) and reads
actual composited colors through axe-core's
color-contrast rule.
Why a separate package instead of a new @misar/guards guard
@misar/guards' guards are all static source scans — fast, dependency-free,
and correct for what they check (a banned import, a hardcoded secret, a
banned color family). Contrast is a different kind of question: the same
text-primary class renders at a passing ratio on one background and a
failing one on another, a background image or a gradient behind text can't
be evaluated from a class name at all, and a hover/focus state's color only
exists once something is actually rendered. Answering "does this actually
pass WCAG" requires a real browser computing real composited pixels — axe-core
is the industry-standard tool for exactly that, and bundling a browser
dependency into the zero-dependency guard family would have changed what
that family's whole design promise means for every other guard in it.
What it checks
color-contrast only, by default — not the full axe-core ruleset. Running
every axe rule against a repo on day one surfaces a flood of pre-existing,
unrelated findings (landmarks, ARIA, alt text) that have nothing to do with
what this package exists to check. Widen rules in your config if you want
more; the CLI, report format and --fix machinery all already generalize to
any axe rule id.
The CLI contract
misar-a11y-guards [--advisory] [--fix] [--only=<ids>] [--json] [--config=<path>]
- Reads
a11y-guards.config.jsonfrom the repo root. - Detection mode (default): boots each configured route in headless
Chromium, runs axe-core, reports violations.
- Exit 0 — no findings (or
--advisory). - Exit 1 — findings, blocking mode.
- Exit 2 — usage/config error, or no routes configured.
- Exit 0 — no findings (or
- Fix mode (
--fix): rewrites source in place using known-safe token substitutions (see "How fixing works" below) — does not touch the live audit at all.--only=<ids>restricts to specific fix ids;--listshows the built-in ones. --json: machine-readable output for both modes.
Adopt in a repo
Same three-step shape as @misar/guards (see misar-io's
docs/DEVOPS/guards-ci.md):
- Copy
misar-io/.forgejo/workflows/a11y-guards.yml(the audit — runs on every PR) into your repo's.forgejo/workflows/. Copya11y-guards-fix.ymltoo if you also want the scheduled bot-PR auto-fix; it's a separate file on purpose (see its own header comment) and is optional — the audit works standalone without it. - Copy
a11y-guards.config.example.jsonto your repo root asa11y-guards.config.json, and list your public, unauthenticated routes — marketing/landing pages, pricing, anywhere a real visitor or crawler reads text on a color without signing in first. Deliberately scoped to public pages: authenticated app surfaces differ per repo (SSO flows, seeded test accounts, per-product auth) in ways a single shared workflow cannot safely generalize across ten differently-built products. - Ensure the
NPM_TOKENsecret exists (same one@misar/guardsand the publish workflow use).
The workflow boots your app (pnpm run build && pnpm run start, or your
repo's equivalent — see the template) against baseUrl, then runs
misar-a11y-guards against it. Advisory by default; flip to blocking once a
repo is clean, exactly like the @misar/guards ramp.
How fixing works — and what it deliberately does NOT do
A tempting auto-fix is to programmatically darken/lighten the offending color
until the ratio clears (walk lightness in HSL, write the result back). This
package does not do that: the result is a color nobody chose, drifts hue on
the sRGB↔HSL round-trip, and produces a different wrong color on every
component it touches. Rejected on purpose — see src/lib/known-fixes.mjs's
header for the full reasoning.
Instead, --fix applies known, deterministic token substitutions —
replacing a source pattern already proven to fail contrast in a given role
with a token from the same design system already proven to pass in that
role. The built-in default targets text-primary (raw brand green as text,
~2.0–2.2:1) → text-[var(--color-primary-text)] (the readable variant
@misar/ui already ships, 5.01:1 light / 11.29:1 dark) — the exact fix
already applied by hand in MisarAI's marketing pages. A repo extends this via
its config's fixes array with its own project-specific pairs; nothing about
the mechanism is Tailwind- or @misar/ui-specific, only the seeded defaults
are.
When no known-safe substitution exists for a violation, --fix does not
invent one — that violation is left for a human to actually look at and
decide, and is called out as such rather than silently "improved" by an
algorithm.
The bot-PR flow
--fix only rewrites files on disk; it does not commit or open anything.
The CI workflow wraps it: on a scheduled run (or manually dispatched), it
runs --fix, and if anything changed, commits on a new branch and opens a
PR via the Forgejo API — the same "a bot proposes, a human reviews and
merges" pattern renovate.yml already uses in every one of these repos for
dependency bumps. It never pushes directly to a PR someone has open; that
would risk colliding with in-progress work.
Config schema
{
// Ramp mode. false (blocking) is the default — the opposite of
// @misar/guards, whose bare-metal default IS advisory. A silently-advisory
// contrast check that nobody looks at is worse than no check at all, so a
// repo opts INTO advisory explicitly rather than getting it for free.
"advisory": true,
// Every entry in "routes" resolves against this.
"baseUrl": "http://localhost:3000",
// Public, unauthenticated routes only.
"routes": ["/", "/pricing"],
// axe-core rule ids to run. Default ["color-contrast"] if omitted.
"rules": ["color-contrast"],
// Repo-specific known-fixes, merged with (and able to override, by id)
// the built-ins in src/lib/known-fixes.mjs.
"fixes": [
{
"id": "my-repo-specific-fix",
"description": "...",
"pattern": "text-someRiskyToken\\b",
"replacement": "text-[var(--some-safe-token)]"
}
],
// Repo-relative path prefixes --fix never rewrites.
"ignore": ["packages/a11y-guards"]
}
Dependencies
Dependencies
| ID | Version |
|---|---|
| axe-core | ^4.10.0 |
| playwright-core | ^1.48.0 |