Implement live-region announcement pipeline
epic-expense-type-selection-foundation-task-008 — Add the announcement dispatch mechanism to ExpenseTypeAccessibilityService: queue announcements through Flutter's SemanticsService.announce with assertive politeness for conflict blocks and polite for selection confirmations. Implement debounce (150 ms) to prevent announcement flooding during rapid multi-selection. Expose a testable AnnouncementSink interface so widget tests can assert announcement content without relying on platform semantics.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
The AnnouncementSink interface should be: abstract class AnnouncementSink { void announce(String message, TextDirection textDirection, AnnouncementPoliteness politeness); }. The real implementation wraps SemanticsService.announce — note that SemanticsService.announce does not natively support politeness levels on all platforms; map assertive to immediate dispatch and polite to debounced dispatch as a pragmatic implementation. For the debounce, maintain two Timer? fields (_politeTimer and _assertiveTimer) and cancel/restart on each call.
The convenience methods announceConflict and announceSelectionConfirmed should be the primary API that consuming BLoC/Cubit classes call — raw announce() is semi-internal. Register the service with Riverpod using a Provider that is overridable in tests, consistent with the pattern established in task-005 for AnalyticsSink.
Testing Requirements
Unit and widget tests using flutter_test. Unit tests: debounce collapses 3 rapid polite announcements to 1 (using FakeAsync), assertive announcement fires immediately (not debounced past 50 ms), sink receives correct politeness level per announcement type. Widget tests: inject TestAnnouncementSink into ExpenseTypeAccessibilityService via Riverpod override, trigger a conflict block in the UI, assert TestAnnouncementSink.records contains an assertive announcement with the expected conflict message. Do not use tester.semantics or SemanticsController — the TestAnnouncementSink approach makes widget tests fully portable across platforms.
The compatibility matrix might be under-specified in source documentation. If a new organisation adds expense types or redefines rules, hardcoded pairwise logic becomes a maintenance liability and can silently allow previously excluded combinations.
Mitigation & Contingency
Mitigation: Model the matrix as a const Map<ExpenseType, Set<ExpenseType>> rather than if-else chains; add a unit test that exhaustively asserts every pair combination so any future matrix change forces explicit test updates.
Contingency: If per-organisation matrix variants are requested before the epic closes, extract matrix loading into expense-type-config with an org-override slot and defer per-org configuration to the repository epic.
VoiceOver (iOS) and TalkBack (Android) handle Semantics widget announcements differently in Flutter. Live-region behaviour for disabled state changes is inconsistent across Flutter versions and may require platform-specific workarounds that are not yet documented.
Mitigation & Contingency
Mitigation: Write accessibility integration tests using Flutter's SemanticsController targeting both iOS and Android simulators from the outset; pin to a Flutter version known to handle Semantics.liveRegion correctly.
Contingency: If platform parity is unachievable before release, ship with a known gap documented in the WCAG audit log and schedule a dedicated accessibility sprint; do not block other epics.