Context
Split from #15512 (the D2 implementing leaf on epic #14805). The ruling (here) covers two different success criteria, and they are not one deliverable:
- this ticket — the dot's accessible name (WCAG 4.1.2 / 1.1.1);
- #15512 (narrowed) — the visible State line on the card (WCAG 1.4.1).
Splitting because they genuinely diverge: the accessible name is a mechanical contract on a primitive, while the visible channel carries a density judgement at measured fleet row counts. Keeping them in one ticket would have meant two PRs against one leaf, which the one-PR-per-ticket model (operator rule #12367) correctly refuses — the PR-body lint caught the attempt.
The Problem
StateDot renders no text, title, or aria-label — it only swaps a state class — so a graphical indicator carrying session state reaches assistive tech as unlabelled decoration.
Fixing it exposes a structural constraint: stateLabel(state) lives in HealthSwatch.mjs, but HealthSwatch imports stateClass from StateDot.mjs. Sourcing the label from the consumer would close an import cycle.
The Fix
StateDot exposes role="img" + an aria-label from the shared state vocabulary, kept in sync with the state config so it can never name a stale state.
- Move
stateLabel (+ its map) to StateDot.mjs, beside stateToken / stateClass — the module already documents itself as "the single source of truth for the fleet primitives (HealthSwatch reuses it)". HealthSwatch re-exports it so existing consumers and specs are untouched.
Acceptance Criteria
Out of Scope — and this is the load-bearing boundary
This does NOT satisfy 1.4.1 and must not be recorded as doing so. An aria-label does nothing for a sighted operator who cannot separate the hues. The visible non-colour channel is #15512.
Related
#15512 (D2 part 2 — the card State line) · #14805 (epic) · #14619 (the audit) · CARD-CONTRACT.md (#14605).
Live latest-open sweep at 2026-07-18T18:52Z: no equivalent leaf (this is a deliberate split of #15512, cross-linked both ways).
Authored by Grace (Claude Opus 4.8, Claude Code).
Context
Split from #15512 (the D2 implementing leaf on epic #14805). The ruling (here) covers two different success criteria, and they are not one deliverable:
Splitting because they genuinely diverge: the accessible name is a mechanical contract on a primitive, while the visible channel carries a density judgement at measured fleet row counts. Keeping them in one ticket would have meant two PRs against one leaf, which the one-PR-per-ticket model (operator rule #12367) correctly refuses — the PR-body lint caught the attempt.
The Problem
StateDotrenders no text,title, oraria-label— it only swaps a state class — so a graphical indicator carrying session state reaches assistive tech as unlabelled decoration.Fixing it exposes a structural constraint:
stateLabel(state)lives inHealthSwatch.mjs, butHealthSwatchimportsstateClassfromStateDot.mjs. Sourcing the label from the consumer would close an import cycle.The Fix
StateDotexposesrole="img"+ anaria-labelfrom the shared state vocabulary, kept in sync with the state config so it can never name a stale state.stateLabel(+ its map) toStateDot.mjs, besidestateToken/stateClass— the module already documents itself as "the single source of truth for the fleet primitives (HealthSwatch reuses it)".HealthSwatchre-exports it so existing consumers and specs are untouched.Acceptance Criteria
benched / offlinethat did not happen.HealthSwatchconsumers and specs keep their existing import.stateDot·healthSwatch·agentCard).Out of Scope — and this is the load-bearing boundary
This does NOT satisfy 1.4.1 and must not be recorded as doing so. An
aria-labeldoes nothing for a sighted operator who cannot separate the hues. The visible non-colour channel is #15512.Related
#15512 (D2 part 2 — the card State line) · #14805 (epic) · #14619 (the audit) ·
CARD-CONTRACT.md(#14605).Live latest-open sweep at 2026-07-18T18:52Z: no equivalent leaf (this is a deliberate split of #15512, cross-linked both ways).
Authored by Grace (Claude Opus 4.8, Claude Code).