critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

`write(String orgId, Map<String, String> labels)` serializes the map to JSON and stores it under a key derived from orgId (e.g., `terminology_cache_{orgId}`)
`read(String orgId)` returns the deserialized `Map<String, String>` if the key exists, or null if not found
`clear(String orgId)` removes the cached entry for the given orgId only — does not affect other orgs
Serialization uses `jsonEncode`/`jsonDecode` from `dart:convert` — no third-party serialization
All three methods are async (return Future)
Invalid/corrupt JSON in SharedPreferences is handled gracefully: `read` returns null and logs a warning, does not throw
orgId used as part of the cache key is sanitized to avoid key collisions (only alphanumeric + underscore)
Unit tests cover: write then read returns same map, read on missing key returns null, clear removes only the target org, corrupt JSON returns null
Class is located at `lib/core/labels/terminology_cache_adapter.dart`
Works correctly on both iOS and Android (SharedPreferences behavior is cross-platform)

Technical Requirements

frameworks
Flutter
Dart
shared_preferences package
data models
LabelMap (Map<String, String>)
organization_configs (source of labels)
performance requirements
Read operation must complete in under 50ms on a low-end Android device
Write operation is async — must not block the UI thread
Cache key lookup is O(1)
security requirements
SharedPreferences data is stored unencrypted — do not cache any sensitive personal data, only UI label strings
OrgId must be sanitized before use as a storage key to prevent key injection

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use `SharedPreferences.getInstance()` lazily — do not store the instance as a field if the class is long-lived, as the singleton can be invalidated in tests. Instead, call `SharedPreferences.getInstance()` at the start of each method or inject it via constructor for testability. Cache key format: `terminology_cache_v1_{sanitizedOrgId}` — include a version prefix (`v1`) so future cache format changes can be invalidated cleanly by bumping the version. For JSON encoding, cast the decoded value carefully: `(jsonDecode(raw) as Map).map((k, v) => MapEntry(k, v as String))` — the `dynamic` cast is necessary because `jsonDecode` returns `dynamic`.

This adapter will be replaced or extended by the Hive backend in task-003, so keep the interface clean and avoid SharedPreferences-specific types leaking into the public API.

Testing Requirements

Unit tests using flutter_test with a mocked SharedPreferences instance (use `SharedPreferences.setMockInitialValues({})` in setUp). Test cases: (1) write then read returns identical map, (2) read on empty cache returns null, (3) write for org A does not affect read for org B, (4) clear(orgA) removes orgA but orgB still readable, (5) corrupt JSON string in SharedPreferences causes read to return null without throwing. All tests are fast (no real I/O). Use `group()` to organize by method name.

Component
Terminology Local Cache Adapter
data low
Epic Risks (2)
high impact medium prob integration

The labels JSONB column in organization_configs may lack a consistent schema across organizations, causing deserialization failures or silent missing keys when a new organization is onboarded with a differently structured map.

Mitigation & Contingency

Mitigation: Define and enforce a canonical JSONB schema via a Supabase check constraint and a migration script. Validate the schema in TerminologyRepository at parse time and emit structured errors for any key-type mismatches.

Contingency: If schema drift is discovered post-deployment, LabelKeyResolver's fallback logic ensures the app continues to function with English defaults while a data migration is prepared to normalize the offending organization's config.

medium impact low prob technical

Device local storage corruption or platform-specific SharedPreferences serialization bugs could render a cached terminology map unreadable, causing the app to fall back to English defaults unexpectedly for an organization with custom terminology.

Mitigation & Contingency

Mitigation: Wrap all cache reads in try/catch, validate the deserialized map against a minimum-required-keys check, and evict corrupted entries automatically before re-fetching from Supabase.

Contingency: Surface a non-blocking in-app warning to the coordinator that terminology may be in default English until the next sync completes; trigger an immediate background sync.