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/.
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 --coveragewas 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— sharedparseListaTolerante<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,_cacheRawnormalization on partial load (D3),_lecturaAlarmasDegradadaguard onrecalcularTodas/_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,.corruptsidecar quarantine,_customDegradadoguard (D5); post-verify W1 fix wraps_archivoCustom()resolution in the same IO-fail treatmentlib/servicios/servicio_ecualizador.dart— 4 readers converted toparseMapaTolerante, principal-preset skip logging (D6, no flag/quarantine)test/estado/estado_radio_test.dart— +5 tests,_ArchivoEspiaspy,_crearArchivoCustomRawhelper, W1 regression testtest/servicios/servicio_ecualizador_test.dart— +5 tests,debugPrintcapture harness
Localization
lib/l10n/*.arb: 0 changes — confirmed viagit show --staton all commits; this capability is diagnostics-only, developer-facing, no user-visible copy.
Deviations from Design
Documented and accepted, no design violation:
- B8 (Phase B, Stations IO-fail split):
_archivoCustom()'s own path resolution was initially called BEFORE the new_leerContenidoCustomtry block, matching the task's literal scope ("readarchivo.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). - 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. - 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
- S1: add a dedicated corruption test for
_leerPresetsPorEmisora(EQ per-station presets) for defense-in-depth, even though residual risk is already very low. - 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.
- 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.
- 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
_corruptobackup-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-resiliencecapability) - #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.