critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

TranscriptionStateManager is implemented as a StateNotifier<DictationState> registered via StateNotifierProvider.family<TranscriptionStateManager, DictationState, String> keyed by fieldId (String)
Initial state on construction is DictationIdle()
Public method startDictation(String fieldId) transitions state from DictationIdle → DictationRequestingPermission; calling it in any other state is a no-op with a logged warning
Public method stopDictation() transitions state from DictationRecording → DictationProcessing; calling it in invalid states is a no-op
Public method cancelDictation() transitions any non-idle, non-complete state back to DictationIdle(), discarding in-progress transcript
A secondary post-activity constraint guard exists inside startDictation(): if the current form context is not post-activity (determined via injected context validator), the state transitions to DictationError(DictationErrorCode.scopeViolation, ...)
All state transitions are synchronous at the notifier level; async side-effects (platform calls) are delegated to SpeechRecognitionService
The notifier exposes a Stream<String> partialTranscriptStream that emits each time DictationRecording.interimTranscript changes
The notifier does not directly import flutter/widgets.dart — it is a pure Dart/Riverpod class
Disposing the provider (when the family instance is no longer listened to) cancels any active SpeechRecognitionService subscription and leaves state as DictationIdle
flutter analyze on all new files produces zero warnings

Technical Requirements

frameworks
Flutter
Riverpod (flutter_riverpod — StateNotifier, StateNotifierProvider.family)
Freezed
apis
SpeechRecognitionService internal API (component 659)
performance requirements
State updates during DictationRecording (partial results) may fire at 100–300ms intervals; use throttling or debouncing (≥100ms) on interimTranscript updates to avoid excessive widget rebuilds
Family provider instances must be disposed when no longer used to prevent platform resource leaks
security requirements
The secondary scope guard must always execute even if DictationScopeGuard (component 661) is bypassed by a future refactor — defence in depth
No transcript text may be logged at INFO level or above; use verbose/debug logging only
Audio cannot be activated during sensitive peer conversations — enforce via scope check before any platform microphone call

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Place the file at lib/features/dictation/state/transcription_state_manager.dart with its provider at lib/features/dictation/providers/transcription_state_manager_provider.dart. Use StateNotifier rather than Cubit to stay within Riverpod's idiomatic pattern — avoid mixing BLoC Cubit with Riverpod unless the project already does so. The family key (fieldId) allows one independent state machine per text field, which is essential for forms with multiple dictation-capable fields. Inject SpeechRecognitionService via the constructor (pass it from the provider's ref.watch) so it can be mocked in tests.

Implement the state machine as a private _transition(DictationState newState) method that validates the transition is legal before calling state = newState, logging invalid transitions at debug level. The partialTranscriptStream can be implemented as a StreamController that is fed from the state listener — close it in dispose().

Testing Requirements

Unit tests using flutter_test and a mock/fake SpeechRecognitionService. Test cases required: (1) initial state is DictationIdle; (2) startDictation() in idle transitions to DictationRequestingPermission; (3) startDictation() in non-idle states is no-op; (4) cancelDictation() returns to DictationIdle from DictationRecording; (5) scope violation guard transitions to DictationError with scopeViolation code; (6) stopDictation() in recording state transitions to DictationProcessing; (7) partialTranscriptStream emits when DictationRecording.interimTranscript updates; (8) dispose cancels the service subscription. Use Riverpod's ProviderContainer for isolated testing. Minimum 85% line coverage on TranscriptionStateManager.

Component
Transcription State Manager
service medium
Epic Risks (2)
medium impact low prob technical

If a peer mentor rapidly switches between dictation-enabled fields while a session is still processing, the Riverpod family provider may share state or the SpeechRecognitionService may receive conflicting start/stop commands, causing orphaned recording sessions or state corruption.

Mitigation & Contingency

Mitigation: Design the state manager to enforce a single active dictation session globally via a shared active-field-key notifier. When a new field requests dictation while another session is active, automatically issue a stop command to the existing session and await completion before starting the new one. Test this concurrency scenario explicitly.

Contingency: If concurrency issues persist in integration testing, add a global dictation mutex at the SpeechRecognitionService level that serialises all start requests, with a short debounce to handle rapid field switching.

medium impact medium prob scope

The recovery flow for interrupted dictation sessions (app restart with persisted partials) has ambiguous UX requirements. If recovery is automatic and the user has already typed different content into the field, the merge logic may overwrite or duplicate user input, causing data loss.

Mitigation & Contingency

Mitigation: Define the recovery behaviour explicitly: recovery should surface as an opt-in prompt ('You have unsaved dictation — insert or discard?') rather than an automatic merge. The state manager emits a dedicated recoverable-partial state that the UI layer renders as a non-blocking action sheet.

Contingency: If the recovery UI adds too much complexity for the initial release, scope recovery to display only the partial text in the preview field on re-open without auto-insertion, letting the user manually copy if needed, and defer the automatic recovery prompt to a subsequent iteration.