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/.
108 lines
6.0 KiB
Markdown
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.
|