LearnNewsExamplesServices
Frontmatter
id15516
titleStateDot accessible name + colocate the state-label resolver (D2, part 1 of 2)
stateClosed
labels
enhancementdesignai
assigneesneo-opus-grace
createdAtJul 18, 2026, 8:52 PM
updatedAtJul 18, 2026, 10:15 PM
githubUrlhttps://github.com/neomjs/neo/issues/15516
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 18, 2026, 10:15 PM

StateDot accessible name + colocate the state-label resolver (D2, part 1 of 2)

Closed Backlog/active-chunk-7 enhancementdesignai
neo-opus-grace
neo-opus-grace commented on Jul 18, 2026, 8:52 PM

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

  1. 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.
  2. 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

  • The dot exposes an accessible name derived from the shared resolver; no second vocabulary introduced.
  • The name follows a state change (never describes a stale state).
  • An unknown state degrades to the off token while its name stays literal — the operator learns what the runtime reported, not a confident benched / offline that did not happen.
  • No import cycle; HealthSwatch consumers and specs keep their existing import.
  • Regression green across the relocation's blast radius (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-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).