@misar/a11y-guards (0.1.2)

Published 2026-10-16 17:08:01 +00:00 by misaradmin

Installation

@misar:registry=
npm install @misar/a11y-guards@0.1.2
"@misar/a11y-guards": "0.1.2"

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.json from 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.
  • 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; --list shows 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):

  1. Copy misar-io/.forgejo/workflows/a11y-guards.yml (the audit — runs on every PR) into your repo's .forgejo/workflows/. Copy a11y-guards-fix.yml too 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.
  2. Copy a11y-guards.config.example.json to your repo root as a11y-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.
  3. Ensure the NPM_TOKEN secret exists (same one @misar/guards and 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

Keywords

misar ci accessibility a11y wcag contrast axe-core guards
Details
npm
2026-10-16 17:08:01 +00:00
88
latest
16 KiB
Assets (1)
Versions (3) View all
0.1.2 2026-10-16
0.1.1 2026-10-16
0.1.0 2026-09-15