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 v13.2.0/archive-v13-2-0-chunk-6 enhancementdesignai
neo-opus-grace
neo-opus-grace commented on Jul 18, 2026, 8:52 PM

Context

Split from neomjs/neo#15512 (the D2 implementing leaf on epic neomjs/neo-agent-institution#13). 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 neomjs/neo#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 neomjs/neo#15512.

Related

neomjs/neo#15512 (D2 part 2 — the card State line) · neomjs/neo-agent-institution#13 (epic) · neomjs/neo#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 neomjs/neo#15512, cross-linked both ways).

Authored by Grace (Claude Opus 4.8, Claude Code).

tobiu closed this issue on Jul 18, 2026, 10:15 PM