high priority medium complexity infrastructure pending backend specialist Tier 0

Acceptance Criteria

SpeechToTextAdapter exposes a Stream<TranscriptionEvent> where TranscriptionEvent is a sealed class with subtypes: PartialResult(text), FinalResult(text), StateChange(RecordingState), and ErrorEvent(SttError)
RecordingState enum has exactly four values: idle, listening, processing, error — state transitions are valid only in the allowed sequence (idle→listening→processing→idle|error)
Microphone permission is requested lazily on first startListening() call; if denied, adapter emits ErrorEvent(SttError.permissionDenied) and returns to idle state without crashing
Default locale is nb-NO (Norwegian BokmĂĄl); adapter falls back to device locale if nb-NO is unavailable, logging a warning via the app logger
When speech recognition is unavailable (no engine, low-memory kill), adapter emits ErrorEvent(SttError.unavailable) and the stream completes cleanly — no unhandled exceptions
stopListening() always transitions state to processing then idle regardless of current state; calling it from idle is a no-op
Adapter is registered as a Riverpod Provider (not StateNotifier) so it can be overridden in tests with a FakeSpeechToTextAdapter
All stream subscriptions are cancelled and resources disposed on adapter dispose()
Unit tests pass with FakeSpeechToTextAdapter covering: happy path, permission denied, unavailable engine, and cancel mid-session scenarios
No direct dependency on flutter/widgets — adapter lives in the infrastructure layer only

Technical Requirements

frameworks
Flutter
Riverpod
speech_to_text (Flutter plugin)
apis
speech_to_text SpeechToText.initialize()
speech_to_text SpeechToText.listen()
speech_to_text SpeechToText.stop()
permission_handler PermissionStatus
data models
TranscriptionEvent (sealed class)
RecordingState (enum)
SttError (enum)
performance requirements
Partial results must be delivered within 300ms of speech pause
Stream must not buffer more than 50 events; older partials should be dropped in favour of newer ones
Adapter initialization (permission check + engine warm-up) must complete within 2 seconds on mid-range devices
security requirements
Microphone permission must never be requested at app launch — only on explicit user action
No audio data is stored or transmitted; transcription happens on-device via OS speech engine
Sensitive field content produced by speech is treated as PII — never logged at INFO level or above

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use the abstract interface pattern: define ISpeechToTextAdapter with startListening(), stopListening(), dispose(), and a stream getter. Provide SpeechToTextAdapterImpl using the speech_to_text plugin, and FakeSpeechToTextAdapter for tests. Register with Riverpod as a Provider so tests can override via ProviderContainer(overrides: [...]). Use a StreamController.broadcast() internally so multiple listeners (overlay widget + BLoC) can subscribe independently.

Guard all plugin calls with try/catch and map exceptions to SttError cases. Keep locale logic in a separate _resolveLocale() private method for testability. Norwegian locale string is 'nb_NO' for the speech_to_text plugin (underscore, not hyphen).

Testing Requirements

Unit tests (flutter_test) must cover all RecordingState transitions using a FakeSpeechToTextAdapter that simulates the plugin. Test cases: (1) successful full transcription cycle idle→listening→processing→idle with PartialResult and FinalResult events, (2) permission denied on first call emits ErrorEvent and returns to idle, (3) engine unavailable emits ErrorEvent.unavailable, (4) stopListening() from listening state emits StateChange(processing) then StateChange(idle), (5) dispose() cancels stream with no errors. Integration test: mount a minimal widget that subscribes to the adapter stream and assert displayed text matches injected FinalResult. Minimum 90% line coverage on the adapter class.

No golden tests required for this task.

Component
Speech-to-Text Adapter
infrastructure medium
Epic Risks (3)
high impact medium prob technical

Dynamically rendered form fields built from runtime JSON schema are significantly harder to make accessible than statically declared widgets — Flutter's Semantics tree must be correct for every possible field type and every validation state. Failures here block the entire feature for Blindeforbundet's visually impaired peer mentors.

Mitigation & Contingency

Mitigation: Define WCAG 2.2 AA semantics requirements for each field type before implementation and write widget tests using Flutter's SemanticsController for every type. Include a real-device VoiceOver test session in the acceptance gate for this epic before marking it done.

Contingency: If dynamic semantics prove too difficult to get right generically, implement field-type-specific Semantics wrappers (one per supported field type) instead of a single generic renderer, accepting slightly more code duplication in exchange for reliable accessibility.

high impact medium prob technical

The report-form-orchestrator must manage a complex state machine — schema loading, draft persistence, per-field validation, submission retries, and error recovery — across multiple async operations. Incorrect state transitions could result in lost user data, double submissions, or UI freezes.

Mitigation & Contingency

Mitigation: Define all Bloc states and events explicitly as sealed classes before writing any logic. Use a state machine diagram reviewed by the team before implementation. Write exhaustive Bloc unit tests covering every state transition, including concurrent events and network interruption mid-submission.

Contingency: If Bloc complexity becomes unmanageable, extract draft persistence into a separate DraftManagerCubit and keep report-form-orchestrator focused solely on the submit workflow. The additional granularity makes each component independently testable.

medium impact low prob scope

Organisations may require field types beyond the five currently specified (text, multiline, checkbox group, radio, date). If a new type is discovered during pilot testing, the dynamic-field-renderer must be extended, potentially requiring changes across multiple layers.

Mitigation & Contingency

Mitigation: Design dynamic-field-renderer as a registry of field-type renderers with a clear extension point. Document the pattern for adding a new field type so that it can be done in one file without touching existing renderers.

Contingency: If an unhandled field type is encountered at runtime, dynamic-field-renderer renders a labelled plain-text fallback widget and logs a warning so the missing type is surfaced in monitoring, preventing a crash while making the gap visible.