critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

TerminologyMapState is a sealed class (or freezed union) with exactly three variants: loading, loaded(TerminologyMap data), error(LabelsNotifierException exception)
TerminologyMap is an immutable value type with fields: orgId (String), labels (Map<String, String>), updatedAt (DateTime)
TerminologyMap implements == and hashCode based on orgId and updatedAt (label map equality is not required for performance reasons — document this decision)
TerminologyMap.copyWith() is available for producing updated instances without mutation
LabelsNotifierException is a sealed class hierarchy with at least: LabelsNetworkException, LabelsCacheException, LabelsParseException — each with a message field and optional cause (Object?)
LabelsNotifierException subtypes extend Exception (not Error) for catchability
All classes are defined in a single file (e.g. `organization_labels_state.dart`) with no cross-file circular imports
The file exports all public types via a barrel or direct export from the feature's public API
All classes pass `dart analyze` with zero warnings
A Dart doc comment is present on each class explaining its role in the downstream provider contract

Technical Requirements

frameworks
Flutter
Riverpod
freezed (optional but recommended for sealed unions and copyWith)
data models
TerminologyMapState (sealed: loading | loaded | error)
TerminologyMap {orgId, labels, updatedAt}
LabelsNotifierException (sealed hierarchy: network, cache, parse)
performance requirements
All state classes must be immutable — no mutable fields
TerminologyMap must not perform deep equality on the labels Map in operator== — use identity or orgId+updatedAt only to avoid O(n) equality on large label maps
security requirements
LabelsNotifierException must not expose raw SupabaseException or network stack traces in its message field — wrap with a sanitized message
TerminologyMap must not be serializable to JSON by default to prevent accidental logging of label data

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use freezed if the team already has it as a dependency — it generates copyWith, ==, hashCode, and the sealed union pattern automatically with minimal boilerplate. If freezed is not used, implement the sealed class manually with Dart 3.0+ `sealed` keyword. The LabelsNotifierException hierarchy should mirror the TerminologyFailure types from the foundation layer (task-006) but is a distinct type — LabelsNotifierException is the service-layer contract, TerminologyFailure is the repository-layer contract. Map between them in the notifier implementation (a later task).

Define the initial state of OrganizationLabelsNotifier as TerminologyMapState.loading() so the UI always starts in a defined state. Document in the class-level comment that TerminologyMap.labels is a defensive copy (created with Map.unmodifiable()) to prevent downstream mutation.

Testing Requirements

Write a small unit test file verifying the state model contracts: (1) TerminologyMapState.loading is distinct from .loaded and .error in switch exhaustiveness; (2) TerminologyMap equality uses orgId + updatedAt (not label content); (3) TerminologyMap.copyWith(labels: newMap) produces a new instance with updated labels but same orgId; (4) all three LabelsNotifierException subtypes can be thrown and caught as Exception. These are simple structural tests — no mocks needed. Use flutter_test. Confirm that exhaustive switch on TerminologyMapState produces a compile error if a variant is missing (validated by attempting to compile a partial switch in a test helper).

Component
Organization Labels Notifier
service medium
Epic Risks (3)
high impact medium prob technical

When a user switches organization context (e.g., a coordinator with multi-org access), a race condition between the outgoing organization's map disposal and the incoming organization's fetch could briefly expose the wrong organization's terminology to the widget tree.

Mitigation & Contingency

Mitigation: Implement an explicit loading state in OrganizationLabelsNotifier that widgets check before rendering any resolved labels. The provider graph should cancel the previous organization's fetch via Riverpod's ref.onDispose before initiating the next.

Contingency: If the race manifests in production, fall back to English defaults during the transition window and emit a Sentry error event for investigation; the UX impact is a brief English flash rather than wrong-org terminology.

high impact low prob security

Supabase Row Level Security policies on organization_configs may inadvertently restrict the authenticated user from reading their own organization's labels JSONB column, causing silent empty maps that appear as English fallbacks.

Mitigation & Contingency

Mitigation: Write and test explicit RLS policies that grant SELECT on the labels column to any authenticated user whose organization_id matches. Add an integration test that verifies label fetch succeeds for each role (peer mentor, coordinator, admin).

Contingency: If RLS blocks are discovered in production, temporarily escalate label fetch to a service-role edge function while the RLS policy is corrected, ensuring no labels are exposed cross-organization.

medium impact medium prob scope

A peer mentor who installs the app for the first time with no internet connection will have no cached terminology map and will see only English defaults, which may be confusing for organizations like NHF that use Norwegian-specific role names exclusively.

Mitigation & Contingency

Mitigation: Bundle a default fallback terminology map for each known organization as a compile-time asset (Dart asset file) so that even fresh installs without connectivity render correct organizational terminology immediately.

Contingency: If bundled assets are out of date, display a one-time informational banner noting that terminology will update on next connectivity restore, with no functional blocking of the app.