# Archive Report: Alarm Volume Ramp & Device-Volume Immunity **Change**: alarm-volume-ramp-restore **Archived**: 2026-07-11 **Status**: ARCHIVED AND CLOSED (Phase 5 human QA gate pending — see below) **Archive Path**: `openspec/changes/archive/2026-07-11-alarm-volume-ramp-restore/` **Spec Merged To**: `openspec/specs/native-alarms/spec.md` (new capability domain — no prior main spec existed) --- ## Executive Summary The **alarm-volume-ramp-restore** change is archived with a **PASS WITH WARNINGS** verification verdict. Slices 2 (ring-scoped `STREAM_MUSIC` override + idempotent restore) and 3 (single fade-in driver across the native→Flutter handoff) are code-complete, fully test-covered where automatable, and static-analysis-clean (29/29 automatable tasks done, `flutter analyze` 0 issues). Slice 1 (FGS manifest/runtime-type fix) was **cancelled by design** after `sdd-apply` proved via `javap -constants` against the local Android SDK that the target constants (`FOREGROUND_SERVICE_TYPE_ALARM`, `android.permission.FOREGROUND_SERVICE_ALARM`) do not exist — the existing `mediaPlayback|systemExempted` declaration was confirmed correct, and Root Cause B from the original exploration was withdrawn. Post-verify, the orchestrator closed WARNING-1 (a genuine, evidence-backed defect in the `dispose()` restore safety net) via commit `79f6f8e`. Phase 5 (8 mandatory human on-device QA tasks) remains the only outstanding work — it cannot be completed by any agent and is required before this change can be considered fully verified in production. --- ## Artifact Traceability | Artifact | Engram Observation ID | Location | Notes | |----------|----------------------|----------|-------| | Explore | (embedded, no separate obs) | `openspec/changes/.../explore.md` | Root causes A (architectural steady-state), B (manifest bug — later withdrawn), C (double fade-in) | | Proposal | #2302 | sdd/alarm-volume-ramp-restore/proposal | Scope (3 slices), approach, risks, rollback plan | | Spec | #2308 | sdd/alarm-volume-ramp-restore/spec; on-disk delta merged to `openspec/specs/native-alarms/spec.md` | 3 requirements, 10 scenarios; delta carries a 2026-07-11 correction banner re-scoping the FGS requirement after Slice 1 cancellation | | Design | #2310 | sdd/alarm-volume-ramp-restore/design | 9 architecture decisions, data flow, file changes, testing strategy; carries the Slice 1 cancellation correction banner | | Tasks | #2316 | sdd/alarm-volume-ramp-restore/tasks | 44 checkboxes: 29 complete (Phases 2/3/4/6), 7 N/A (Phase 1, cancelled by design), 8 pending (Phase 5, human QA) | | Apply-Progress | #2332 | sdd/alarm-volume-ramp-restore/apply-progress | Batches 1 (blocked/reverted), 2 (Slice 2), 2b (flutterOwnsRing risk fix), 3 (Slice 3 + final sweep) — all merged evidence | | Discovery | #2335 | (RESOLVED) PluriWaveAlarmService.stopAlarm() handoff-vs-exit ambiguity | Risk found in Batch 2, resolved in Batch 2b via the `flutterOwnsRing` flag | | Verify-Report | #2342 | sdd/alarm-volume-ramp-restore/verify-report | PASS WITH WARNINGS; 0 CRITICAL, 2 WARNING, 4 SUGGESTION; 6/10 scenarios COMPLIANT, 1 PARTIAL, 2 DEFERRED (manual QA), 1 ACCEPTED GAP | --- ## Implementation Completion ### Phase 1: FGS Manifest + Runtime Type Fix — CANCELLED (By Design, Not a Defect) | Phase | Description | Tasks | Status | Rationale | |-------|-------------|-------|--------|-----------| | 1 | Manifest `foregroundServiceType` + Kotlin runtime type fix (`alarm` variant) | 1.1–1.7 (7) | 0/7, CANCELLED | `sdd-apply` verified via `javap -constants` against local android-34/35/36 platform `android.jar`s, plus a full `FOREGROUND_SERVICE_TYPE_*` sweep of `api-versions.xml`, that `FOREGROUND_SERVICE_TYPE_ALARM` / `android.permission.FOREGROUND_SERVICE_ALARM` do not exist anywhere in the Android SDK (13 real FGS types exist through API 36; no `alarm` variant). The existing `mediaPlayback|systemExempted` declaration is the correct, intentional pattern — matches the prior `app-quality-and-native-alarms` T-S1-03/04 deviation. Edits were applied then reverted (`git checkout --`) before landing. Root Cause B from exploration is withdrawn; the user-visible bug is fully attributed to Root Cause A (media-stream steady state), addressed by Slices 2 and 3. | ### Phases 2–4, 6: Code-Gated Work — COMPLETE | Phase | Description | Tasks | Status | Evidence | |-------|-------------|-------|--------|----------| | 2 | Kotlin ring-scoped `STREAM_MUSIC` override/restore channel methods + `flutterOwnsRing` handoff-aware backstop (Batch 2b risk fix) | 2.1–2.9 (9) | 9/9 [x] | Code-inspection verified (no Android test harness in this repo); `flutter analyze` 0 issues | | 3 | Dart port + wiring: `forzarVolumenMediaParaAlarma`/`restaurarVolumenMedia`, restore call sites, override trigger point in `app.dart` | 3.1–3.12 (12) | 12/12 [x] | Strict TDD; 3.9 has one documented, justified deviation (not testable in isolation — verified via source inspection) | | 4 | Fade-in dedup: single `_iniciarFadeIn()` call site gated on handoff confirmation (success or failure, via `finally`) | 4.1–4.5 (5) | 5/5 [x] | Strict TDD; 22/22 tests green in the touched-suite re-run | | 6 | Final static sweep (`systemExempted` consistency check, full-directory test run, `dart format .`) | 6.1–6.3 (3) | 3/3 [x] | 279/279 tests green (hang-fallback split: 272 batched + 7 standalone); `flutter analyze` 0 issues | ### Phase 5: Manual/On-Device QA — PENDING (Human Gate) | Phase | Description | Tasks | Status | Notes | |-------|-------------|-------|--------|-------| | 5 | Manual/on-device QA (volume-0 audibility, Android 14+ service start, dismiss/snooze exact-value restore, no double-ramp at handoff, normal-playback regression check, kill-mid-ring residual gap, sign-off) | 5.1–5.8 (8) | 0/8 [ ] | Mandatory human sign-off gate; requires real/emulated Android 14+ device; Kotlin volume-override and handoff-timing paths are not unit-testable (code-inspected only); these 8 items are the ONLY remaining work for this change | **Task Summary**: - 29 tasks complete (`[x]`) across Phases 2, 3, 4, 6 - 7 tasks N/A (`[ ]` with justification) in Phase 1 — cancelled by design, ships no code - 8 tasks pending (`[ ]`) in Phase 5 — human gate - **Total: 44 checkboxes, 0 unaccounted** --- ## Test Results ### Unit/Widget Tests | Suite | Result | Notes | |-------|--------|-------| | `pantalla_alarma_sonando_test.dart` | PASS | Fade-in dedup gate pair + failure-edge test (Slice 3) | | `pantalla_alarma_sonando_dismiss_guard_test.dart` | PASS | Dismiss/snooze restore-once, dual-call-site no-double-restore, normal-playback regression guard (Slice 2); +1 post-verify dispose-as-sole-caller regression test | | `pantalla_alarma_sonando_scaffold_test.dart` | PASS | Re-run for regression confidence (shared fake extended) | | `servicio_alarmas_android_test.dart` | PASS | Channel-call contract tests for the two new wrapper methods | | `estado_alarmas_snooze_test.dart` | PASS | Included in verify session's targeted run | | **Targeted total (verify session, 2026-07-11)** | **34/34** | Independently re-run by `sdd-verify` | | **Post-verify fix total** | **35/35** | +1 test from commit `79f6f8e`'s dispose-as-sole-caller regression test | | **Full-directory batched run (apply Batch 3, task 6.2)** | **279/279** | 272 files batched + 2 known-flaky files (`estado_alarmas_ejecuciones_test.dart`, `servicio_grabacion_radio_test.dart`) run standalone due to a documented, pre-existing, unrelated full-suite hang | ### Static Analysis | Tool | Result | Coverage | |------|--------|----------| | `flutter analyze` | 0 issues | Whole project (verified independently by `sdd-verify` and again post-fix) | | `dart format .` | Clean for all touched files | 8 unrelated files show pre-existing formatter-version drift on `main`; reverted via `git checkout --` each time to stay in scope | | `rg 'systemExempted\|SYSTEM_EXEMPTED'` (Phase 6.1) | 3 references, all `systemExempted` (manifest x2, Kotlin x1), none `alarm` | Confirms manifest/runtime consistency post-cancellation, matching the pre-change state exactly | ### Code Coverage - Exact-line coverage metrics not collected (informational/non-blocking per verification scope). - Scenario-level mapping: 6/10 spec scenarios fully COMPLIANT with automated coverage; 1/10 PARTIAL (Dart-side gating green, native cross-process timing untestable by any agent); 2/10 DEFERRED (manual-QA-only by spec's own testability note); 1/10 ACCEPTED GAP (process-death residual, explicitly not required in automated coverage per spec text). --- ## Verification Summary ### Verdict and Post-Verify Fix | Pass | Result | Finding | |------|--------|---------| | Verify (2026-07-11) | **PASS WITH WARNINGS** | 0 CRITICAL. WARNING-1: `dispose()`'s restore call site is non-functional as a standalone safety net — verified via actual `flutter test` output, not speculation. Flutter's `Element.unmount()` flips the widget to `defunct` before `State.dispose()` runs, so a fresh `context.read()` inside `dispose()` throws `FlutterError` ("Looking up a deactivated widget's ancestor is unsafe") — caught and silently logged by the method's own try/catch, so no crash and no NAMED spec scenario fails (because `_silenciarAudio()` always runs first, while mounted, on every code path that reaches it), but `dispose()` provides zero real coverage for any abrupt teardown that bypasses `_detener()`/`_posponer()`. WARNING-2: task 3.9 (override-ordering test in `app.dart`) has no automated RED/GREEN pair — `PluriWaveApp` hardcodes non-injectable `EstadoRadio`/`EstadoAlarmas`, so the seam is unreachable via `flutter test`; verified via source inspection instead (accurate on re-check). | | Post-verify fix (orchestrator, commit `79f6f8e`) | **WARNING-1 CLOSED** | `EstadoAlarmas` is now captured in `initState()` instead of being freshly read via `context.read()` inside `dispose()`, and a new dispose-as-sole-caller regression test was added. 35/35 alarm suites green (was 34/34), `flutter analyze` 0 issues. This fix is not reflected as a numbered checkbox in `tasks.md` (it is an orchestrator-level post-verify fix); documented here and in `state.yaml` for the audit trail instead. WARNING-2 remains OPEN/ACCEPTED — a documented, justified deviation, not fixed. | ### Finding Breakdown | Severity | Count | Status | |----------|-------|--------| | CRITICAL | 0 | — | | WARNING | 2 | 1 CLOSED (WARNING-1, commit `79f6f8e`); 1 OPEN/ACCEPTED (WARNING-2, documented DI-seam gap) | | SUGGESTION | 4 | OPEN (non-blocking: apply-progress arithmetic drift, dispatch-note scenario miscount, pre-existing `dart format .` drift on unrelated files, pre-existing full-suite test hang) | ### Compliance Matrix Snapshot | Requirement | Scenarios | Compliant | Partial | Deferred (manual QA) | Accepted Gap | |-------------|-----------|-----------|---------|----------------------|--------------| | Manifest declares alarm-eligible FGS | 2 | 1 | 0 | 1 | 0 | | Ring-scoped device-volume override | 7 | 4 | 0 | 2 | 1 | | Single fade-in driver across handoff | 1 | 0 | 1 | 0 | 0 | | **Total** | **10** | **6** | **1** | **2** | **1** | Zero FAILING, zero UNTESTED-when-testable. --- ## File Changes Summary **Estimated Changed Lines**: 260–360 (per tasks.md Review Workload Forecast: manifest 0 lines shipped — Slice 1 cancelled — Kotlin 2 files, Dart 3 prod files, 3 Dart test files) **400-Line Budget Risk**: Medium (mitigated by the chained-slice work-unit structure: Slice 1 / Slice 2 / Slice 3, each independently rollback-isolated) **Build Execution**: Not run — `flutter build`/gradle explicitly forbidden throughout (Kotlin is code-inspection only per design) ### Kotlin (Code-Inspection Verified) - `android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt`: `@Volatile` companion state (`mediaVolumeOverridden`, `capturedMediaVolume`); `overrideMediaVolumeForRing`/`restoreMediaVolume` channel cases; `restoreMediaVolumeBestEffort()`; `flutterOwnsRing` set point in `confirmFlutterAudio` (Batch 2b) - `android/app/src/main/kotlin/es/freetimelab/pluriwave/PluriWaveAlarmService.kt`: `stopAlarm()`/`onDestroy()` backstop calls gated by `flutterOwnsRing`; flag declaration + reset in `startAlarm()` (Batch 2b); FGS type constants at L123-124 confirmed UNCHANGED (Slice 1 cancelled) ### Dart Production - `lib/servicios/servicio_alarmas_android.dart`: `forzarVolumenMediaParaAlarma(double)` / `restaurarVolumenMedia()` added to `PuertoAlarmasAndroid` + `ServicioAlarmasAndroid` impl - `lib/app.dart`: override call as the FIRST statement in `_prearrancarAudioAlarma`, before the `emisora == null` early return; `if (!mounted) return;` guard added (fixed a real `use_build_context_synchronously` finding) - `lib/pantallas/pantalla_alarma_sonando.dart`: `_volumenMediaRestaurado` guard + `_restaurarVolumenMediaUnaVez()`, wired into `_silenciarAudio()` and `dispose()`; fade-in dedup (removed 2 ungated call sites, moved the single call into `_confirmarAudioFlutterListo()`'s `finally` block); **post-verify (commit `79f6f8e`)**: `EstadoAlarmas` now captured in `initState()` instead of `dispose()`'s fresh `context.read()` ### Dart Tests - `test/servicios/servicio_alarmas_android_test.dart`: 2 new channel-call contract tests - `test/helpers/fakes_alarmas.dart`: `FakePuertoAlarmasAndroid` extended (`volumenForzado`, `volumenRestaurado`, `puertaConfirmarAudioFlutter` gate, `fallaConfirmarAudioFlutter`) - `test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart`: dismiss-restores-once, snooze-restores-once, dual-call-site-no-double-restore, normal-playback regression guard; **post-verify**: +1 dispose-as-sole-caller regression test - `test/pantallas/pantalla_alarma_sonando_test.dart`: fade-in gate pair (pre-confirm/post-confirm) + failure-edge test (native channel dead/never-there) ### Localization - `lib/l10n/*.arb` (13 files): **0 changes** — no new user-visible strings shipped by this change --- ## Deviations from Design **Documented and Signed Off**: 1. **Slice 1 cancellation (design correction, not an apply-time deviation)**: design.md and the delta spec both carry a 2026-07-11 correction banner. The two Architecture Decisions describing the FGS manifest/runtime-type change were built on SDK constants that do not exist (`FOREGROUND_SERVICE_TYPE_ALARM`, `android.permission.FOREGROUND_SERVICE_ALARM`), verified via `javap -constants`. Root Cause B is withdrawn; the existing `mediaPlayback|systemExempted` declaration is confirmed correct. 2. **Task 3.9 (Phase 3)**: no automated `flutter test` RED/GREEN pair exists — `PluriWaveApp` (the only public entry point) hardcodes non-injectable `EstadoRadio`/`EstadoAlarmas`, so `_prearrancarAudioAlarma`'s ring-start seam is unreachable via widget test without an unscoped DI refactor (out of scope for this design). Verified via source inspection instead; independently re-verified accurate during `sdd-verify`. 3. **`flutterOwnsRing` handoff-aware backstop (Batch 2b)**: not in the original design — added mid-apply after Batch 2 discovered that `stopAlarm()` fires identically at both a real ring exit AND the native-to-Flutter handoff moment, meaning the best-effort restore backstop specified by the original design could silence the Flutter-driven remainder of the ring right at handoff. This is a coherent evolution of the design (resolves a real risk found during implementation), not a harmful deviation — see discovery #2335 (RESOLVED). 4. **Post-verify fix (commit `79f6f8e`)**: not part of the original design or task list — added by the orchestrator after `sdd-verify` found WARNING-1 (dispose()'s restore call site non-functional as a standalone safety net). `EstadoAlarmas` is now captured in `initState()` per the verify report's own suggested fix direction, rather than left as a fresh `context.read()` inside `dispose()`. --- ## Risks and Mitigations | Risk | Likelihood | Status | |------|------------|--------| | Native handoff-timing premature restore (Batch 2 discovery) | Was Med | RESOLVED in Batch 2b via `flutterOwnsRing` flag; Phase 5 QA 5.1/5.5 remain the recommended on-device confirmation since code inspection cannot fully substitute for a real handoff-timing test | | App killed mid-ring leaves volume overridden | Low-Med | Documented, accepted residual gap (best-effort restore only); Phase 5 QA 5.7 is optional/best-effort verification | | Override leaks into normal radio playback | Low | Regression-guard test confirms the override channel is never invoked outside a ring; Phase 5 QA 5.6 is the on-device confirmation | | `dispose()` teardown paths that bypass `_detener()`/`_posponer()` (e.g. raw back-button pop) get no functional restore safety net | Was Med, now Low | CLOSED via commit `79f6f8e` (initState capture); no test currently exercises a raw back-button pop specifically, but the underlying defunct-context failure mode is eliminated | | Kotlin permission/volume flow has no automated test harness | Med | Code-inspection verified against the existing channel/idempotence pattern; Phase 5 manual QA is the runtime gate | | Task 3.9 override-ordering seam untestable in isolation | Low | Accepted, documented deviation; recommended follow-up is a testable DI seam on `PluriWaveApp` if genuine automated coverage is wanted later | --- ## Next Steps ### Mandatory Before Shipping 1. **Phase 5 Manual/On-Device QA** (pending, 8 tasks): Requires a real or emulated Android 14+ device. - Media volume 0 → alarm still audible for the full ring (5.1), specifically re-confirming the `flutterOwnsRing` handoff fix holds on-device - `PluriWaveAlarmService` starts without `ForegroundServiceTypeException` on Android 14+ from a background/killed-app broadcast context (5.2) - Dismiss/snooze restore media volume to the exact pre-alarm value (5.3, 5.4) - No audible double-ramp/volume jump at the native→Flutter handoff moment (5.5) - Normal radio playback unaffected by the override (5.6) - Optional/best-effort: force-kill mid-ring residual-gap check (5.7) - QA sign-off recorded (device model, Android version, pass/fail per scenario) (5.8) ### Follow-Up Opportunities (Out of Scope) - **WARNING-2 (open, accepted)**: task 3.9's override-ordering seam has no automated coverage. Add a testable DI seam to `PluriWaveApp` if genuine automated coverage is required. - **Fade-in constant/curve single-sourcing** (proposal's own noted debt): Kotlin and Dart fade-in implementations still duplicate the same algorithm/constants independently — flagged for future single-sourcing to prevent drift. - **Pre-existing `dart format .` drift** on 8 unrelated files (main branch) — already flagged by apply-progress as out-of-scope follow-up cleanup, still pending. - **Pre-existing full-suite `flutter test` hang** (environment/tooling issue, unrelated to this change, reproduced across 3 apply batches) — recommend separate investigation. - **Full native-only audio for the whole ring** (exploration option b, explicitly out of scope for this change) — remains a future architectural follow-up if the current dual-track handoff proves insufficient after Phase 5 QA. --- ## Archive Integrity **Attempted Move**: `openspec/changes/alarm-volume-ramp-restore/` → `openspec/changes/archive/2026-07-11-alarm-volume-ramp-restore/` **IMPORTANT — INCOMPLETE MOVE, TOOLING LIMITATION**: The `sdd-archive` executor for this change was launched with only `Read`, `Edit`, `Write`, `Glob`, and `mem_*` tools available — **no Bash/shell tool was provided**, so `cp` could not be run and the source folder could not be deleted. All six on-disk artifacts (`explore.md`, `proposal.md`, `design.md`, `tasks.md`, `verify-report.md`, `specs/native-alarms/spec.md`) were reconstructed into this archive folder via `Read`-verified `Write` calls — full content preserved, not summarized or paraphrased — but this is NOT a verified byte-identical filesystem copy in the way `cp` would guarantee, and the original source folder **still exists** at `openspec/changes/alarm-volume-ramp-restore/` alongside this archive copy. **Action required**: a human or an agent with shell access must run the equivalent of `Remove-Item -Recurse -Force openspec/changes/alarm-volume-ramp-restore` (PowerShell) or `rm -rf openspec/changes/alarm-volume-ramp-restore` (POSIX) to complete the move, then confirm the folder is gone. **Spec Merged**: `openspec/specs/native-alarms/spec.md` created as a new domain (no prior main spec existed for `native-alarms`). The delta's content was preserved verbatim for every requirement/scenario; only the framing was adapted from delta language ("ADDED Requirements", a "CORRECTED (2026-07-11...)" narrative note) into normative present-tense baseline-spec language, following this project's existing convention for freshly-created domain specs (see `openspec/specs/alarm-pre-notice-countdown/spec.md`). The archived copy of the delta (`specs/native-alarms/spec.md` under this archive folder) keeps the original delta framing unmodified, as the historical record — matching the `multi-device-eq` archive precedent. **Artifacts Preserved**: All 6 openspec files (explore, proposal, spec, design, tasks, verify-report) reconstructed in full, plus this `archive-report.md` and `state.yaml`. --- ## Sign-Off **Archive Ready For**: Documentation/traceability closure now; production shipping is PENDING Phase 5 human QA sign-off **Blocking Issues**: (1) Phase 5 is a known human gate, not a blocker to archiving. (2) The source folder deletion could not be performed by this executor — see "Archive Integrity" above; this DOES block calling the openspec move fully complete. **Recommendation**: Proceed to Phase 5 manual QA on a real/emulated Android 14+ device; separately, have an agent or human with shell access delete `openspec/changes/alarm-volume-ramp-restore/` to finish the archive move. --- ## Appendix: Observation IDs for Traceability - #2302: sdd/alarm-volume-ramp-restore/proposal (scope, 3 slices, approach, risks, rollback plan) - #2308: sdd/alarm-volume-ramp-restore/spec (3 requirements, 10 scenarios, correction banner) - #2310: sdd/alarm-volume-ramp-restore/design (9 architecture decisions, data flow, Slice 1 cancellation banner) - #2316: sdd/alarm-volume-ramp-restore/tasks (44 checkboxes, 4 batches, phase status) - #2332: sdd/alarm-volume-ramp-restore/apply-progress (Batches 1/2/2b/3 merged evidence, 29 tasks complete) - #2335: discovery — RESOLVED: `PluriWaveAlarmService.stopAlarm()` handoff-vs-exit ambiguity (Batch 2b `flutterOwnsRing` fix) - #2342: sdd/alarm-volume-ramp-restore/verify-report (PASS WITH WARNINGS, 0 CRITICAL, 2 WARNING, 4 SUGGESTION) - (new) sdd/alarm-volume-ramp-restore/archive-report — this document, persisted to Engram project `pluriwave` All artifacts archived and accessible at `openspec/changes/archive/2026-07-11-alarm-volume-ramp-restore/` for audit trail, pending the source-folder deletion noted above.