Severity Tiers and SLA Tracking for Accessibility Violations
To assign a defensible priority and deadline to an accessibility violation, derive a severity tier from two axes at once — the axe-core impact and the WCAG conformance level of the failed success criterion — then attach a fixed SLA window per tier, compute a due date at first-seen, and detect breaches by comparing the due date to now. This page defines the tier matrix, the window table, and the Python that computes due dates and flags overdue findings.
This is the severity and SLA data model behind the router in remediation routing: violation JSON to issue tracker, within the Enterprise WCAG Audit Architecture & Standards Mapping section. The parent router and the two tracker bindings all import the tier table defined here; this page is its single source of truth, so priority and due dates never get re-derived inconsistently across Jira and GitHub.
When This Applies
Use this model once you are routing findings into a tracker and need every issue to carry a priority and a deadline that survives an audit:
- Two findings with the same axe
impactofseriousneed different priorities because one fails a Level A criterion and the other a Level AAA enhancement. - Leadership asks “how many accessibility issues are past their remediation deadline right now?” and nothing computes a breach.
- The tracker’s priority field is being set by hand per ticket, so it drifts and cannot be defended in a compliance review.
- A
criticalkeyboard trap and aminordecorative-contrast nit are given the same 30-day window because the deadline ignores severity.
If you only need the impact buckets for a CI pass/fail gate, that is a simpler threshold problem handled in the scanning section; this page is about durable per-issue SLAs, not the merge gate.
Minimal Reproducible Example
Mapping axe impact straight to a priority throws away the conformance level, so a AAA enhancement and a Level A blocker at the same impact collapse into one tier.
# NAIVE: impact alone loses the WCAG level, so priority is indefensible.
PRIORITY = {"critical": "P1", "serious": "P2", "moderate": "P3", "minor": "P4"}
def priority(finding):
return PRIORITY[finding["impact"]] # a serious Level-A fail == a serious AAA fail
# Both return "P2" despite very different remediation urgency:
priority({"impact": "serious", "wcag_level": "A"}) # -> "P2"
priority({"impact": "serious", "wcag_level": "AAA"}) # -> "P2"Correct Implementation
Score both axes and combine them into a tier. Impact sets the base; the conformance level shifts it — a Level A failure is escalated, a AAA enhancement is capped — so a serious Level A defect outranks a serious AAA one.
from datetime import datetime, timedelta, timezone
# axe impact -> base rank; higher is more urgent.
IMPACT_RANK = {"critical": 4, "serious": 3, "moderate": 2, "minor": 1}
# WCAG level -> adjustment. Level A defects block core use; AAA are enhancements.
LEVEL_ADJUST = {"A": +1, "AA": 0, "AAA": -1}
# Final tier -> (priority name, SLA window in days). THE source of truth.
TIER = {
"T1-critical": {"priority": "Highest", "sla_days": 3},
"T2-serious": {"priority": "High", "sla_days": 10},
"T3-moderate": {"priority": "Medium", "sla_days": 30},
"T4-minor": {"priority": "Low", "sla_days": 90},
}
_RANK_TO_TIER = {4: "T1-critical", 3: "T2-serious", 2: "T3-moderate", 1: "T4-minor"}
def severity_tier(impact: str, wcag_level: str) -> str:
"""Combine axe impact and WCAG level into one tier."""
score = IMPACT_RANK[impact] + LEVEL_ADJUST[wcag_level]
score = max(1, min(4, score)) # clamp into the 1..4 tier range
return _RANK_TO_TIER[score]
def sla_due(tier: str, first_seen: datetime) -> datetime:
"""Deadline = first-seen timestamp + the tier's window."""
return first_seen + timedelta(days=TIER[tier]["sla_days"])
def is_breached(tier: str, first_seen: datetime, now: datetime | None = None) -> bool:
now = now or datetime.now(timezone.utc)
return now > sla_due(tier, first_seen)
# A serious Level-A failure escalates to T1 (3-day window);
# a serious AAA enhancement de-escalates to T3 (30-day window).
assert severity_tier("serious", "A") == "T1-critical"
assert severity_tier("serious", "AAA") == "T3-moderate"The due date is stamped once, from first_seen, and never recomputed on later updates — otherwise a defect that keeps reproducing would perpetually reset its own deadline and never breach. Breach detection is then a pure comparison against now, cheap enough to run over the whole open backlog on every scan.
The tier-to-window mapping is deliberately explicit and short:
| Tier | Composite source | Priority | SLA window | Typical rules |
|---|---|---|---|---|
T1-critical |
critical impact, or serious on a Level A criterion | Highest | 3 days | keyboard trap, aria-required-attr on core controls |
T2-serious |
serious impact at AA | High | 10 days | color-contrast (1.4.3), link-name (2.4.4) |
T3-moderate |
moderate impact, or serious enhancement at AAA | Medium | 30 days | target-size (2.5.8), heading order |
T4-minor |
minor impact | Low | 90 days | decorative image-alt nits, cosmetic landmarks |
Pipeline Integration Note
Every consumer imports the TIER table rather than restating it. The parent remediation routing reconciler calls severity_tier when it first sees a finding and passes the resulting tier into the create call, where the Jira binding maps it to a Jira priority and duedate, and the GitHub binding maps it to a sev:* label and an SLA line in the body. The WCAG level feeding this model comes from the criterion taxonomy in the parent section; the impact comes from the engine’s own classification, set in the scanning configuration. Because the due date is stamped at creation and carried on the issue, a scheduled job can sweep the open backlog, call is_breached per issue, and emit the overdue count straight into a reporting dashboard without re-scanning anything.
Gotchas
- Multi-tenant and auth-gated routes can inflate impact. The same rule can be flagged
seriouson a public route andcriticalon an authenticated flow where it blocks a checkout. Tier on the route’s business criticality where you have it, not just the raw impact, so a blocker on a revenue path is not capped at the same window as a marketing page. - First-seen must be immutable, or SLAs silently extend. If
first_seenis refreshed each run instead of frozen at the first occurrence, a chronically unfixed defect resets its clock nightly and never breaches. Persist the original timestamp on the issue and read it back; never recompute it from the current run. - Clock and timezone skew flips breach status at the boundary. Comparing a naive local
first_seenagainst a UTCnowcan mark an issue breached or clear hours early. Store every timestamp as timezone-aware UTC end to end, as the examples do, so breach detection is deterministic across runners and regions.
Frequently Asked Questions
Why combine axe impact with the WCAG level instead of using impact alone?
Impact measures how badly a specific element is broken; the conformance level measures how essential the criterion is to basic access. A serious-impact failure of a Level A criterion blocks core use and deserves the tightest window, while the same impact on a AAA enhancement does not. Using one axis discards the other and produces priorities you cannot defend in a review.
Should the SLA due date update when a violation keeps reproducing?
No. Stamp the due date once from the first-seen timestamp and leave it fixed. If you recompute it each run, a defect that reproduces nightly perpetually resets its own deadline and can never breach, which defeats the entire point of an SLA. Track occurrence count separately if you want to show persistence.
How do I detect which open issues are past their remediation deadline?
Read the stamped due date off each open issue and compare it to the current UTC time; anything past due is breached. A scheduled job can sweep the whole open backlog with that pure comparison every scan and emit the overdue count to a dashboard without re-running any browser scan, since the deadline already lives on the issue.
Can I use different SLA windows for different products or teams?
Yes, but keep the tier definition central and layer overrides on top rather than forking the table. Resolve the base tier from impact and level first, then apply a per-team window multiplier or floor in one place so the composite logic stays auditable. Scattering bespoke day counts across trackers is exactly the drift this model exists to prevent.