Equall

Known issues

Deliberate limitations and trade-offs in the current Equall release.

This page lists the deliberate limitations of the CLI as it stands today — design decisions that are accurate to the current version, plus a few active TODOs with user-visible impact. These aren't bugs we'll fix on report; they're intentional trade-offs we've made (usually to keep v0 lean) and want to be upfront about.

If you hit something that's not listed here, please file it on the issue tracker.

Most items below have a planned resolution path. Where one exists, it's noted at the end of the entry.

Scanning coverage

No browser-based checks

The CLI runs scanners statically — axe-core is fed an HTML fragment via jsdom, never a real browser. That means runtime-only checks don't run, including colour-contrast (resolved CSS), keyboard-trap detection, focus-order verification, and any rule that needs to observe a rendered, interactive page. The report says so: contrast (1.4.3), use of color (1.4.1) and target size (2.5.8) read "Not evaluated — assisted", never "Supports (automated)".

JSX/TSX is parsed by ESLint, not by axe-core

eslint-plugin-jsx-a11y covers JSX/TSX with a real AST via @typescript-eslint/parser, and that's what produces line numbers on issues for those files. axe-core runs on the static HTML it can extract — directly from .html, or pulled out of JSX/TSX/Vue/Svelte/Astro markup — never a mounted component tree. Anything that requires the component to actually mount (computed aria-* from a hook, dynamically injected children) is out of reach for the static path. When an axe-core finding depends on such runtime values, it is reported for review instead of counted, so a real defect hidden behind props shows up in the "To review" list, not in the score.

eslint-plugin-jsx-a11y does not run on .vue or .svelte files: their templates are checked by axe-core on the extracted markup only.

Readability scanner skips JSX/TSX (and non-English documents)

The Flesch-Kincaid / WCAG 3.1.5 scanner runs on .html, .vue, and .astro files. JSX/TSX are skipped on purpose: regex-based text extraction would capture className strings and {expression} fragments that pollute the grade. A real JSX parser would fix this, but it isn't a priority for v1.

The scanner also skips:

  • Files with fewer than 30 words — readability formulas are statistically invalid on short text.
  • Documents whose <html lang="…"> is set to a non-English language — every formula in the bundle is English-calibrated, so running them on French or German text produces meaningless scores.

You can disable the scanner entirely with --no-readability if you're not shipping English content. See Scanning for the flag.

Dynamic attribute values aren't statically asserted

In .astro, JSX, and Vue, attribute values written as expressions (aria-label={label}, class={cls}, aria-selected={i === 0}) are neutralised before axe sees them — replaced with a placeholder or stripped — so they don't produce phantom violations. The trade-off is that their runtime values can't be checked statically: a dynamic accessible name is treated as "present, value unverified", not asserted. Resolving them would take a rendered check, which the CLI does not perform.

Cross-engine merging is conservative

When axe-core and eslint-plugin-jsx-a11y flag the same defect, the two findings are merged only when the match is unambiguous (one occurrence of each in the file). When a file holds several occurrences, both findings are kept, so the same defect can be counted twice. Over-counting is visible; dropping a real finding would not be.

Route detection reads conventions, not config

routes is derived from file paths. A custom Next.js pageExtensions or a custom Astro srcDir is not read, SvelteKit and Nuxt routes are not mapped (the scan says so on diagnostics), and in-memory scans skip route detection. Detection is anchored to the scanned root: in a monorepo, scan the package directory.

Diff mode (--diff)

Identical copies added to one file count once

The diff compares occurrences per fingerprint, so a copy of an existing violation reads as new. But identical copies added to the same file fold into one new finding, as they do in a full scan.

Moved code counts as new

Renames are not tracked and the comparison is per file path: code moved to another file, or a renamed file, is reported as new in its new location.

Whole files, not changed lines

Each changed file is compared with its base version as a whole. A violation that already existed in the file stays legacy even if the change moved or reformatted it within the file; a violation that appears in the file is new even if it sits outside the edited lines. Editing the offending element itself changes its identity, so it then reads as new.

Output

ScannerInfo.rules_count is always 0

Every entry in scanners_used[] reports rules_count: 0, regardless of how many rules the scanner actually runs. The field is reserved for a future enhancement (per-scanner rule counts) but is currently hardcoded in src/scan.ts. Treat it as informational only — the meaningful field is issues_found. See Output format.

Issues from axe-core have no line numbers

axe-core reports against a parsed DOM tree, not the source file, so line and column are always null on its findings. The html_snippet field carries the offending element (truncated to 200 chars) — that's how to locate the issue in your file. JSX/TSX issues from eslint-plugin-jsx-a11y do carry line and column numbers.

Performance

No incremental or cached scans

Each equall scan re-discovers files, re-runs every scanner, and re-computes the score from scratch. There's no on-disk cache and no per-commit memoization. Identical inputs produce identical results, but they still take the same wall-clock time.

A diff-aware "only-new" mode exists: equall scan --diff (see CI) or the programmatic runDiffScan API. It scans only the changed files, twice each (at the change and at its base).

For large repos this rarely matters in practice (typical scans are sub-second to a few seconds), but if you scan 500+ files in a tight CI loop you'll feel it.

JSDOM is not pooled

The axe-core scanner instantiates a fresh JSDOM per HTML file. For projects with hundreds of HTML files this becomes a measurable cost — a worker-pool optimization is tracked internally but not yet implemented.

Tooling integration

ESM-only

equall-cli ships a single ESM build. CommonJS consumers must use a dynamic await import('equall-cli') rather than require(). The minimum supported runtime is Node ≥ 20.

Single workspace, single project per run

The CLI scans one project root per invocation. There is no built-in monorepo mode, no multi-root configuration, and no way to merge results across runs into a single report. If you have a monorepo, run equall scan once per package and aggregate the JSON yourself.

No config file

Configuration today happens entirely through CLI flags and equall-ignore comments. There is no .equallrc or package.json block. This is a deliberate "zero config" stance for v0 — a config file (with custom severity weights, per-rule overrides, project-scoped excludes) is something we'll consider once the rule set stabilizes.

Reporting something that isn't here

If you find behaviour that looks like a bug rather than one of the deliberate trade-offs above — wrong WCAG mapping, scanner crash, score that doesn't match the reported issues — please open an issue on github.com/GotaBird/equall/issues. Include the command you ran, the project type, and ideally the --json output.

On this page