Files
pluriwave/openspec/changes/archive/2026-07-11-alarm-volume-ramp-restore/archive-report.md
T
FreeTLab efbf289f6b
Build & Deploy PluriWave / Análisis de código (push) Successful in 39s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m48s
docs(openspec): archive alarm-volume-ramp-restore and promote native-alarms spec
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.
2026-07-11 10:32:07 +02:00

22 KiB
Raw Blame History

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.11.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 24, 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.12.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.13.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.14.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.16.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.15.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: 260360 (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.