docs(openspec): archive persistence-corruption-guard and promote its spec
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m49s

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/.
This commit is contained in:
2026-07-11 17:34:36 +02:00
parent 762e740c89
commit c65497e58a
9 changed files with 1002 additions and 0 deletions
@@ -0,0 +1,86 @@
# 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.