Output format
Canonical JSON contract returned by runScan() and emitted by equall scan --json. The shape CI integrations and SDK consumers should code against.
This page documents the ScanResult document — the same shape whether it's emitted by equall scan . --json (the CLI calls JSON.stringify(result, null, 2) on it) or returned by runScan(). CI integrations and SDK consumers should treat this as the canonical contract.
When --json is passed, the JSON document is written to stdout and a one-line confirmation is written to stderr (✓ JSON report written (N issues)). Redirect the report cleanly with > report.json while still seeing progress in your terminal.
ScanResult
| Field | Type | Description |
|---|---|---|
score | number | Final 0–100 score (rounded). |
conformance_level | ConformanceLevel | WCAG conformance against the requested level. |
pour_scores | PourScores | Per-principle 0–100 scores; a principle is null when no covered criteria were tested. |
issues | EquallIssue[] | Active and ignored issues. Ignored ones carry ignored: true. |
summary | ScanSummary | Counts, criteria tested/failed, ignored count. |
scanners_used | ScannerInfo[] | Which scanners ran, with their version and issue count. |
criteria_covered | string[] | Union of WCAG criteria the active scanners cover (e.g. ['1.1.1', '4.1.2']). |
criteria_total | number | Total WCAG 2.2 criteria for the target level (A: 32, AA: 56, AAA: 86 — 4.1.1 Parsing excluded as obsolete). |
scanned_at | string | ISO-8601 timestamp. |
duration_ms | number | Wall-clock time of the scan. |
EquallIssue
Every scanner normalizes its findings to this shape before they reach the report.
| Field | Type | Description |
|---|---|---|
scanner | string | Source engine, e.g. 'axe-core', 'eslint-jsx-a11y', 'readability'. |
scanner_rule_id | string | Original rule ID from the source engine. |
wcag_criteria | string[] | WCAG 2.2 criteria this issue maps to. Empty array = best-practice (not a WCAG violation). |
wcag_level | WcagLevel | null | Strictest level among wcag_criteria. |
pour | PourPrinciple | null | POUR principle. |
file_path | string | Path relative to the scanned root. |
line | number | null | 1-indexed line when the scanner provides one. axe-core findings are always null here. |
column | number | null | 1-indexed column when available. |
html_snippet | string | null | Offending HTML element, truncated to 200 chars (axe-core only). |
severity | Severity | Used to weight the score. |
message | string | Human-readable description. |
help_url | string | null | Link to documentation on how to fix the issue. |
suggestion | string | null | Plain-language fix guidance. |
ignored | boolean | undefined | true when suppressed by an equall-ignore comment. Omitted otherwise. |
PourScores
interface PourScores {
perceivable: number | null // null when no criteria of that principle were tested
operable: number | null
understandable: number | null
robust: number | null
}ScanSummary
interface ScanSummary {
files_scanned: number
total_issues: number
by_severity: Record<Severity, number>
by_scanner: Record<string, number> // keyed by scanner.name
criteria_tested: string[] // WCAG criteria evaluated
criteria_failed: string[] // WCAG criteria with at least one violation
ignored_count: number // Issues suppressed via equall-ignore
}ScannerInfo
interface ScannerInfo {
name: string
version: string
rules_count: number
issues_found: number
}rules_count is currently always 0 — the field is reserved for a future enhancement and should be treated as informational only. See Known issues.
Enum-ish unions
type ConformanceLevel = 'AAA' | 'AA' | 'A' | 'Partial A' | 'None'
type Severity = 'critical' | 'serious' | 'moderate' | 'minor'
type WcagLevel = 'A' | 'AA' | 'AAA'
type PourPrinciple = 'perceivable' | 'operable' | 'understandable' | 'robust''Partial A' means at least one Level A criterion is failing — the report is "not yet conformant, fix Level A first". 'None' is reserved for runs that fail every level checked.
Example payload
A truncated real-shaped output:
{
"score": 56,
"conformance_level": "Partial A",
"pour_scores": {
"perceivable": 89,
"operable": 76,
"understandable": null,
"robust": 85
},
"issues": [
{
"scanner": "axe-core",
"scanner_rule_id": "image-alt",
"wcag_criteria": ["1.1.1"],
"wcag_level": "A",
"pour": "perceivable",
"file_path": "src/components/Logo.tsx",
"line": null,
"column": null,
"html_snippet": "<img src=\"/logo.svg\">",
"severity": "critical",
"message": "Images must have alternate text",
"help_url": "https://dequeuniversity.com/rules/axe/4.11/image-alt",
"suggestion": "Add an alt attribute describing the image, or alt=\"\" if it's decorative."
}
],
"summary": {
"files_scanned": 33,
"total_issues": 34,
"by_severity": { "critical": 2, "serious": 11, "moderate": 19, "minor": 0 },
"by_scanner": { "axe-core": 23, "eslint-jsx-a11y": 13 },
"criteria_tested": ["1.1.1", "1.3.1", "2.4.4", "4.1.2"],
"criteria_failed": ["1.1.1", "4.1.2"],
"ignored_count": 2
},
"scanners_used": [
{ "name": "axe-core", "version": "4.11.1", "rules_count": 0, "issues_found": 23 },
{ "name": "eslint-jsx-a11y", "version": "6.10.2", "rules_count": 0, "issues_found": 13 }
],
"criteria_covered": ["1.1.1", "1.3.1", "2.4.4", "4.1.2"],
"criteria_total": 56,
"scanned_at": "2026-04-26T10:32:11.482Z",
"duration_ms": 812
}