# Tasks: Persistence Corruption Guard Change: persistence-corruption-guard Delivery strategy: stacked-to-main work-unit commits (chain decision already resolved by orchestrator — see forecast below) File: openspec/changes/persistence-corruption-guard/tasks.md ## Review Workload Forecast | Field | Value | |-------|-------| | Estimated changed lines | ~750-850 total (Phase A ~350-420, Phase B ~250-300, Phase C ~150-200) | | 400-line budget risk | High (total); Phase A alone is borderline-High, B is Medium, C is Low-Medium | | Chained PRs recommended | Yes | | Suggested split | PR 1 (Alarms + shared helper) -> PR 2 (Stations) -> PR 3 (EQ) | | Delivery strategy | stacked-to-main work-unit commits | | Chain strategy | stacked-to-main | Decision needed before apply: No Chained PRs recommended: Yes Chain strategy: stacked-to-main 400-line budget risk: High The 3-work-unit split (Alarms -> Stations -> EQ) IS the chaining answer: each phase ships as its own commit/PR, independently reviewable and independently shippable, gated by its own targeted suite. No further user decision needed before `sdd-apply` starts. ### Suggested Work Units | Unit | Goal | Likely PR | Notes | |------|------|-----------|-------| | 1 | Shared tolerant-parse helper + Alarms degraded guard (Phase A) | PR 1 | Base: main. Creates `persistencia_tolerante.dart`; highest severity (auto-wipe via `recalcularTodas`) | | 2 | Custom-station quarantine + IO-degraded guard (Phase B) | PR 2 | Base: PR 1 branch (stacked-to-main). Depends on `persistencia_tolerante.dart` from PR 1 | | 3 | EQ tolerant readers + logging (Phase C) | PR 3 | Base: PR 2 branch (stacked-to-main). Depends on `persistencia_tolerante.dart` from PR 1 | If Phase A's actual diff exceeds ~450 lines, an optional 4th interim split (helper commit vs. alarms-consumption commit) is available but not required. Stacked-to-main is the correct chain strategy here per the `chained-pr` skill's decision gate ("PR >400, each slice can land independently -> Stacked PRs to main") — each phase ships real user-facing value alone. When opening each work-unit PR, `sdd-apply` should follow `chained-pr`'s Output Contract (dependency diagram with a current-PR marker, review budget, verification plan) and `work-unit-commits`' checklist (tests travel with the behavior commit, as already reflected in every RED/GREEN pairing below). ## Phase A: Alarms (work unit 1 — worst blast radius, ships first) — COMPLETE (2026-07-11) **RED — shared tolerant-parse helper (D1)** - [x] A1 Create `test/servicios/persistencia_tolerante_test.dart`: `parseListaTolerante` skips 1 bad entry among 3 valid, returns the 3 survivors, logs the skip. (D1, Req: per-entry tolerant parsing, diagnostics) — done; plus a triangulation case (valid-shape entry whose parser itself throws) and a null-input case. - [x] A2 Same file: `parseMapaTolerante` skips 1 bad VALUE among 3 map entries, original keys preserved untouched. (D1, Req: per-entry tolerant parsing) — done. - [x] A3 Same file: both helpers on all-bad input return empty survivors, no exception escapes, still log. (D1, Req: degraded-read precondition) — done. - [x] A4 Run `flutter test test/servicios/persistencia_tolerante_test.dart` — confirm RED (helpers undefined). — confirmed: compile error, `parseListaTolerante`/`parseMapaTolerante` not found. **GREEN — shared tolerant-parse helper** - [x] A5 Create `lib/servicios/persistencia_tolerante.dart`: `parseListaTolerante` over an ALREADY-decoded `List?` (top-level string decode stays caller-owned — total-failure policy differs per subsystem, D3/D4 vs D5 vs D6); per-entry try/catch; calls the skip logger per skip. (D1) — done. - [x] A6 Same file: `parseMapaTolerante` analogous for `Map?`, preserves original keys (needed later for EQ's colon-delimited matrix keys). (D1) — done. - [x] A7 Same file: `registrarSaltoPersistencia({subsistema, detalle, razon})` -> `debugPrint('[PluriWave][persistencia] ...')`. No l10n string added. (Req: diagnostics are developer-facing only) — done. - [x] A8 Re-run A1's test file — confirm GREEN. — confirmed: 5/5 pass. **RED — Alarms tolerant parse + degraded flag** - [x] A9 Create `test/servicios/servicio_alarmas_corrupcion_test.dart` with a local `_PrefsEspia` (mirrors `servicio_alarmas_cache_test.dart`'s pattern, add an `escriturasString` counter). — done; also seeds via constructor + exposes `lecturasString`. - [x] A10 **Test A1 (no-thrash, explicit per orchestrator ask):** seed `alarmas_musicales_v1` with 2 valid + 1 malformed alarm entry; `cargar()` -> exactly 2 `alarmas`; call `recalcularTodas()` across 2 separate ticks; `escriturasString` grows by AT MOST 1 total (normalized cache stops the dirty-guard from re-firing every cycle). (D3, Req: per-entry tolerant parsing + healthy-behavior parity) — done; malformed entry = an `id` field of the wrong type (`42`) so `AlarmaMusical.fromJson` throws — this is what makes the test genuinely RED pre-fix (a non-Map entry alone is already silently filtered by the old `.whereType()` and does not exercise the bug). - [x] A11 Test A2 (total fail, zero writes): seed `'{bad'`; `cargar()` -> `alarmas` empty; `recalcularTodas()` -> `escriturasString` unchanged. (D4, Req: degraded read suppresses automatic writes) — done; also asserts the raw string on disk is byte-identical to the original corrupt seed. - [x] A12 Test A3 (explicit save restores authority): after A2's seed, `guardarAlarma(...)` writes once and clears the flag; a following real-change `recalcularTodas()` writes normally again. (D4, Req: degraded read — explicit mutation restores authority) — done. - [x] A13 Test A4 (missing id never fabricated): seed 1 entry with no `id` among 2 valid; `cargar()` -> exactly 2 alarmas, none carrying a freshly generated id. (D2, Req: entry identity is never fabricated) — done. - [x] A14 Run the new file — confirm RED. — confirmed: 3/4 new tests failed pre-fix (A10 got 0 survivors instead of 2; A11 got 1 spurious write instead of 0; A13 got 0 survivors instead of 2). A12 passed vacuously pre-fix since old code has no degraded flag to get stuck on — expected, not a rigor gap (its precondition setup is shared with A11, which independently proves the bug). **GREEN — Alarms implementation** - [x] A15 Restructure `_parsear`/`_configActual` (`lib/servicios/servicio_alarmas.dart:59-110`): top-level `jsonDecode` failure -> `_lecturaAlarmasDegradada=true`, `_cache`=empty config, `_cacheRaw=raw` unchanged (D4); success -> parse `alarmas`/`vacaciones`/`excepciones` each via `parseListaTolerante` (D1); any skips -> `_cache`=survivors, `_cacheRaw=_serializar(survivors)` normalized (D3); zero skips -> unchanged behavior, clear the flag if previously set (Req: suppression lifts on next successful read). — done; kept the per-field `data['x'] as List?` casts INSIDE the same try as `jsonDecode` (same boundary as the original code) so a container-level field of the wrong shape also degrades cleanly instead of throwing uncaught. - [x] A16 Keep `AlarmaMusical.fromJson`'s `id: json['id'] as String` unchanged (`lib/modelos/alarma_musical.dart:145-147`); doc-comment only — the caller-side per-entry try (A15) is what enforces skip-never-fabricate. (D2) — done (doc comment only, zero functional change; included despite not being spelled out verbatim in the orchestrator's file-scope summary line, since it is explicit task A16 and design's own File Changes table). - [x] A17 Add `bool _lecturaAlarmasDegradada = false;` field; in `recalcularTodas()` (line 201) add `if (_lecturaAlarmasDegradada) return config;` BEFORE the existing dirty-guard. (D4) — done. - [x] A18 In `_guardar()` (line 424) add `_lecturaAlarmasDegradada = false;` — every explicit mutation funnels through `_guardar`, restoring write authority. (D4, Req: explicit mutation restores authority) — done. - [x] A19 Run `servicio_alarmas_corrupcion_test.dart` — confirm GREEN (A1-A4 all pass). — confirmed: 4/4 pass. **REFACTOR + suite gate** - [x] A20 Review naming/doc comments in `_parsear`/`_configActual`/`persistencia_tolerante.dart`; no behavior change. — done during GREEN (doc comments written alongside the implementation); re-reviewed, no further changes needed. - [x] A21 Targeted suite: `flutter test test/servicios/servicio_alarmas_cache_test.dart test/servicios/servicio_alarmas_proxima_test.dart test/servicios/servicio_alarmas_snooze_test.dart test/servicios/servicio_alarmas_corrupcion_test.dart test/servicios/persistencia_tolerante_test.dart` — all green. NOTE: `servicio_alarmas_android_test.dart`, `servicio_alarmas_android_instance_test.dart`, `servicio_alarmas_pre_notice_template_test.dart` exercise `ServicioAlarmasAndroid` (native channel bridge, a different class untouched here) — confirmed via import check, excluded from this gate. (Req: healthy reads unchanged) — confirmed: 19/19 pass. - [x] A22 `flutter analyze --no-fatal-infos` on the changed/created source files — clean. — confirmed: ran plain `flutter analyze` (stricter, no info-suppression) on all 5 changed/created files — "No issues found!". - [x] A23 Commit as work unit 1: "Alarms — persistence corruption guard" (stacked-to-main, base: main). — done: commit `b66eec0` `fix(alarm): stop corrupt entries and unreadable payloads from wiping saved alarms` (5 files changed, 514 insertions, 24 deletions — above the ~350-420 forecast; still a single self-contained, independently-revertable work unit, no further split performed). Note: `openspec/changes/persistence-corruption-guard/` left untracked/uncommitted, matching this repo's convention of separate `docs(openspec)` commits. ## Phase B: Custom Stations (work unit 2) — COMPLETE (2026-07-11) **RED** - [x] B1 In `test/estado/estado_radio_test.dart` add a local `_ArchivoEspia implements File` fake (mirrors `_PrefsEspia`'s implements+`noSuchMethod` pattern) with overridable `exists`/`readAsString`/`writeAsString`/`rename`/`path`, to force an IO failure independent of the OS. — done; injectable overrides via constructor + `noSuchMethod` fallback for the rest of `File`'s interface, plus a `writeAsStringCalls` counter used by the write-guard assertion. - [x] B2 Test B1 (partial survivors, covers 2 requirements): temp file (raw-JSON helper, not `Emisora.toMap`) with 2 valid station maps + 1 entry missing a required field + 1 entry missing `uuid`; after `inicializar()`, exactly 2 stations survive, none carrying a freshly generated uuid. (D5 partial, Req: per-entry tolerant parsing + entry identity never fabricated) — done via a new `_crearArchivoCustomRaw` helper; confirms exactly `{custom-1, custom-2}` survive. - [x] B3 Test B2 (total parse-fail -> quarantine): temp file content `'{bad'`; after load, `emisoras_custom.json.corrupt` exists holding the ORIGINAL bytes, live path absent/empty. (D5 parse-fail) — done. - [x] B4 Test B3 (add after B2 restores authority): after B2's quarantine, `agregarEmisoraCustom(nueva)` -> live file has ONLY `nueva`; `.corrupt` sidecar untouched. (D5, Req: explicit mutation restores authority) — done. - [x] B5 Test B4 (IO error suppresses writes, NOT lifted by explicit add): `_ArchivoEspia` with `readAsString` throwing, `exists()` true; after load, `agregarEmisoraCustom(...)` -> the fake's `writeAsString` is NEVER invoked. NOTE (non-obvious, do not "fix"): unlike Alarms/EQ, station IO-fail suppression is intentionally NOT lifted by an explicit mutation this session — D5's rationale is avoiding clobbering a possibly-transiently-unreadable good file. (D5, Req: degraded read suppresses automatic writes) — done; also confirms the in-memory list still reflects the add (only the FILE write is suppressed, matching `agregarEmisoraCustom`'s existing reassign-then-persist structure). - [x] B6 Test B5 (sidecar-already-exists edge case): temp dir pre-seeded with an existing `.corrupt` file (content X); live file has a NEW parse failure; after load, `.corrupt` still holds X untouched, live path cleared. (D5 "else drop the live copy") — done. - [x] B7 Run the file — confirm RED for the new/changed cases. — confirmed: 5/5 new tests failed pre-fix for the correct reasons (0 survivors instead of 2; sidecar missing; `PathNotFoundException` reading a sidecar that doesn't exist yet pre-quarantine; 1 spurious write instead of 0; live file not cleared when a sidecar already exists). All 13 pre-existing tests stayed green. **GREEN** - [x] B8 Split `_cargarEmisorasCustom`'s catch (`lib/estado/estado_radio.dart:503-521`): read `archivo.readAsString()` in its own try — IO exception -> `_customDegradado=true`, `_emisorasCustom=[]`, log, do not touch the file. (D5 IO-fail, Req: diagnostics) — done via a new `_leerContenidoCustom(File)` helper: `exists()`+`readAsString()` share one try; any throw sets the flag, clears the in-memory list, logs, and returns `null` (file untouched). Judgment call: `_archivoCustom()`'s own resolution (building the `File`/calling `getApplicationDocumentsDirectory()`) is called BEFORE this try, matching the task's literal scope ("read `archivo.readAsString()` in its own try"); the old code's single catch-all also covered that resolution step, so a hypothetical failure there is now uncaught. No test (old or new) exercises it and it is a stable `path_provider` call in production — flagged here for reviewer awareness, not fixed, to avoid unrequested scope creep. - [x] B9 Decode/parse the read string via `parseListaTolerante` (D1); top-level `jsonDecode`/`as List` failure -> rename live file to `'${path}.corrupt'` (only if no sidecar exists yet; else delete the live file per D5), clear live path, `_emisorasCustom=[]`, log — NO flag set. — done inside `_cargarEmisorasCustom`'s second try, delegating the rename/delete choice to a new `_ponerEnCuarentena(File)` helper. - [x] B10 Per-entry skips with survivors -> `_emisorasCustom`=survivors, no flag, no quarantine. (D5 partial) — done via `parseListaTolerante(data, Emisora.fromMap, subsistema: 'emisoras_custom', coleccion: 'emisoras_custom')`; `_customDegradado` explicitly cleared on this path (clean and partial reads both restore authority). - [x] B11 Add `bool _customDegradado = false;` field; guard the top of `_guardarEmisorasCustom()` (line 523) with `if (_customDegradado) return;`. (D5) — done. - [x] B12 Clear `_customDegradado=false` only inside `_cargarEmisorasCustom` on a clean/partial read — NOT inside `agregarEmisoraCustom`/`eliminarEmisoraCustom` (intentional asymmetry, see B5). (D5) — done; cleared in both the file-absent branch and the successful-parse branch; the parse-fail (quarantine) branch leaves the flag untouched (D5 says "NO flag set" — interpreted as neither set nor cleared, since the quarantine's cleared live path is itself the "fresh start" signal). `agregarEmisoraCustom`/`eliminarEmisoraCustom` unmodified. - [x] B13 Run `test/estado/estado_radio_test.dart` — confirm new cases GREEN and existing custom-station cases (e.g. "incluye emisoras custom en el listado principal de inicio") unchanged. — confirmed: 18/18 pass (13 pre-existing + 5 new), diagnostic `[PluriWave][persistencia]` lines observed for every corrupt/degraded scenario. **REFACTOR + suite gate** - [x] B14 Review naming/doc comments; confirm `agregarEmitoraCustom`/`eliminarEmitoraCustom` compat aliases (lines 541-542, 551-552) still route through the guarded methods. — confirmed: both aliases (now ~622-624/632-634 after the new helpers were inserted) delegate unchanged to `agregarEmisoraCustom`/`eliminarEmisoraCustom`, which call the now-guarded `_guardarEmisorasCustom()`; `importarConfig`'s own direct `_guardarEmisorasCustom()` call is covered by the same single choke point (an explicit import while IO-degraded is also suppressed, consistent with D5's stated asymmetry). - [x] B15 Targeted suite: `flutter test test/estado/estado_radio_test.dart` — full file green (only Glob match for `estado_radio*`). (Req: healthy reads unchanged) — confirmed: 18/18 pass; `Glob test/estado/estado_radio*` returns only this one file. - [x] B16 `flutter analyze --no-fatal-infos` — clean. — confirmed: ran plain `flutter analyze` on `lib/estado/estado_radio.dart` + `test/estado/estado_radio_test.dart` — "No issues found!". - [x] B17 Commit as work unit 2: "Custom stations — persistence corruption guard" (stacked-to-main, base: work-unit-1 branch). Depends on `persistencia_tolerante.dart` from Phase A. — done: commit `cbed0a6` `fix(radio): quarantine corrupt custom-station files instead of silently emptying them` (2 files changed, 312 insertions, 10 deletions), stacked directly on `main` on top of Phase A's `65c1ac2`. Not pushed — orchestrator pushes. `openspec/changes/persistence-corruption-guard/` left untracked/uncommitted, matching Phase A's convention of separate `docs(openspec)` commits. ## Phase C: Equalizer (work unit 3) — COMPLETE (2026-07-11) **RED** - [x] C1 In `test/servicios/servicio_ecualizador_test.dart` add a new group using `SharedPreferences.setMockInitialValues` with raw corrupt strings (matches the file's existing convention). — done: new group `"ServicioEcualizador — lectura tolerante y diagnostico (persistence-resilience)"`, with a `debugPrint` capture `setUp`/`tearDown` mirroring `persistencia_tolerante_test.dart`'s pattern; added a top-level `_presetJson(nombre)` helper and a `dart:convert`/`package:flutter/foundation.dart` import. - [x] C2 Test C1 (partial matrix survivors + colon round-trip): seed `eq_presets_matriz_v1` with 2 valid `"stationUuid:deviceId"` entries + 1 malformed value; `cargar()` -> `presetsMatriz` has exactly 2, keys byte-identical to input. (D6, Req: per-entry tolerant parsing) — done; malformed entry is Map-shaped but has `'bandas': 'no-es-lista'` (a String, not a List) so `PresetEcualizador.desdeJson` throws on the internal cast — this is what makes it genuinely RED pre-fix, not just a shape filter. - [x] C3 Test C4 (colon round-trip with a skipped sibling, MAC-style deviceId): same shape but one surviving key's deviceId segment itself contains colons (e.g. `bt_a2dp:AA:BB:CC:DD:EE:FF`); confirm the split-on-first-colon convention (`servicio_ecualizador.dart:96-98`) is untouched by the skip. (D6, Req: per-entry tolerant parsing) — done; the MAC-style key `'station-X:bt_a2dp:AA:BB:CC:DD:EE:FF'` survives byte-for-byte alongside a plain key, with 1 malformed sibling dropped. Since `cargar()` auto-invokes `migrarClavesPlaceholder()` first (which itself calls `_leerMapa` and splits every matrix key on the first colon), this test also proves that code path doesn't choke or misfire on a multi-colon surviving key. - [x] C4 Test C2 (total corrupt map -> empty, explicit save still persists): seed `eq_preset_por_dispositivo_v1='{bad'`; `cargar()` -> `presetsDispositivo` empty; `guardarPresetDispositivo(...)` still writes successfully (no flag exists for EQ). (D6, Req: explicit mutation restores authority) — done. - [x] C5 Test C3 (corrupt principal -> flat + logged): seed `eq_preset_principal_v1='{bad'`; `cargar().principal == PresetEcualizador.flat` (existing fallback) AND a `[PluriWave][persistencia]` log line is now emitted (currently silent). (Req: diagnostics are developer-facing only) — done. - [x] C6 Test: `_leerMapaStrings` (nombresDispositivos) partial survivors — 1 bad value among 2 valid names. (D6, Req: per-entry tolerant parsing) — done; bad value is `12345` (not a String). - [x] C7 Run the file — confirm RED for the new cases only; existing 20 cases stay green (untouched). — confirmed: 4/5 new tests genuinely RED pre-fix — C2/C3 got an empty `Set` instead of 2 survivor keys, C6 got `{}` instead of 2 survivor names, C5 got `logs.any(...) == false` (no log line existed yet). C4 passed VACUOUSLY pre-fix — exactly mirrors Phase A's A12 precedent: EQ never had a degraded-write flag in either the old or new code, so "`guardarPresetDispositivo` still writes successfully" was already true beforehand; its role is characterization/regression-lock for D6's "no flag" design choice (matching the spec's own EQ scenario wording), not a bug-fix proof — the genuine per-entry-tolerance bug evidence comes from C2/C3/C6. All 20 pre-existing cases stayed green throughout. **GREEN** - [x] C8 Convert `_leerMapa` (`lib/servicios/servicio_ecualizador.dart:231-248`) to delegate per-entry conversion to `parseMapaTolerante` (D1) — shared by `presetsPorDispositivo` and `presetsMatriz`; keep the existing outer try around `jsonDecode` for total-failure (log + return `{}`, unchanged behavior). — done; outer catch now also calls `registrarSaltoPersistencia` before returning `{}` (previously silent) — consistent with the design's "log degraded reads" statement and Phase A's A15 precedent of logging total-failure catches beyond a single dedicated test's literal scope. - [x] C9 Convert `_leerMapaStrings` (lines 260-269) the same way -> `parseMapaTolerante`. — done; outer catch also now logs. - [x] C10 Convert `_leerPresetsPorEmisora` (lines 293-311) the same way -> `parseMapaTolerante`. — done; outer catch also now logs. NOTE: no NEW dedicated corruption test targets this method directly (C2-C6 exercise `_leerMapa`/`_leerMapaStrings`/`_leerPresetPrincipal`, not `_leerPresetsPorEmisora`) — its correctness rests on (a) `parseMapaTolerante`'s own exhaustive Phase A unit tests, (b) being byte-for-byte the same conversion shape as `_leerMapa` which C2/C3 DO exercise directly, and (c) existing healthy-path regression tests (4.1h, 4.1i) proving the wiring didn't regress normal reads. Flagged for reviewer awareness, matching Phase B's B8 precedent of documenting an intentionally-unexercised path rather than expanding scope beyond the assigned tasks. - [x] C11 Add a skip-logger call inside `_leerPresetPrincipal`'s existing catch (line 288) — single-value key, logging-only change, no structural change. (D6, Req: diagnostics) — done. - [x] C12 No flag/quarantine anywhere in this file (D6 intentional asymmetry vs. Alarms/Stations — EQ is explicit-only, presets trivially recreatable); read-modify-write callers (`guardarPresetDispositivo`, `guardarPresetMatriz`, `guardarNombresDispositivos`, `guardarPorEmisora`) need no code change — they already read via the now-fixed tolerant readers. — confirmed: no flag field added; all `guardarXxx` methods and `guardarConfiguracion` left byte-for-byte unmodified; C4 independently proves the "no flag" contract holds (explicit write after total corruption still lands on disk). - [x] C13 Run `test/servicios/servicio_ecualizador_test.dart` — confirm new cases GREEN and all 20 pre-existing cases (multi-device, nombresDispositivos, placeholder migration) pass unmodified. — confirmed: 25/25 pass (20 pre-existing + 5 new). **REFACTOR + suite gate** - [x] C14 Review naming/doc comments; confirm `migrarClavesPlaceholder` (itself calls `_leerMapa`/`_leerMapaStrings`, lines 83-126) is unaffected — its own 5.x test group still passes. — confirmed: all 7 tests in the placeholder-migration group (5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.9) pass unchanged; C3 additionally exercises this exact call path (via `cargar()`) with a multi-colon surviving key with no incident. - [x] C15 Targeted suite: `flutter test test/servicios/servicio_ecualizador_test.dart` — full file green (only Glob match for `servicio_ecualizador*`). (Req: healthy reads unchanged) — confirmed: 25/25 pass. - [x] C16 `flutter analyze --no-fatal-infos` — clean. — confirmed: ran plain `flutter analyze` on `lib/servicios/servicio_ecualizador.dart` + `test/servicios/servicio_ecualizador_test.dart` — "No issues found!". - [x] C17 Commit as work unit 3: "EQ presets — persistence corruption guard" (stacked-to-main, base: work-unit-2 branch). Depends on `persistencia_tolerante.dart` from Phase A. — done: commit `5e53a6c` `fix(eq): keep valid presets when stored maps are partially corrupt` (2 files changed, 231 insertions, 15 deletions), stacked directly on `main` on top of Phase B's `13ad736`. NOT pushed — orchestrator pushes. ## Phase D: Final Sweep (not a separate work unit — closes the change) — COMPLETE (2026-07-11) - [x] D1 `flutter analyze --no-fatal-infos` — zero issues across all 5 changed/created source files (`persistencia_tolerante.dart`, `servicio_alarmas.dart`, `alarma_musical.dart`, `estado_radio.dart`, `servicio_ecualizador.dart`). — confirmed: ran plain `flutter analyze` with NO path argument (whole-project sweep, strictly broader than the per-file requirement) — "No issues found!" (5.5s). - [x] D2 Targeted full sweep in one command: `flutter test test/servicios/persistencia_tolerante_test.dart test/servicios/servicio_alarmas_cache_test.dart test/servicios/servicio_alarmas_proxima_test.dart test/servicios/servicio_alarmas_snooze_test.dart test/servicios/servicio_alarmas_corrupcion_test.dart test/estado/estado_radio_test.dart test/servicios/servicio_ecualizador_test.dart` — all green. — confirmed: 62/62 pass in one run across all 7 files (persistencia_tolerante_test.dart, servicio_alarmas_cache_test.dart, servicio_alarmas_proxima_test.dart, servicio_alarmas_snooze_test.dart, servicio_alarmas_corrupcion_test.dart, estado_radio_test.dart, servicio_ecualizador_test.dart). - [x] D3 If a full `flutter test` run is attempted instead and hangs, fall back to D2's explicit file list — known intermittent hang, pre-existing/unrelated (per `sdd-init/pluriwave` cache and `eq-audiofocus-reapply` precedent); do not block delivery on it. — did not attempt a full `flutter test` run given this known issue; went straight to D2's targeted file list. - [ ] D4 Optional, non-blocking on-device/emulator sanity: launch once with a pre-existing (pre-change) alarm + custom station + EQ preset already saved; confirm all three still load correctly. Skip if no device/emulator is available — every behavior here is already Dart-testable and covered above; no l10n, no Kotlin changes in this capability. — left unchecked: no device/emulator available in this environment; explicitly optional per this task's own skip clause. ## Requirement Traceability | Spec Requirement | Satisfied by | |---|---| | Per-entry tolerant parsing preserves valid siblings | A10/A15, B2/B9-10, C2-C3/C6/C8-C10 | | Entry identity is never fabricated | A13/A16, B2/B9 | | Degraded read suppresses automatic writes / restores on authority | A11-A12/A17-A18, B4-B5/B11-B12, C4/C12 | | Healthy reads are behaviorally unchanged | A21, B15, C13/C15, D2 | | Diagnostics are developer-facing only, no l10n | A7, B8-B9, C5/C11, D1 | ## Dependency Summary Phase A -> Phase B -> Phase C -> Phase D, strictly sequential (stacked-to-main: each branch bases on the previous). Within each phase: RED sub-tasks are independent of each other (parallel); GREEN is sequential and depends on all of that phase's RED tests existing; REFACTOR + suite gate is sequential and closes the phase. Phase B and Phase C both depend on `persistencia_tolerante.dart` existing from Phase A — no other cross-phase code dependency.