Close the SDD cycle: verified pass with warnings (0 critical), slice 1 cancelled with SDK evidence, slices 2-3 shipped, post-verify dispose fix landed. The native-alarms capability spec is promoted to openspec/specs/ with the corrected FGS requirement. Phase 5 on-device QA remains the pending human gate.
22 KiB
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.jars, 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 |
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<EstadoAlarmas>() 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:@Volatilecompanion state (mediaVolumeOverridden,capturedMediaVolume);overrideMediaVolumeForRing/restoreMediaVolumechannel cases;restoreMediaVolumeBestEffort();flutterOwnsRingset point inconfirmFlutterAudio(Batch 2b)android/app/src/main/kotlin/es/freetimelab/pluriwave/PluriWaveAlarmService.kt:stopAlarm()/onDestroy()backstop calls gated byflutterOwnsRing; flag declaration + reset instartAlarm()(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 toPuertoAlarmasAndroid+ServicioAlarmasAndroidimpllib/app.dart: override call as the FIRST statement in_prearrancarAudioAlarma, before theemisora == nullearly return;if (!mounted) return;guard added (fixed a realuse_build_context_synchronouslyfinding)lib/pantallas/pantalla_alarma_sonando.dart:_volumenMediaRestauradoguard +_restaurarVolumenMediaUnaVez(), wired into_silenciarAudio()anddispose(); fade-in dedup (removed 2 ungated call sites, moved the single call into_confirmarAudioFlutterListo()'sfinallyblock); post-verify (commit79f6f8e):EstadoAlarmasnow captured ininitState()instead ofdispose()'s freshcontext.read()
Dart Tests
test/servicios/servicio_alarmas_android_test.dart: 2 new channel-call contract teststest/helpers/fakes_alarmas.dart:FakePuertoAlarmasAndroidextended (volumenForzado,volumenRestaurado,puertaConfirmarAudioFluttergate,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 testtest/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:
- 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 viajavap -constants. Root Cause B is withdrawn; the existingmediaPlayback|systemExempteddeclaration is confirmed correct. - Task 3.9 (Phase 3): no automated
flutter testRED/GREEN pair exists —PluriWaveApp(the only public entry point) hardcodes non-injectableEstadoRadio/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 duringsdd-verify. flutterOwnsRinghandoff-aware backstop (Batch 2b): not in the original design — added mid-apply after Batch 2 discovered thatstopAlarm()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).- Post-verify fix (commit
79f6f8e): not part of the original design or task list — added by the orchestrator aftersdd-verifyfound WARNING-1 (dispose()'s restore call site non-functional as a standalone safety net).EstadoAlarmasis now captured ininitState()per the verify report's own suggested fix direction, rather than left as a freshcontext.read()insidedispose().
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
- 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
flutterOwnsRinghandoff fix holds on-device PluriWaveAlarmServicestarts withoutForegroundServiceTypeExceptionon 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)
- Media volume 0 → alarm still audible for the full ring (5.1), specifically re-confirming the
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
PluriWaveAppif 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 testhang (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 2bflutterOwnsRingfix) - #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.