Files
pluriwave/openspec/specs/persistence-resilience/spec.md
T
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

108 lines
6.0 KiB
Markdown

# Spec: Persistence Corruption Guard
## New Capability: persistence-resilience
### Purpose
Cross-cutting invariants so a corrupt or schema-incompatible read of persisted user data (alarms, custom stations, EQ presets/device names) never silently destroys prior valid data: per-entry tolerant parsing, never-fabricate identity, and a degraded-read guard that suppresses automatic writes until a good read or an explicit user mutation restores write authority.
### Requirements
#### Requirement: Per-entry tolerant parsing preserves valid siblings
When a persisted collection decodes at the top level but one entry is malformed, the system MUST skip and log only that entry (index/key/id + reason), retaining every other valid entry. A skipped entry MUST NOT cause any sibling to be lost, regardless of whether a normalizing rewrite of the survivors later occurs — that mechanism is a design-time decision; this requirement constrains only the observable outcome.
##### Scenario: alarms — one corrupt alarm among N — Dart-testable via SharedPreferences mock seeding
- GIVEN `alarmas_musicales_v1` has 3 valid alarms and 1 malformed entry
- WHEN the config loads
- THEN `alarmas` MUST contain exactly the 3 valid entries; corrupt one skipped + logged
##### Scenario: custom stations — one corrupt station among N — Dart-testable via injected temp-file fixture
- GIVEN the custom-stations file has 2 valid stations and 1 entry missing required fields
- WHEN custom stations load
- THEN `_emisorasCustom` MUST contain exactly the 2 valid stations; corrupt one skipped + logged
##### Scenario: EQ maps — one corrupt value among N — Dart-testable via SharedPreferences mock seeding
- GIVEN a persisted EQ map (per-station/per-device/matrix presets, device names) has 3 valid entries and 1 entry whose value fails to parse
- WHEN that map loads
- THEN it MUST contain exactly the 3 valid entries under their original keys; corrupt one skipped + logged
---
#### Requirement: Entry identity is never fabricated
An entry whose identity field (alarm `id`, station `uuid`) is missing, empty, or the wrong type MUST be treated as corrupt and skipped per the requirement above. The system MUST NOT synthesize a substitute identity. (EQ entries are keyed by their persisted map key, always present by construction — no scenario needed here.)
##### Scenario: alarms — entry with no id is skipped, never assigned one — Dart-testable via SharedPreferences mock seeding
- GIVEN one persisted alarm entry has no `id` field
- WHEN the config loads
- THEN that entry MUST be absent, and no alarm with a freshly generated id MUST appear in its place
##### Scenario: custom stations — entry with no uuid is skipped, never assigned one — Dart-testable via injected temp-file fixture
- GIVEN one persisted station entry has no `uuid` field
- WHEN custom stations load
- THEN that entry MUST be absent, and no station with a freshly generated uuid MUST appear in its place
---
#### Requirement: Degraded top-level read suppresses automatic writes until authority is restored
When a persisted payload's top-level decode fails entirely, in-memory state MAY be empty for the session, but the system MUST NOT let any non-explicit write reach disk while degraded. Suppression MUST lift on the next successful read or an explicit user mutation, whichever comes first (user intent wins).
##### Scenario: alarms — recalcularTodas writes nothing after a degraded read; explicit save restores authority — Dart-testable via SharedPreferences mock seeding
- GIVEN `alarmas_musicales_v1` is unparseable
- WHEN the config loads and `recalcularTodas()` runs
- THEN no write occurs; the original string stays on disk
- AND WHEN the user calls `guardarAlarma` afterward
- THEN the write reaches disk and later automatic writes resume
##### Scenario: custom stations — no automatic write after a degraded file; explicit add/remove restores authority — Dart-testable via injected temp-file fixture
- GIVEN the custom-stations file is unparseable
- WHEN custom stations load
- THEN no write to the file occurs
- AND WHEN the user calls `agregarEmisoraCustom` or `eliminarEmisoraCustom`
- THEN that write reaches the file and normal writes resume
##### Scenario: EQ — no automatic write after a degraded key; explicit tweak restores authority — Dart-testable via SharedPreferences mock seeding
- GIVEN one of the 4 EQ persistence keys is unparseable
- WHEN the EQ config loads
- THEN no `guardarXxx` write occurs
- AND WHEN the user performs an explicit EQ tweak (e.g. `guardarPresetDispositivo`)
- THEN that write reaches disk and normal writes resume for that key
---
#### Requirement: Healthy reads are behaviorally unchanged
For a payload that decodes fully and validly at container and entry level, this capability MUST NOT alter resulting state, timing, or persisted bytes versus pre-change behavior.
##### Scenario: existing suites lock byte-for-byte parity — Dart-testable (regression)
- GIVEN the existing green suites (`test/servicios/servicio_alarmas_*`, `test/estado/estado_radio_*`, `test/servicios/servicio_ecualizador_test.dart`) seeded with well-formed data
- WHEN they run unmodified against the changed code
- THEN every assertion MUST still pass with no behavioral difference
---
#### Requirement: Diagnostics are developer-facing only
Every skipped entry and every degraded read MUST log with context (subsystem, key/id/index, reason) via the existing `[PluriWave]`-prefixed convention. This capability MUST NOT introduce any user-visible string or l10n key.
##### Scenario: skip and degraded-read events are logged, no l10n added — Dart-testable (log/hook assertion)
- GIVEN a corrupt entry is skipped or a top-level decode fails
- WHEN that event occurs
- THEN a `[PluriWave]`-prefixed log line MUST identify the subsystem and reason, and no string MUST be added to `lib/l10n/app_en.arb` or any locale file
### Localization
Not applicable — no user-visible copy is introduced by this capability; all diagnostics are developer-facing log lines only.