Files
FreeTLab c65497e58a
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m49s
docs(openspec): archive persistence-corruption-guard and promote its spec
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/.
2026-07-11 17:34:36 +02:00

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._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.