Files
pluriwave/openspec/changes/archive/2026-07-11-persistence-corruption-guard/archive-report.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

19 KiB

Archive Report: Persistence Corruption Guard

Change: persistence-corruption-guard Archived: 2026-07-11 Status: ARCHIVED AND CLOSED (see Archive Integrity note — folder deletion pending, content fully preserved) Archive Path: openspec/changes/archive/2026-07-11-persistence-corruption-guard/ Spec Merged To: openspec/specs/persistence-resilience/spec.md (new capability domain)


Executive Summary

The persistence-corruption-guard change is now archived with a PASS WITH WARNINGS verification verdict (0 CRITICAL, 2 WARNING, 4 SUGGESTION at verify time). It closes the highest-severity class from the 2026-07-11 full-app silent-failure audit (CRITICAL-1 + CRITICAL-4): a corrupt or schema-incompatible persistence read was silently coerced to an empty state, and the next write then committed that empty state to disk — permanently destroying user data with zero signal. Alarms was the worst instance: recalcularTodas() auto-persists on cold start and every 60 seconds with no user action, so a single bad read could erase all alarms automatically. The fix applies one shared architecture (per-entry tolerant parsing, skip-never-fabricate identity, degraded-read write suppression) across three subsystems — alarms, custom radio stations, and EQ presets/device names — shipped as three independently-revertible stacked commits. All 60 code-completable tasks are done (61 total; the sole remaining item, D4, is an explicitly optional on-device sanity check with no functional gap). 62/62 targeted tests pass, flutter analyze reports 0 issues, and WARNING W1 (a narrow uncaught-exception path introduced in estado_radio.dart) was fixed by the orchestrator after verification, with a RED-GREEN regression test added and a 53/53 re-run of the affected suites. WARNING W2 (a documentation-only gap in the multi-batch apply-progress evidence table) is accepted as non-blocking.


Artifact Traceability

Artifact Engram Observation ID Location Notes
Explore (embedded, no separate observation) sdd/persistence-corruption-guard/explore.md on-disk only Root-cause audit trace across 3 subsystems, shared defect architecture
Proposal #2350 sdd/persistence-corruption-guard/proposal Intent, scope (alarms/stations/EQ), shared 3-part fix approach, risks, rollback
Spec #2351 sdd/persistence-corruption-guard/spec; merged to openspec/specs/persistence-resilience/spec.md 5 requirement groups, 10 scenarios; new persistence-resilience capability domain
Design #2354 sdd/persistence-corruption-guard/design 6 architecture decisions (D1-D6), degraded-flag/write-authority matrix, file changes, testing strategy
Tasks #2359 sdd/persistence-corruption-guard/tasks 61 checkboxes across Phases A-D; review workload forecast + stacked-to-main work-unit plan
Apply-Progress #2360 sdd/persistence-corruption-guard/apply-progress 3 batches (Phase A, Phase B, Phase C+D) merged; 57/57 code tasks + 3/4 Phase D tasks complete
Verify-Report #2363 sdd/persistence-corruption-guard/verify-report PASS WITH WARNINGS; 10/10 spec scenarios independently re-verified compliant; W1/W2 findings

Implementation Completion

Phase A: Alarms (work unit 1, worst blast radius) — COMPLETE

Phase Description Tasks Status Evidence
A Shared tolerant-parse helper (persistencia_tolerante.dart) + ServicioAlarmas per-entry tolerant parse + _lecturaAlarmasDegradada guard on recalcularTodas/_guardar A1-A23 (23) 23/23 [x] 19/19 targeted alarms suite green; flutter analyze clean; commit 65c1ac2

Phase B: Custom Stations (work unit 2) — COMPLETE

Phase Description Tasks Status Evidence
B EstadoRadio._cargarEmisorasCustom parse-vs-IO split, .corrupt sidecar quarantine, _customDegradado guard on _guardarEmisorasCustom B1-B17 (17) 17/17 [x] 18/18 estado_radio_test.dart green (13 pre-existing + 5 new); flutter analyze clean; commit 13ad736

Phase C: Equalizer (work unit 3) — COMPLETE

Phase Description Tasks Status Evidence
C 4 ServicioEcualizador readers converted to parseMapaTolerante, principal-preset skip logging, no flag/quarantine (explicit-only, low recovery value) C1-C17 (17) 17/17 [x] 25/25 servicio_ecualizador_test.dart green (20 pre-existing + 5 new); flutter analyze clean; commit 45b7fc8

Phase D: Final Sweep — 3/4 COMPLETE (D4 optional, non-blocking)

Phase Description Tasks Status Notes
D Whole-project flutter analyze + targeted 7-file cross-phase test sweep; optional on-device sanity check D1-D4 (4) 3/4 [x] D1/D2/D3 done (0 issues, 62/62 pass, hang-avoidance documented); D4 explicitly optional — no device/emulator available in this environment, every behavior already Dart-testable and covered

Task Summary: 60 tasks complete ([x]), 1 task intentionally left [ ] with an explicit skip rationale (D4). Total: 61 checkboxes, 0 unaccounted.


Test Results

Unit Tests (independently re-executed at verify time, not trusted from self-report)

Suite Tests Result Notes
persistencia_tolerante_test.dart 5/5 PASS New file — shared helper unit tests
servicio_alarmas_corrupcion_test.dart 4/4 PASS New file — alarms corruption/degraded-guard cases
servicio_alarmas_cache_test.dart, servicio_alarmas_proxima_test.dart, servicio_alarmas_snooze_test.dart included in 19/19 PASS Pre-existing, unmodified, regression-locked
estado_radio_test.dart 18/18 PASS 13 pre-existing + 5 new
servicio_ecualizador_test.dart 25/25 PASS 20 pre-existing + 5 new
Total (targeted 7-file gate) 62/62 PASS Matches apply-progress D2 claim exactly, independently reproduced at verify time

Static Analysis

Tool Result Coverage
flutter analyze 0 issues Whole project, independently reproduced at verify time

Coverage

  • flutter test --coverage was launched as a background/informational check; did not complete within the verification report-writing window. Not blocking — Strict TDD treats coverage as informational only. Correctness is established by the pass/fail gate above plus direct source-diff inspection.

Verification Summary

Verdict

PASS WITH WARNINGS — 0 CRITICAL, 2 WARNING, 4 SUGGESTION at verify time.

All 10/10 spec scenarios independently verified compliant with named, re-executed passing tests. All 6 design decisions (D1-D6) verified against actual source diffs via git show, not self-reported summaries alone. 62/62 targeted tests and 0 flutter analyze issues independently reproduced. 60/61 tasks complete, with the sole incomplete task (D4) explicitly optional and non-blocking.

WARNING Resolution (post-verify, before archive)

Finding Severity Status at Archive
W1_archivoCustom() path resolution left outside the IO try/catch in estado_radio.dart; a hypothetical path_provider resolution failure would throw uncaught into EstadoRadio._init()'s Future.wait(...), also affecting sibling loads (cargarPopulares/cargarFavoritos/cargarGruposFavoritos) WARNING CLOSED. Fixed by the orchestrator post-verify (commit 316e9a7/48e74e6): _archivoCustom() is now called inside its own try/catch in _cargarEmisorasCustom(), setting _customDegradado=true and logging via registrarSaltoPersistencia on failure instead of throwing. A RED-GREEN regression test was added. Targeted gate re-run: 53/53 green across the 4 persistence suites touched. Independently confirmed present in source at archive time via direct read of lib/estado/estado_radio.dart.
W2 — Multi-batch apply-progress "TDD Cycle Evidence" table only carries a row for the final batch (Phase C); Phase A/B evidence lives inline in tasks.md instead of a consolidated table, likely lost on topic_key upsert across the 3 apply-progress save revisions WARNING ACCEPTED-DOCUMENTED, non-blocking. Not a RED/GREEN discipline failure — independently corroborated via source-diff inspection and the full regression re-run. Flagged as a process/tooling gap: future multi-batch changes should concatenate TDD Cycle Evidence rows across batches rather than keeping only the latest. No code or spec impact.

Compliance Matrix Snapshot

Requirement Scenarios Compliant Notes
Per-entry tolerant parsing preserves valid siblings 3 3 alarms, stations, EQ maps
Entry identity is never fabricated 2 2 alarms (no id), stations (no uuid)
Degraded read suppresses automatic writes / restores on authority 3 3 alarms (flag), stations (parse-vs-IO split, see Note A), EQ (no flag, explicit-only, see Note B)
Healthy reads are behaviorally unchanged 1 1 full 62/62 regression re-run
Diagnostics are developer-facing only, no l10n 1 1 log-assertion tests + zero l10n file changes across all commits
Total 10 10 0 deferred, 0 N/A, 0 untested

Note A: design D5 splits stations' total-corruption into a parse-fail path (flag-free, restores immediately on the next explicit add/remove) and an IO-fail path (_customDegradado deliberately NOT lifted by an explicit mutation — only a subsequent clean/partial load clears it, to avoid clobbering a possibly-intact file). This narrows the spec's general "user intent wins" prose for that one sub-case without contradicting the written scenario (flagged as SUGGESTION S2 for a spec-wording fast-follow, non-blocking).

Note B: EQ has no automatic writer at all, so "no automatic write while degraded" is true by construction in both old and new code — this half of the EQ scenario is structurally vacuous (mirrors Phase A's own A12 precedent); the test's real value is proving the explicit-write-after-total-corruption round-trip still works (SUGGESTION S3, non-blocking).


File Changes Summary

Estimated Changed Lines: ~1057 total across 3 stacked work-unit commits (Alarms 514, Stations 312, EQ 231), plus a small post-verify W1 fix commit. 400-Line Budget Risk: High (forecasted and mitigated by the tasks-phase stacked-to-main chain plan — Alarms → Stations → EQ, each independently shippable and revertible). Build Execution: Not run (flutter build explicitly excluded per repo convention — known unrelated full-build/full-suite hang risk).

New Files

  • lib/servicios/persistencia_tolerante.dart — shared parseListaTolerante<T>, parseMapaTolerante<V>, registrarSaltoPersistencia() (D1)
  • test/servicios/persistencia_tolerante_test.dart (5 tests)
  • test/servicios/servicio_alarmas_corrupcion_test.dart (4 tests)

Modified Files

  • lib/servicios/servicio_alarmas.dart — tolerant _parsear, _cacheRaw normalization on partial load (D3), _lecturaAlarmasDegradada guard on recalcularTodas/_guardar (D4)
  • lib/modelos/alarma_musical.dart — doc comment only, zero functional change (D2: caller hardens, model unchanged)
  • lib/estado/estado_radio.dart — parse-vs-IO split, .corrupt sidecar quarantine, _customDegradado guard (D5); post-verify W1 fix wraps _archivoCustom() resolution in the same IO-fail treatment
  • lib/servicios/servicio_ecualizador.dart — 4 readers converted to parseMapaTolerante, principal-preset skip logging (D6, no flag/quarantine)
  • test/estado/estado_radio_test.dart — +5 tests, _ArchivoEspia spy, _crearArchivoCustomRaw helper, W1 regression test
  • test/servicios/servicio_ecualizador_test.dart — +5 tests, debugPrint capture harness

Localization

  • lib/l10n/*.arb: 0 changes — confirmed via git show --stat on all commits; this capability is diagnostics-only, developer-facing, no user-visible copy.

Deviations from Design

Documented and accepted, no design violation:

  1. B8 (Phase B, Stations IO-fail split): _archivoCustom()'s own path resolution was initially called BEFORE the new _leerContenidoCustom try block, matching the task's literal scope ("read archivo.readAsString() in its own try"). This was self-disclosed by the apply agent as WARNING-worthy and confirmed as W1 at verify time; closed post-verify (see WARNING Resolution above).
  2. C10 (Phase C, _leerPresetsPorEmisora): no dedicated new corruption test targets this method directly — its correctness rests on being byte-for-byte the same conversion shape as _leerMapa (directly tested by C2/C3) plus the shared helper's own exhaustive unit tests. Flagged and accepted as SUGGESTION S1, non-blocking.
  3. C4 / A12 (vacuous-RED characterization tests): EQ never had a write-suppression flag in either the old or new code, so "explicit write still succeeds after total corruption" cannot be made RED by construction for EQ. This is intentional regression-lock engineering against ever accidentally adding a flag to EQ (which would violate D6's explicit-only design), not a rigor gap. Accepted as SUGGESTION S3, non-blocking.

Risks and Mitigations

Risk Likelihood Status
Guard suppresses legitimate recalcularTodas writes Med Mitigated — flag triggers ONLY on a failed last read; existing recalc tests lock normal behavior; no-thrash independently verified with a real write-counting test
_cacheRaw normalizing rewrite loses survivor data Low Mitigated — non-destructive, logged, surviving valid entries only; verified via source-diff
EQ matrix key round-trip breakage (colon-delimited station:device keys) Low Mitigated — explicit round-trip tests including a MAC-style multi-colon key survivor case
Flag lifecycle leaks across sessions Low Mitigated — in-memory per instance, cleared on success/mutation (asymmetric for stations IO-fail by design, D5)
_archivoCustom() path-resolution failure escapes uncaught (W1) Low probability, real if triggered CLOSED — fixed post-verify, RED-GREEN regression test added

Next Steps

  1. S1: add a dedicated corruption test for _leerPresetsPorEmisora (EQ per-station presets) for defense-in-depth, even though residual risk is already very low.
  2. S2: fast-follow the spec text for Requirement 3 ("degraded top-level read suppresses automatic writes until authority is restored") to explicitly distinguish the decode/parse-failure sub-case (authority restorable by explicit mutation) from the IO/read-failure sub-case (D5's deliberately stricter, non-restorable-by-mutation station behavior), so a reader of the spec alone is not surprised by the asymmetry.
  3. S4: none required — cardinality mismatch between spec wording (3 valid + 1 malformed) and covering tests (2 valid + 1 malformed) is behaviorally equivalent and non-blocking.
  4. W2 process fix: when merging multi-batch apply-progress under one topic_key, concatenate each batch's TDD Cycle Evidence rows instead of keeping only the latest batch.

Follow-Up Opportunities (Out of Scope for This Change)

  • EQ/alarms total-corruption on an explicit mutation still forfeits the unreadable blob (accepted per the user-intent rule in the proposal). A symmetric SP _corrupto backup-key quarantine for alarms/EQ was considered and rejected as unnecessary complexity for this change; revisit only if a future reviewer requires zero-loss everywhere.

Archive Integrity

Moved From: openspec/changes/persistence-corruption-guard/openspec/changes/archive/2026-07-11-persistence-corruption-guard/

Copy method: This execution had access only to Read / Write / Edit / Glob / Engram tools — no Bash, shell, or file-deletion tool was available. All 6 artifact files (explore.md, proposal.md, spec.md, design.md, tasks.md, verify-report.md) were reproduced by reading the exact on-disk source content and writing it verbatim to the archive path (no retyping, paraphrasing, or summarizing). Fidelity was spot-checked by re-reading both source and copy at the tail of the two longest files (tasks.md line 154, verify-report.md line 145 — exact match) and at a unicode-heavy table section of design.md (lines 20-29, including , , characters — exact match).

NOT a verified true move: because no delete-capable tool was available in this execution context, the original source folder openspec/changes/persistence-corruption-guard/ was left untouched on disk rather than deleted. This is a known, disclosed gap, not a silent failure: content is fully and faithfully duplicated at the archive path and merged into openspec/specs/persistence-resilience/, but the active openspec/changes/ directory will still show persistence-corruption-guard/ until a Bash-capable process (or a human) removes it. Required follow-up: run Remove-Item -Recurse -Force openspec/changes/persistence-corruption-guard (PowerShell) or rm -rf openspec/changes/persistence-corruption-guard (Bash) to complete the move, ideally after an independent diff -r confirms the two folders are identical.

Spec Merged: openspec/specs/persistence-resilience/spec.md created as a new capability domain (no prior main spec existed for this domain — verified via Glob openspec/specs/** returning alarm-pre-notice-countdown, eq-audiofocus, multi-device-eq, bt-device-identity, native-alarms, none named persistence-resilience).

Artifacts Preserved: All 6 openspec files (explore, proposal, spec, design, tasks, verify-report) + new state.yaml + this archive-report.md, all present at the archive path.


Sign-Off

Archive Ready For: Production — no blocking issues. Blocking Issues: None. W1 closed, W2 accepted-documented, D4 optional/non-blocking. Outstanding Non-Code Follow-Up: Delete the original openspec/changes/persistence-corruption-guard/ source folder (see Archive Integrity) — a filesystem-hygiene step, not a functional or spec-compliance gap. Recommendation: No further code changes required for this change. Optional S1/S2 fast-follows may be picked up independently at any time.


Appendix: Observation IDs for Traceability

  • #2350: sdd/persistence-corruption-guard/proposal (intent, scope, shared 3-part fix approach)
  • #2351: sdd/persistence-corruption-guard/spec (5 requirement groups, 10 scenarios, persistence-resilience capability)
  • #2354: sdd/persistence-corruption-guard/design (6 architecture decisions D1-D6, degraded-flag/write-authority matrix)
  • #2359: sdd/persistence-corruption-guard/tasks (61 checkboxes, stacked-to-main work-unit plan, review workload forecast)
  • #2360: sdd/persistence-corruption-guard/apply-progress (3 batches merged, 57/57 code tasks + 3/4 Phase D)
  • #2363: sdd/persistence-corruption-guard/verify-report (PASS WITH WARNINGS, 10/10 scenarios compliant, W1/W2 findings)
  • (this report): sdd/persistence-corruption-guard/archive-report

All artifacts archived and accessible at openspec/changes/archive/2026-07-11-persistence-corruption-guard/ with full change history preserved for audit trail.