Output schema
This is the exact shape of the object returned by runDomRulesInPage(...) / runa11yCoreInPage(...) (see INTEGRATION.md for which one to call). Every example on this page is real output from the current engine (schemaVersion: "1.0.0"), not hand-written. runa11yCoreAcrossFrames returns a different, recursive shape wrapping this one — see Cross-frame result below.
Top-level result
{engine: {tag: string,schemaVersion: string,locale: { requested: string, resolved: string, reason: string },wcagVersion: "2.0" | "2.1" | "2.2"},url: string | null,title: string | null,timestamp: string | null,perfStats: object | null,contextSelector: string | string[] | null,checksResults: CheckResult[],rulesResults: CompositeResult[],overriddenBuiltinIds: string[]}
| Field | Meaning |
|---|---|
Cross-frame result (runa11yCoreAcrossFrames)
runa11yCoreAcrossFrames (see INTEGRATION.md) returns a different, recursive shape instead of a plain top-level result:
{topFrame: <the normal top-level result shape above>,frames: Array<| { url: string | null, topFrame: <top-level result>, frames: [...same shape, recursively] }| { url: string | null, error: string }>}
topFrameis exactly the top-level result shape, for the frame the function was called in.frameshas one entry per direct child<iframe>/<frame>in the scanned scope. A reachable child (one that calleda11yCoreEnableFrameResponder()) contributes its own complete{ url, topFrame, frames }— including its own nestedframes, recursively, since a further-nested grandchild is only reachable through its immediate parent. An unreachable child (the common case for most third-party embeds — no cooperating responder, or it timed out) contributes{ url, error }instead, and does not abort the rest of the scan.- This is a tree, not a flat list — a deliberate difference from the
@surea11y/playwrightbinding's.frames(true), which can flatten because Playwright'spage.frames()already gives every frame regardless of nesting depth; apostMessagerelay has no such global view, so nesting is expressed structurally instead.
A check result (checksResults[i])
{ruleId: string,outcome: "pass" | "fail" | "cantTell" | "notApplicable",outcomeNormalized: "pass" | "fail" | "cantTell" | "inapplicable",severity: "minor" | "moderate" | "serious" | "critical",confidence: "high" | "medium" | "low",type: "automatic" | "manual",occurrences: Occurrence[],title: string,description: string,i18n: { titleKey: string, descriptionKey: string } | null,meta: {ruleId: string,ruleInterfaceVersion: string,ruleVersion: string,normative: boolean,atomic: boolean,deprecated: boolean,deprecation: object | null,category: "perceivable" | "operable" | "understandable" | "robust" | null,normativeMappings: Array<{ standard: string, version: string, requirement: string, title: string, conformanceLevel: string }>,standard: string | null,applicability: string,expectation: string,references: string[],requirements: object | null,mappings: object | null},engineOptions: object, // the resolved engineOptions this rule actually ran underschemaVersion: string,wcagVersionScope?: { // present only when the target WCAG version changed this outcometarget: "2.0" | "2.1" | "2.2",removedSc: string[],coercedFrom: "fail"},error?: string // present only if the rule threw — see below}
Notes:
outcomevsoutcomeNormalized: identical exceptnotApplicablebecomes"inapplicable"inoutcomeNormalized. Both are provided so you can match either your own vocabulary or the engine's internal one.type: "manual"rules can never reportoutcome: "fail". If a manual rule's own logic would have saidfail, the engine coerces it tocantTelland appends an explanatory note toerror— this is enforced centrally (policy.coerceManualFailToCantTell, on by default under thea11ypolicy contract; seePOLICY.md), not something each rule has to remember.failis reserved for deterministic,type: "automatic"findings only.meta.normativeMappingsis how a check result ties back to a WCAG Success Criterion —[]for rules with no formal WCAG mapping (this engine calls them advisorytype: "manual"rules). See Conformance for how these roll up.wcagVersionScope: only present when the run's target WCAG version turned this rule'sfailinto acantTell— today that means a rule mapped to SC 4.1.1 Parsing (duplicate-id) under the default 2.2 target, since 2.2 removed that criterion.removedSclists the criteria that stopped existing,targetis the version that removed them, andcoercedFromis the outcome the rule itself reported. The occurrences are the rule's own, unchanged — nothing was dropped, only the conformance verdict was. Absent on every other result, and never reported througherror: nothing went wrong. See Filtering by WCAG version.error: only present in three cases — the rule implementation threw an uncaught exception, the manual-fail coercion above fired, or every occurrence of afailresult had its ancestor walk hit an internal depth limit (so the engine couldn't confirm the flagged content is actually exposed) and the whole result was coerced tocantTellthe same way. A thrown rule always surfaces asoutcome: "cantTell"withoccurrences: []anderrorset to the exception message — the engine never lets one broken rule crash the whole scan.engineOptionson each result is the resolved options object (after locale/contrast defaults were applied), not literally what you passed in — useful for confirming what a given rule actually saw, especially the resolvedlocaleandcontrast.mode/contrast.rootCanvasFallback.
An occurrence (occurrences[i])
Normally present only when outcome is fail or cantTell: a pass result has occurrences: [], since this engine does not enumerate the elements it passed, only the ones it flagged.
notApplicable is the one exception. A rule that had nothing to judge may attach a single occurrence saying why, and the contrast rules do exactly that when no text had a computable background — the difference between "checked, nothing to flag" and "could not check" is one this engine reports rather than hides. Such an occurrence describes the scan, not an element, so its selector is empty. Do not read occurrences.length as a violation count without checking outcome first.
{selector: string,html: string,structuralPath: number[] | null,summary: string,hint: string,i18n: { summaryKey: string, hintKey: string, params: object } | null,occurrenceOutcome?: "fail" | "cantTell", // present when the rule graded its findings into tiersuncertainty?: { // present only on a cantTell-tier occurrencecode: "not-computable" | "runtime-dependent" | "spec-only"| "equivalence-unknown" | "judgement-required" | "out-of-scope",needed?: string, // what would settle the questionevidence?: object // what the rule did establish, rule-specific},data: {visibilityFilter?: { eligible: boolean, targetSet: string, accEligible: boolean | null, reasons: string[] },details?: object // rule-specific, non-normative — see below}}
| Field | Meaning |
|---|---|
Uncertainty codes
A cantTell says the engine did not decide. uncertainty says why, from a closed vocabulary, so a consumer can branch on the reason rather than parse a summary string. It is present only on a cantTell-tier occurrence: a fail-tier one would be claiming the rule both decided and did not, so the engine drops it.
| code | Meaning | Typical shape |
|---|---|---|
needed states, in one sentence, what would settle the question — the thing a reviewer has to go and check. evidence carries what the rule did establish, so the reviewer starts from the engine's work rather than repeating it; its shape is rule-specific and, like data.details, not a stable contract. The code is: new codes may be added in a minor release, but an existing one does not change meaning, so branch on the codes you know and treat an unrecognised one as "needs review" rather than an error.
Every automatic rule that can report cantTell carries this, and a test holds that line so a new one cannot arrive without it. The out-of-scope code is attached by the engine rather than by a rule, on the same occurrences that produce a result-level wcagVersionScope. Manual rules do not carry it: judgement-required is what type: "manual" already means, so repeating it per occurrence would say nothing the result does not.
A composite result (rulesResults[i])
Composites roll multiple atomic rules up to one WCAG Success Criterion (e.g. wcag-1.1.1-non-text-content rolls up 21 atomic rules). Shape is the same envelope as a check result, with composite-specific data.details:
{ruleId: string, // e.g. "wcag-1.1.1-non-text-content"outcome: "pass" | "fail" | "cantTell" | "notApplicable",severity, confidence, type, title, description, meta, engineOptions, schemaVersion, // same as a check resultoccurrences: [], // always empty — composites are rollups, not element-level findingsdata: {details: {reasonCode: string, // e.g. "composite.rollup.fail.anyFail"checksIds: string[], // every atomic ruleId this composite rolls upcontributors: Array<{ testId: string, outcome: string, severity: string | null }>,metrics: { failCount, cantTellCount, notApplicableCount, passCount, missingCount }}}}
Rollup precedence (deterministic, in this order): any contributor fail → composite fail; else any cantTell (or a contributor rule that didn't run at all, missingCount > 0) → composite cantTell; else all contributors notApplicable → composite notApplicable; else pass. See Conformance for what this means for an overall conformance claim.
Outcome values
| Outcome | Meaning | Can appear on type: "manual"? |
|---|---|---|
fail is intentionally the narrowest, highest-bar outcome in this engine: reserved for deterministic, normative violations; chasing rule coverage must never dilute this.
Severity and confidence values
severity:minor<moderate<serious<critical— the rule author's assessment of user impact, independent ofconfidence.confidence:low<medium<high— how certain the engine is that afail/cantTellverdict is correct. Both are informational metadata for prioritization; neither changesoutcome's meaning.
A fail is not always confidence: "high", and that is not a contradiction. The outcome describes the decision procedure — it resolved the question without guessing — while confidence describes the model that decision was made against. A handful of automatic rules decide deterministically against something that is itself an approximation (the curated WAI-ARIA role tables, the native-role mappings, an accessibility tree inferred from static markup) and report medium: aria-required-children, aria-prohibited-children, aria-required-parent, aria-allowed-attr, form-control-programmatic-label-present, identical-iframes-same-purpose, svg-image-text-alternative-present, video-poster-text-alternative-present and target-size-minimum. confidence is on every result, so a consumer that wants only the most certain failures can gate on it directly; policy.allowedConfidence will not do it for you, since a disallowed value is replaced with the rule's own defaultConfidence rather than changing the outcome (see POLICY.md).
Worked example
Scanning <img src="logo.png"> (no alt) and <button></button> (no accessible name), scoped to just those two rules via runOnly: { includeRuleIds: [...] } (see Engine options — this is not a bare array):
const result = runDomRulesInPage('https://example.test/',null,{},{ includeRuleIds: ['img-alt-present', 'button-name-present'] });
{"engine": {"tag": "a11ycore","schemaVersion": "1.0.0","locale": { "requested": "en", "resolved": "en", "reason": "ok" }},"url": "https://example.test/","title": "Example","timestamp": null,"perfStats": null,"contextSelector": null,"checksResults": [{"ruleId": "button-name-present","outcome": "fail","severity": "serious","confidence": "high","type": "automatic","occurrences": [{"selector": "html > body > button","html": "<button></button>","structuralPath": [1, 1],"summary": "This button has no accessible name.","hint": "Provide visible button text or a programmatic accessible-name mechanism (for example aria-label) so assistive technologies can identify the button.","data": {"visibilityFilter": { "eligible": true, "reasons": [], "targetSet": "acc", "accEligible": true },"details": { "reasonCode": "name_missing" }}}]},{"ruleId": "img-alt-present","outcome": "fail","severity": "serious","confidence": "high","type": "automatic","occurrences": [{"selector": "html > body > img","html": "<img src=\"logo.png\">","structuralPath": [1, 0],"summary": "Missing alt attribute on <img>.","hint": "Add an alt attribute (use alt=\"\" only for decorative images)."}]}],"rulesResults": [],"overriddenBuiltinIds": []}
(Trimmed for readability — the real result also includes title/description/i18n/meta/engineOptions/schemaVersion on every entry, per the full shape above. rulesResults is empty here because runOnly.includeRuleIds scoped the scan to two atomic rules and no composite's own ID was included.)