# 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`, `parseMapaTolerante`, `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 ### Recommended, Non-Blocking 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.