Mapping Violations to Component Owners
To derive the team that owns an accessibility violation, resolve the finding’s DOM selector and route against a version-controlled component ownership map — a most-specific-first table of path globs, data-owner attributes, and CODEOWNERS-style entries — so that a color-contrast failure on .checkout .pay-button lands on the checkout team’s board, not in an unattributed queue. This page is the owner-resolution mechanism inside the broader remediation ticket routing workflow, itself part of the accessibility compliance baseline for enterprise web ops; it covers only how a finding becomes an owning team, not what happens to the ticket afterward.
When This Applies
Reach for a resolver the moment scan output must be assigned automatically rather than triaged by a human reading URLs. The conditions are specific:
- Scans span multiple product teams sharing one deployment, so “the accessibility team” is not a valid assignee.
- The same design-system component renders across dozens of routes owned by different teams, and you need the component’s owner, not the route’s owner.
- Findings arrive with a CSS
selectorand aroutebut no team attribution, and a human currently maps them by hand. - Ownership changes as teams reorganize, so the mapping must live in version control and be reviewed like code rather than hard-coded in the routing script.
If instead every violation on a given host belongs to a single team, you do not need a resolver — a static assignee on the tracker client is enough. The resolver earns its complexity only when ownership varies within a page.
Minimal Reproducible Example
The naive approach maps ownership by route prefix alone. It works until a shared component fails, at which point it assigns the defect to whichever team happens to own the page it was first seen on rather than the team that owns the component.
# resolve_naive.py — route-prefix ownership only. Misroutes shared components.
ROUTE_OWNERS = {
"/checkout": "team-checkout",
"/account": "team-account",
"/": "team-web",
}
def owner_for(finding):
route = finding["route"]
for prefix, team in ROUTE_OWNERS.items():
if route.startswith(prefix):
return team
return "team-web"
# A design-system button fails contrast on /checkout AND /account.
print(owner_for({"route": "/checkout", "selector": ".ds-button"})) # team-checkout
print(owner_for({"route": "/account", "selector": ".ds-button"})) # team-accountThe same defect — one broken token in the shared button component — is now assigned to two different feature teams, neither of whom owns the button. Both will bounce it. Route prefix answers “where did this render?”, but ownership routing needs “who owns this DOM subtree?”, and those are different questions whenever a component is shared.
Correct Implementation
A correct resolver matches on the selector first and the route second, evaluates rules most-specific-first, and supports three matching strategies layered by precedence: an explicit data-owner attribute captured during the scan (highest trust), a selector glob, then a route prefix (lowest). It always returns an owner — the triage sentinel — so a gap in the map never drops a finding.
import fnmatch
from dataclasses import dataclass
@dataclass(frozen=True)
class OwnerRule:
team: str
assignee: str
channel: str
selector_glob: str | None = None # e.g. "*.ds-button*" or ".checkout *"
route_prefix: str | None = None # e.g. "/checkout"
specificity: int = 0 # higher wins; usually len of the glob
TRIAGE = OwnerRule("a11y-triage", "a11y-guild", "#a11y-triage")
def resolve_owner(finding: dict, rules: list[OwnerRule]) -> OwnerRule:
"""Derive the owning team from a finding's selector and route.
Precedence, highest first:
1. data-owner attribute captured on the element during the scan
2. selector glob match (component ownership)
3. route prefix match (page ownership)
Rules are pre-sorted most-specific-first so the first match wins.
"""
# 1. An explicit data-owner beats every heuristic — the team labelled
# the component itself, so trust it unconditionally.
explicit = finding.get("data_owner")
if explicit:
for r in rules:
if r.team == explicit:
return r
selector = finding.get("selector", "")
route = finding.get("route", "")
# 2 + 3. Walk most-specific-first; selector rules sort above route rules
# because component ownership is more precise than page ownership.
for r in rules:
if r.selector_glob and fnmatch.fnmatch(selector, r.selector_glob):
return r
if r.route_prefix and route.startswith(r.route_prefix):
return r
return TRIAGE # never None: an unowned finding is still accountable
def load_rules(raw: list[dict]) -> list[OwnerRule]:
"""Build the rule list and enforce most-specific-first ordering."""
rules = [OwnerRule(**r) for r in raw]
# Selector rules before route rules; within each, longer pattern first.
return sorted(
rules,
key=lambda r: (
r.selector_glob is None, # selector rules first
-len(r.selector_glob or r.route_prefix or ""), # longer = more specific
),
)The ownership map itself is committed data, reviewed on every reorg. Capturing data-owner during the scan — reading a data-owner="team-checkout" attribute the component renders — gives the highest-trust signal because the owning team declared it in their own markup:
# ownership-map.yaml — reviewed like CODEOWNERS, shipped with the app repo.
- team: team-checkout
assignee: checkout-lead
channel: "#checkout-a11y"
selector_glob: "*.checkout *" # anything inside the checkout container
- team: design-system
assignee: ds-oncall
channel: "#ds-a11y"
selector_glob: "*.ds-*" # shared component library — owns the token bug
- team: team-account
assignee: account-lead
channel: "#account-a11y"
route_prefix: "/account"With this map, the shared .ds-button resolves to design-system regardless of which route it failed on, because the selector rule for .ds-* is more specific than any route prefix and is evaluated first. The design-system team fixes the token once; the feature teams never see a ticket they cannot action.
Pipeline Integration Note
The resolver is the first stage of the routing pipeline: its output — an owning team, default assignee, and notification channel — feeds directly into the dedupe fingerprint and the severity gate described in remediation ticket routing. Because the owning team is part of the dedupe key, resolving ownership before deduping is what lets the same component defect collapse to one ticket on the right team’s board. The data-owner attribute this resolver trusts is captured during the scan itself, so the error categorization and triage pipelines that normalize the finding must preserve that attribute rather than strip it. Downstream, the resolved channel is what the notification fan-out uses to alert the right team without a broadcast.
Gotchas
- Authenticated shells hide the ownership boundary. A component that renders
data-owneronly after login is invisible to an unauthenticated crawler, so its findings fall through to route-prefix matching and misroute. Scan with the authenticatedstorage_stateso the owning attribute is present in the DOM the resolver reads. - Multi-tenant routing can swap the ownership map underneath you. When one codebase serves several tenants with different team structures, a single global map misassigns for all but one. Key the map by tenant and select it from the finding’s host before resolving, or the checkout team of tenant A inherits tenant B’s tickets.
- Selector drift silently breaks glob matches. A refactor that renames
.ds-buttonto.ds-btnmakes the*.ds-*glob still match but a narrower*.ds-button*rule stop matching, sending the defect to triage. Treat a rising triage rate as the signal that a glob has drifted, and prefer broad, stable prefixes over brittle full class names.
Frequently Asked Questions
Should ownership come from the DOM selector or the route?
Prefer the selector, because it identifies the component and therefore the team that can actually fix the defect, whereas the route only identifies the page it happened to render on. Use route prefix as a lower-precedence fallback for findings on page-level structure that no component rule claims, and let an explicit data-owner attribute override both.
How do I keep the ownership map from going stale after a reorg?
Store it as a reviewed file in the application repo so a reorg PR updates ownership in the same change that moves the code, and instrument the share of findings that fall through to the triage sentinel. A rising unowned rate is the objective signal that the map has drifted behind the component tree, which turns map maintenance into a tracked metric rather than a forgotten chore.
What happens when two rules match the same element?
The resolver sorts rules most-specific-first — selector globs before route prefixes, longer patterns before shorter — so the most precise rule wins deterministically. Treat any pair of rules that could match the same selector with equal specificity as a lint error in the map’s own CI, so ambiguity is caught at review time rather than surfacing as a misrouted ticket.