# Proposal: Persistence Corruption Guard ## Intent A corrupt or schema-incompatible persistence read is silently coerced to empty state; the next write commits that empty state to disk, permanently destroying user data with zero signal. Verified in 3 subsystems sharing ONE defect architecture (audit CRITICAL-1 + CRITICAL-4). Alarms is worst: `recalcularTodas()` auto-persists on cold start and every 60s with NO user action, so a single bad read erases all alarms automatically. Highest-severity class in the full-app audit; fixed as one change because the fix pattern is shared. ## Scope ### In Scope - **Alarms**: `ServicioAlarmas._parsear` per-entry tolerant parse + `AlarmaMusical.fromJson` `id` hardening + degraded-read guard on `recalcularTodas`/auto-persist. - **Custom stations**: `EstadoRadio._cargarEmisorasCustom` tolerant load + write guard on `_guardarEmisorasCustom`. - **EQ presets/device names**: 4 `ServicioEcualizador` readers (`_leerMapa`, `_leerMapaStrings`, `_leerPresetPrincipal`, `_leerPresetsPorEmisora`) + write guards. - Diagnostic logging (`[PluriWave]` prefix) for every skipped entry and degraded read. ### Out of Scope - User-visible strings / l10n — logging only, no UI copy. - Kotlin / native layer — pure Dart. - Other audit fix-groups (separate changes). - Schema versioning/migration of persisted payloads. ## Capabilities ### New Capabilities - `persistence-resilience`: cross-cutting invariants for reading/writing persisted user collections — per-entry tolerant parsing, skip-never-fabricate identity, degraded-read automatic-write suppression. ### Modified Capabilities - None. (Applies the new invariant to alarms/stations/EQ; no existing spec requirement changes.) ## Approach Shared 3-part fix, applied per subsystem: 1. **Per-entry tolerant parse**: decode container, parse each entry in its own try; skip + log corrupt entries; keep all valid siblings. 2. **Skip-never-fabricate identity**: entry with missing/invalid `id` is skipped, never assigned a synthetic id (ghosts are worse than a gap). 3. **Degraded-read guard (load-bearing)**: a failed TOP-LEVEL decode sets a per-subsystem `_lecturaDegradada` flag; while set, AUTOMATIC writes (`recalcularTodas`, timer refresh) are suppressed. An explicit user mutation (save/delete alarm, add/remove station, EQ tweak) re-establishes write authority and clears the flag. ### Design-phase decisions (flagged; design finalizes) - `_cacheRaw`/`_cache` coherence after per-entry skips: parsed != raw would re-trigger save every cycle. Proposed direction — after a partial load perform ONE explicit, logged, non-destructive rewrite normalizing the SURVIVING entries; design confirms mechanism. - Exact flag scope/lifecycle per subsystem; EQ colon-delimited matrix keys (`station:device`) MUST round-trip unchanged. ## Affected Areas | Area | Impact | Change | |------|--------|--------| | `lib/servicios/servicio_alarmas.dart` | Modified | tolerant `_parsear`, degraded guard on `recalcularTodas`/`_guardar` | | `lib/modelos/alarma_musical.dart` | Modified | `fromJson` id hardening (skip signal) | | `lib/estado/estado_radio.dart` | Modified | tolerant custom-station load + write guard | | `lib/servicios/servicio_ecualizador.dart` | Modified | 4 readers tolerant + write guards | | `test/servicios/`, `test/estado/` | Modified | new corruption/guard cases | ## Strict TDD Plan (outline) All Dart-testable via SharedPreferences mocks; existing suites MUST NOT regress. RED per subsystem: - (a) one corrupt entry among valid → valid survive, corrupt skipped, no destructive rewrite; - (b) fully corrupt raw → empty in-memory AND automatic write suppressed; - (c) explicit mutation after degraded read DOES persist (user-intent rule). Extend `test/servicios/servicio_alarmas_*`, `test/estado/estado_radio_*`, `test/servicios/servicio_ecualizador_test.dart` (20 cases). ## Risks | Risk | Likelihood | Mitigation | |------|-----------|------------| | Guard suppresses legitimate recalc writes | Med | Triggers ONLY on failed last read; existing recalc tests lock normal behavior | | Normalizing rewrite loses survivor data | Low | Non-destructive, logged, surviving valid entries only; design-reviewed | | EQ matrix key round-trip breakage | Low | Explicit round-trip test for `station:device` keys | | Flag lifecycle leaks across sessions | Low | In-memory per instance; cleared on success/mutation | ## Rollback Plan Pure Dart; no migration, schema, or native change. Persisted payload format is unchanged, so reverting the change commits/PR restores prior behavior with no data migration or forward-compat concern. ## Dependencies - None external. Uses existing `shared_preferences` and test harnesses. ## Review Workload Estimated 250-400 changed lines incl. tests across 4 source + ~3 test files. **400-line budget risk: Medium.** If tasks phase forecasts >400 lines, chain by subsystem (Alarms → Custom stations → EQ), each an autonomous slice with its own RED/GREEN and rollback boundary; Alarms slice first (highest severity). ## Success Criteria - [ ] Corrupt single entry never blanks its siblings in any of the 3 subsystems. - [ ] Fully corrupt read produces NO automatic disk write; data recoverable on next good read. - [ ] Explicit user mutation after a degraded read persists correctly. - [ ] No fabricated ids; skipped entries logged with context. - [ ] All existing alarm/station/EQ suites pass; new corruption cases green.