Close the SDD cycle for the data-loss shielding change: verified pass with warnings (0 critical, 10/10 scenarios with named tests, W1 fixed post-verify), three stacked work units shipped plus the path-resolution fix. The persistence-resilience capability spec is promoted to openspec/specs/.
5.3 KiB
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._parsearper-entry tolerant parse +AlarmaMusical.fromJsonidhardening + degraded-read guard onrecalcularTodas/auto-persist. - Custom stations:
EstadoRadio._cargarEmisorasCustomtolerant load + write guard on_guardarEmisorasCustom. - EQ presets/device names: 4
ServicioEcualizadorreaders (_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:
- Per-entry tolerant parse: decode container, parse each entry in its own try; skip + log corrupt entries; keep all valid siblings.
- Skip-never-fabricate identity: entry with missing/invalid
idis skipped, never assigned a synthetic id (ghosts are worse than a gap). - Degraded-read guard (load-bearing): a failed TOP-LEVEL decode sets a per-subsystem
_lecturaDegradadaflag; 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/_cachecoherence 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_preferencesand 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.