high priority low complexity backend pending frontend specialist Tier 3

Acceptance Criteria

A Stream<AccessibilityContextChange> (or equivalent typed stream) is exposed on TabStateManager and emits on every tab change
Each emission contains: (1) the new tab index as an integer, (2) the route semantics label string sourced from NavigationRouteConfig.metadata, and (3) a ContextChangeReason enum value
ContextChangeReason enum has exactly three values: userTap, sessionRestore, roleSwitch — no additional values without explicit approval
The stream does NOT emit when the stack snapshot is updated without a tab index change (i.e., sub-route pushes/pops do not trigger accessibility notifications)
When the semantics label is null or empty in NavigationRouteConfig.metadata, the notification falls back to a default string (e.g., 'Tab {index}') rather than emitting null
The stream is a broadcast stream so NavigationAccessibilityService and any test subscriber can both listen without conflict
No SemanticsService or Flutter accessibility APIs are called inside TabStateManager — this class only produces data; consumption is the responsibility of NavigationAccessibilityService
WCAG 2.2 AA: the emitted semantics label is descriptive enough for a screen reader announcement (verified by review, not automated test)

Technical Requirements

frameworks
Flutter
Riverpod
dart:async
apis
StreamController.broadcast()
NavigationRouteConfig.metadata
TabStateManager.setActiveTab (caller passes ContextChangeReason)
data models
AccessibilityContextChange (new value object: tabIndex, semanticsLabel, reason)
ContextChangeReason (new enum)
NavigationRouteConfig
performance requirements
Stream emission must be synchronous with the state update — no additional async delay introduced
StreamController must be closed in TabStateManager.dispose() to prevent resource leaks
security requirements
Semantics labels must not include role names, user IDs, or organization-specific data that could expose access control information to assistive technology logs

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Extend setActiveTab to accept an optional ContextChangeReason parameter (defaulting to userTap) so call sites that are not awareness-sensitive do not need to change. Inside setActiveTab, after updating the internal state, look up the semantics label by calling NavigationRouteConfig.getMetadata(index)?.semanticsLabel and emit the AccessibilityContextChange to the broadcast StreamController. Define AccessibilityContextChange as an immutable Dart class (use @immutable annotation and a const constructor) with final fields — this makes it safe to pass through streams and compare in tests. Keep notifyAccessibilityLayer as a private implementation detail; the public API surface is only the accessibilityContextChanges stream getter.

Testing Requirements

Unit tests should be added to the task-011 test suite or as a companion file. Required test cases: (1) tab change with userTap reason emits correct AccessibilityContextChange, (2) sessionRestore reason emits correctly on hydration, (3) roleSwitch reason emits correctly on role change, (4) sub-route push does NOT emit to the stream, (5) null semantics label falls back to default string. Use StreamQueue from async package or manual stream subscription with expectLater and emitsInOrder.

Component
Tab State Manager
service medium
Epic Risks (2)
high impact medium prob technical

StatefulShellRoute branch navigator state can interact unexpectedly with GoRouter's imperative navigation (go, push, replace), causing state snapshots to desync from actual route stacks. This could manifest as a user returning to a tab and seeing a different screen than expected, breaking the core motor-fatigue promise.

Mitigation & Contingency

Mitigation: Write integration tests that simulate cross-tab navigation with nested pushes before any UI layer is built. Pin go_router to a tested minor version and review the StatefulShellRoute changelog before upgrading.

Contingency: If branch navigator state consistently desyncs, fall back to a manual stack snapshot strategy using a custom NavigatorObserver that records and replays navigation events independently of StatefulShellRoute internals.

medium impact medium prob technical

Persisted navigation stacks in shared_preferences can become stale or corrupt if route paths are renamed during development, causing app crashes or infinite redirect loops on cold start for users who have an old snapshot.

Mitigation & Contingency

Mitigation: Version the persisted schema with a format key. On app start, validate that all stored route paths exist in the current route config before restoring; silently discard invalid entries rather than crashing.

Contingency: Implement a safe-mode cold start that skips state restoration after a detected crash (via a dirty-launch flag written at startup and cleared on successful first frame), falling back to the default root tab.