high priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

PlainLanguageErrorDisplay accepts an errorCode String parameter and resolves the human-readable message via PlainLanguageContentService; it never renders a raw error code or technical stack trace
The widget renders three zones: (1) a short headline message (≤10 words), (2) one actionable instruction sentence, and (3) an optional 'Get help' link that is hidden when no help URL is registered for the error code
The widget wraps its content in a Semantics widget with liveRegion: true so that screen readers announce the error automatically when it appears without requiring user focus
When an error is displayed, the live region announcement fires exactly once — re-rendering the same error code a second time does not re-announce
The widget uses accessibility design tokens exclusively for colors: error surface token for background, error-on-surface token for text, ensuring WCAG 2.2 AA contrast ratio ≥ 4.5:1
The 'Get help' link is a full-tap-target GestureDetector or InkWell with a minimum 44×44dp touch area and a semantic label of 'Get help with [error headline]'
If ErrorMessageRegistry does not contain the given errorCode, PlainLanguageErrorDisplay renders a generic fallback message ('Something went wrong. Please try again.') and logs the missing key in debug mode only
The widget is stateless and accepts an optional onHelpTap VoidCallback parameter for testability — the caller controls navigation
PlainLanguageErrorDisplay replaces every instance of raw Snackbar(content: Text(e.toString())) and AlertDialog with raw error strings across the app
Widget renders correctly in both LTR and RTL text directions without layout overflow

Technical Requirements

frameworks
Flutter
apis
PlainLanguageContentService.resolveError(errorCode)
ErrorMessageRegistry.lookup(errorCode)
LiveRegionAnnouncer.announce(message)
data models
ErrorMessage (headline, instruction, helpUrl, errorCode)
PlainLanguageContent
performance requirements
Widget build time must not exceed 8ms — all registry lookups must be synchronous in-memory operations (no async calls during build)
Live region announcement must fire within one frame (~16ms) of widget insertion into the tree
security requirements
Error messages must never expose internal server error details, SQL fragments, or stack traces to the UI layer
Help URLs must be validated against an allowlist before rendering as tappable links — no user-supplied URLs
ui components
PlainLanguageErrorDisplay (stateless widget)
ErrorHeadline (Text with token typography)
ErrorInstruction (Text with token typography)
ErrorHelpLink (InkWell with semantic label)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Make this widget a pure presentation component — it receives resolved content from PlainLanguageContentService but does not call the service itself. Keep the service call in the parent (screen or BLoC) so the widget remains easy to test in isolation. The live region pattern in Flutter requires wrapping in a Semantics widget with liveRegion: true AND ensuring the widget is inserted (not just updated) each time an error appears — if the parent conditionally shows/hides the widget, insertion triggers the announcement. If errors update in-place (same widget, new content), use a UniqueKey to force a full widget replacement and re-trigger the live region.

Avoid using SnackBar for errors throughout the app — SnackBars are not reliably accessible on all screen readers. Use ONLY PlainLanguageErrorDisplay for user-facing errors going forward.

Testing Requirements

Write flutter_test widget tests covering: (1) known errorCode renders correct headline, instruction, and help link, (2) unknown errorCode renders generic fallback message, (3) live region Semantics node is present with liveRegion: true when widget is in the tree, (4) 'Get help' link is absent when helpUrl is null, (5) onHelpTap callback fires on tap, (6) minimum touch target size of 44dp verified via widget bounds, (7) fallback message does not contain the raw errorCode string. Run accessibility checks using flutter_test's SemanticsController. Include a golden test snapshot of the error widget with a known error code to catch unintended visual regressions. Target 100% line coverage on the widget file.

Component
Error Message Registry
data low
Epic Risks (4)
high impact medium prob technical

The error message registry and help content registry both depend on bundled JSON assets loaded at startup. If asset loading fails silently (e.g. malformed JSON, missing pubspec asset declaration), the entire plain-language layer falls back to empty strings or raw error codes, breaking the accessibility guarantee app-wide.

Mitigation & Contingency

Mitigation: Implement eager validation of both assets during app initialisation with an assertion failure in debug mode and a structured error log in release mode. Add integration tests that verify asset loading in the Flutter test harness on every CI run.

Contingency: Ship a hardcoded minimum-viable fallback message set directly in Dart code so the app always has at least a safe generic message, preventing a blank or code-only error surface.

medium impact medium prob dependency

The AccessibilityDesignTokenEnforcer relies on dart_code_metrics custom lint rules. If the lint toolchain is not already configured in the project's CI pipeline, integrating a new linting plugin may cause unexpected build failures or require significant CI configuration work beyond the estimated scope.

Mitigation & Contingency

Mitigation: Audit the existing dart_code_metrics configuration in the project before starting implementation. Scope the lint rules to a separate Dart package that can be integrated incrementally, starting with the most critical rule (hard-coded colors) and adding others in subsequent iterations.

Contingency: Fall back to Flutter test-level assertions (using the cognitive-accessibility-audit utility) to catch violations in CI if the lint plugin integration is delayed, preserving enforcement coverage without blocking the epic.

medium impact low prob technical

WizardDraftRepository must choose between shared_preferences and Hive for local persistence. Choosing the wrong store for the data volume (e.g. shared_preferences for complex nested wizard state) can lead to serialisation bugs or performance degradation, particularly on lower-end Android devices used by some NHF members.

Mitigation & Contingency

Mitigation: Define a clean repository interface first and implement shared_preferences as the initial backend. Profile serialisation round-trip time with a realistic wizard state payload (≈10 fields) before committing to either store.

Contingency: Swap the persistence backend behind the repository interface without touching wizard UI code, which is possible precisely because the repository abstraction isolates the storage detail.

medium impact high prob scope

The AccessibilityDesignTokenEnforcer scope could expand significantly if a large portion of existing widgets use hard-coded values. Discovering widespread violations during this epic would force either a major refactor or a decision to exclude legacy components, potentially reducing the enforcer's coverage and value.

Mitigation & Contingency

Mitigation: Run a preliminary audit of existing widgets using a simple grep for hard-coded hex colors and raw pixel values before implementation begins. Use the results to set a realistic remediation boundary for this epic and log all out-of-scope violations as tracked tech-debt items.

Contingency: Scope the enforcer to new and modified components only (via file-path filters in dart_code_metrics config), shipping a partial but immediately valuable coverage rather than blocking the epic on full-codebase remediation.