# Spec: Persistence Corruption Guard ## New Capability: persistence-resilience ### Purpose Cross-cutting invariants so a corrupt or schema-incompatible read of persisted user data (alarms, custom stations, EQ presets/device names) never silently destroys prior valid data: per-entry tolerant parsing, never-fabricate identity, and a degraded-read guard that suppresses automatic writes until a good read or an explicit user mutation restores write authority. ### Requirements #### Requirement: Per-entry tolerant parsing preserves valid siblings When a persisted collection decodes at the top level but one entry is malformed, the system MUST skip and log only that entry (index/key/id + reason), retaining every other valid entry. A skipped entry MUST NOT cause any sibling to be lost, regardless of whether a normalizing rewrite of the survivors later occurs — that mechanism is a design-time decision; this requirement constrains only the observable outcome. ##### Scenario: alarms — one corrupt alarm among N — Dart-testable via SharedPreferences mock seeding - GIVEN `alarmas_musicales_v1` has 3 valid alarms and 1 malformed entry - WHEN the config loads - THEN `alarmas` MUST contain exactly the 3 valid entries; corrupt one skipped + logged ##### Scenario: custom stations — one corrupt station among N — Dart-testable via injected temp-file fixture - GIVEN the custom-stations file has 2 valid stations and 1 entry missing required fields - WHEN custom stations load - THEN `_emisorasCustom` MUST contain exactly the 2 valid stations; corrupt one skipped + logged ##### Scenario: EQ maps — one corrupt value among N — Dart-testable via SharedPreferences mock seeding - GIVEN a persisted EQ map (per-station/per-device/matrix presets, device names) has 3 valid entries and 1 entry whose value fails to parse - WHEN that map loads - THEN it MUST contain exactly the 3 valid entries under their original keys; corrupt one skipped + logged --- #### Requirement: Entry identity is never fabricated An entry whose identity field (alarm `id`, station `uuid`) is missing, empty, or the wrong type MUST be treated as corrupt and skipped per the requirement above. The system MUST NOT synthesize a substitute identity. (EQ entries are keyed by their persisted map key, always present by construction — no scenario needed here.) ##### Scenario: alarms — entry with no id is skipped, never assigned one — Dart-testable via SharedPreferences mock seeding - GIVEN one persisted alarm entry has no `id` field - WHEN the config loads - THEN that entry MUST be absent, and no alarm with a freshly generated id MUST appear in its place ##### Scenario: custom stations — entry with no uuid is skipped, never assigned one — Dart-testable via injected temp-file fixture - GIVEN one persisted station entry has no `uuid` field - WHEN custom stations load - THEN that entry MUST be absent, and no station with a freshly generated uuid MUST appear in its place --- #### Requirement: Degraded top-level read suppresses automatic writes until authority is restored When a persisted payload's top-level decode fails entirely, in-memory state MAY be empty for the session, but the system MUST NOT let any non-explicit write reach disk while degraded. Suppression MUST lift on the next successful read or an explicit user mutation, whichever comes first (user intent wins). ##### Scenario: alarms — recalcularTodas writes nothing after a degraded read; explicit save restores authority — Dart-testable via SharedPreferences mock seeding - GIVEN `alarmas_musicales_v1` is unparseable - WHEN the config loads and `recalcularTodas()` runs - THEN no write occurs; the original string stays on disk - AND WHEN the user calls `guardarAlarma` afterward - THEN the write reaches disk and later automatic writes resume ##### Scenario: custom stations — no automatic write after a degraded file; explicit add/remove restores authority — Dart-testable via injected temp-file fixture - GIVEN the custom-stations file is unparseable - WHEN custom stations load - THEN no write to the file occurs - AND WHEN the user calls `agregarEmisoraCustom` or `eliminarEmisoraCustom` - THEN that write reaches the file and normal writes resume ##### Scenario: EQ — no automatic write after a degraded key; explicit tweak restores authority — Dart-testable via SharedPreferences mock seeding - GIVEN one of the 4 EQ persistence keys is unparseable - WHEN the EQ config loads - THEN no `guardarXxx` write occurs - AND WHEN the user performs an explicit EQ tweak (e.g. `guardarPresetDispositivo`) - THEN that write reaches disk and normal writes resume for that key --- #### Requirement: Healthy reads are behaviorally unchanged For a payload that decodes fully and validly at container and entry level, this capability MUST NOT alter resulting state, timing, or persisted bytes versus pre-change behavior. ##### Scenario: existing suites lock byte-for-byte parity — Dart-testable (regression) - GIVEN the existing green suites (`test/servicios/servicio_alarmas_*`, `test/estado/estado_radio_*`, `test/servicios/servicio_ecualizador_test.dart`) seeded with well-formed data - WHEN they run unmodified against the changed code - THEN every assertion MUST still pass with no behavioral difference --- #### Requirement: Diagnostics are developer-facing only Every skipped entry and every degraded read MUST log with context (subsystem, key/id/index, reason) via the existing `[PluriWave]`-prefixed convention. This capability MUST NOT introduce any user-visible string or l10n key. ##### Scenario: skip and degraded-read events are logged, no l10n added — Dart-testable (log/hook assertion) - GIVEN a corrupt entry is skipped or a top-level decode fails - WHEN that event occurs - THEN a `[PluriWave]`-prefixed log line MUST identify the subsystem and reason, and no string MUST be added to `lib/l10n/app_en.arb` or any locale file ### Localization Not applicable — no user-visible copy is introduced by this capability; all diagnostics are developer-facing log lines only.