critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

An ErrorMessageEntry Dart model is defined with fields: errorCode (String, required), plainLanguageMessage (String, required, ≤120 characters), actionableInstruction (String, required, ≤80 characters), helpLinkId (String?, optional)
An ErrorMessageRegistry Dart class wraps a Map<String, ErrorMessageEntry> and exposes: lookup(String errorCode) returning ErrorMessageEntry? (null if not found), and a fallbackEntry for unknown error codes
The registry is loaded from a bundled JSON asset file at `assets/error_messages/error_messages.json` on app startup — not from Supabase at runtime
The JSON asset file contains at minimum 10 representative error codes covering authentication errors (BankID, Vipps), form validation errors, network errors, and permission errors
A Riverpod FutureProvider (errorMessageRegistryProvider) loads the asset asynchronously and caches the result — subsequent accesses return the cached instance without re-reading the asset
The provider exposes an AsyncValue<ErrorMessageRegistry> so consumers can handle loading and error states
lookup() on an unknown errorCode returns the fallback entry ('An error occurred. Please try again.') rather than null or throwing
The JSON schema is documented with an example entry and field constraints
Asset is declared in pubspec.yaml under the assets section
All plain-language messages in the bundled JSON are in Norwegian (bokmål) as the primary language, with English keys

Technical Requirements

frameworks
Flutter
Riverpod
data models
ErrorMessageEntry
ErrorMessageRegistry
performance requirements
Asset loading must complete within 50ms on a mid-range device — the JSON file must remain under 50KB
Registry lookup must be O(1) via Map key lookup
Provider must cache the loaded registry — no re-read of the asset after initial load
security requirements
Error messages must not expose internal system details, stack traces, or database schema information to the user
The bundled asset is read-only and cannot be tampered with at runtime on a non-jailbroken device

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Structure the JSON asset as a top-level object with errorCode keys mapping to entry objects: `{ "AUTH_BANKID_CANCELLED": { "plainLanguageMessage": "...", "actionableInstruction": "...", "helpLinkId": "help-bankid-login" }, ... }`. This flat structure is O(1) to look up and trivial to extend. Keep the JSON file under `assets/error_messages/` so it can be extended with language variants later (e.g., `error_messages_en.json`) without restructuring.

Use Flutter's `rootBundle.loadString()` in the FutureProvider to read the asset. Use `json_serializable` or manual fromJson for ErrorMessageEntry — given the simple structure, manual fromJson is fine and avoids code generation overhead. The character length constraints (120/80) should be enforced as debug-mode assertions in the factory constructor rather than runtime exceptions — they are content authoring constraints, not user-input validation. The helpLinkId field is a key into HelpContentRegistry (task-004) — define the linking contract between the two registries here so task-004 can implement the other side.

Consider adding an error severity field (info, warning, error) in the model even if not used immediately — it costs nothing now and avoids a schema migration later.

Testing Requirements

Unit tests: test ErrorMessageRegistry.lookup() with a known error code (returns correct entry), an unknown error code (returns fallback), and an empty registry (returns fallback). Test ErrorMessageEntry field constraints — assert that a message exceeding 120 characters is caught during development (assertion or factory validation). Asset loading test: in a flutter_test environment with a fake asset bundle, verify that errorMessageRegistryProvider loads and parses the JSON correctly and that the provider state transitions from loading → data. JSON schema test: write a test that loads the actual bundled `error_messages.json` file and validates every entry against the ErrorMessageEntry model, ensuring no malformed entries ship in production.

Regression test: add a test that asserts the fallback entry text is non-empty and does not contain technical jargon.

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.