Equall

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

FieldTypeDescription
scorenumberFinal 0–100 score (rounded).
conformance_levelConformanceLevelWCAG conformance against the requested level.
pour_scoresPourScoresPer-principle 0–100 scores; a principle is null when no covered criteria were tested.
issuesEquallIssue[]Active and ignored issues. Ignored ones carry ignored: true.
summaryScanSummaryCounts, criteria tested/failed, ignored count.
scanners_usedScannerInfo[]Which scanners ran, with their version and issue count.
criteria_coveredstring[]Union of WCAG criteria the active scanners cover (e.g. ['1.1.1', '4.1.2']).
criteria_totalnumberTotal WCAG 2.2 criteria for the target level (A: 32, AA: 56, AAA: 86 — 4.1.1 Parsing excluded as obsolete).
scanned_atstringISO-8601 timestamp.
duration_msnumberWall-clock time of the scan.

EquallIssue

Every scanner normalizes its findings to this shape before they reach the report.

FieldTypeDescription
scannerstringSource engine, e.g. 'axe-core', 'eslint-jsx-a11y', 'readability'.
scanner_rule_idstringOriginal rule ID from the source engine.
wcag_criteriastring[]WCAG 2.2 criteria this issue maps to. Empty array = best-practice (not a WCAG violation).
wcag_levelWcagLevel | nullStrictest level among wcag_criteria.
pourPourPrinciple | nullPOUR principle.
file_pathstringPath relative to the scanned root.
linenumber | null1-indexed line when the scanner provides one. axe-core findings are always null here.
columnnumber | null1-indexed column when available.
html_snippetstring | nullOffending HTML element, truncated to 200 chars (axe-core only).
severitySeverityUsed to weight the score.
messagestringHuman-readable description.
help_urlstring | nullLink to documentation on how to fix the issue.
suggestionstring | nullPlain-language fix guidance.
ignoredboolean | undefinedtrue 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:

report.json
{
  "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
}

On this page