Files
pluriwave/openspec/changes/archive/2026-07-12-native-alarm-ring/archive-report.md
T
FreeTLab 41b95fed44
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m45s
docs(openspec): archive native-alarm-ring and update the native-alarms spec
Close the SDD cycle for the ring architecture replacement: verified
with one critical (channel silence by omission) fixed and re-checked
before archive, delta merged into the main native-alarms spec (2
requirements removed, 5 added), artifacts archived byte-for-byte.
Phase 3 on-device QA (9 items) remains the mandatory human gate.
2026-07-12 12:36:22 +02:00

24 KiB
Raw Blame History

Archive Report: Native Alarm Ring

Change: native-alarm-ring Archived: 2026-07-12 Status: ARCHIVED AND CLOSED (Phase 3 on-device human QA gate pending — see below) Archive Path: openspec/changes/archive/2026-07-12-native-alarm-ring/ Spec Merged To: openspec/specs/native-alarms/spec.md (existing capability domain — merge, not creation)


Executive Summary

The native-alarm-ring change is archived with an effective verdict of PASS WITH SUGGESTIONS, contingent on the still-pending Phase 3 on-device QA gate. This is an architecture replacement: after 5 consecutive on-device failures under a split Dart/native media-stream handoff design, the ring was rebuilt native-only to AOSP DeskClock's AsyncRingtonePlayer pattern. PluriWaveAlarmService is now the SOLE ring-audio owner for the entire ring on STREAM_ALARM; the Flutter ringing screen is pure UI. The change shipped as two stacked work-unit commits: WU1 (bd7f883, Dart pure-UI ring screen + reduced port, +76/638) and WU2 (a69f397, pushed as 884567b, Kotlin native rebuild, +159/247). sdd-verify found the change FAIL (1 CRITICAL, 1 WARNING, 3 SUGGESTION); both actionable findings were closed by the orchestrator post-verify — CRITICAL-1 via a one-line fix (commit 3f0267f, pushed as 6f07e27) and WARNING-1 by adding two QA checklist items (tasks 3.10, 3.11). All 3 SUGGESTIONs are accepted-documented, non-blocking. The only remaining work is the mandatory human on-device QA checklist (tasks 3.33.11), which cannot be executed by any agent.


Artifact Traceability

Artifact Engram Observation ID Location Notes
Explore (embedded, no separate obs) openspec/changes/.../explore.md Consolidates 3 research briefs (reliable-delivery, audio-architecture, code map); supersedes retired media-stream/handoff design after 5 on-device failures
Proposal #2388 sdd/native-alarm-ring/proposal; on-disk proposal.md (full, authoritative) Scope, do-not-touch list, risks, rollback, 3-unit review-workload estimate
Spec #2389 sdd/native-alarm-ring/spec; on-disk delta merged to openspec/specs/native-alarms/spec.md 5 ADDED requirements / 10 scenarios, 2 REMOVED requirements / 8 scenarios
Design #2390 sdd/native-alarm-ring/design; on-disk design.md (full, authoritative) 11 ADRs (D1D11), 2-unit Dart-first work-split with explicit reversal justification
Tasks #2391 sdd/native-alarm-ring/tasks; on-disk tasks.md (full, authoritative) See "Tasks/Engram Divergence" note below — on-disk copy includes 2 post-verify checklist additions not present in obs #2391
Apply-Progress #2392 sdd/native-alarm-ring/apply-progress (engram-only, no separate on-disk file) Both work units complete; TDD Cycle Evidence table for WU1 (collapsed cycle) and WU2 (no harness)
Verify-Report #2393 sdd/native-alarm-ring/verify-report; on-disk verify-report.md (full, authoritative) FAIL — 1 CRITICAL, 1 WARNING, 3 SUGGESTION; preserved verbatim as the historical record of the pre-fix state

Tasks/Engram Divergence (documented for traceability)

The on-disk tasks.md (copied verbatim into this archive) contains checklist items 3.10 ("Real upgrade-path channel migration") and 3.11 ("Focus hygiene: dumpsys audio") in the Phase 3 QA section. These two items were added by the orchestrator after sdd-verify produced observation #2391/#2393, to close WARNING-1. Engram observation #2391 predates this addition and still shows only 3.33.9 (9 Phase 3 items instead of 11; 35 tasks total instead of 37). The on-disk file is authoritative per this archive run's instructions and was copied byte-for-byte; engram #2391 is left untouched as the pre-closure snapshot. This is the mirror-image of the divergence documented in the alarm-volume-ramp-restore precedent (there, a post-verify fix was NOT reflected in tasks.md; here, the post-verify closure WAS folded into tasks.md but not back into engram).


Implementation Completion

Phase 1: Work Unit 1 — Dart pure-UI ring screen + port reduction — COMPLETE

Phase Description Tasks Status Evidence
1 Pure-UI ring screen (pantalla_alarma_sonando.dart), reduced PuertoAlarmasAndroid port, app.dart pre-start deletion, fake/test rewrite 1.11.14 (14) 14/14 [x] Commit bd7f883 (rebase of 83e003a, identical tree/stat) on main. 8 files changed, 76 insertions(+), 638 deletions(-). Baseline 26/26 green → post-rewrite 16/16 green (10 deleted audio-mechanism tests). flutter analyze 0 issues.

Phase 2: Work Unit 2 — Kotlin native audio rebuild — COMPLETE

Phase Description Tasks Status Evidence
2 PluriWaveAlarmService.kt rebuild (dB-curve fade, manual audio focus, silent v3 channel, handoff-surface deletion) + MainActivity.kt surface deletion 2.12.12 (12) 12/12 [x] Commit a69f397, pushed as 884567b (identical tree/stat), base bd7f883 on main. 2 files changed, 159 insertions(+), 247 deletions(-) = 406 lines (slightly over the ~300390 forecast; expected — tasks 2.8/2.9 are compile-coupled). No Kotlin test harness exists in this project; verification is code-inspection + static rg sweeps (all 0 matches for every deleted/renamed symbol, exactly 1 match for the new legacy-channel-id constant).

Phase 3: Final Sweep + On-Device QA — PENDING (Human Gate)

Phase Description Tasks Status Notes
3 flutter test/flutter analyze final sweep + mandatory on-device QA checklist 3.13.11 (11) 0/11 [ ] Mandatory human sign-off gate; requires a real device (e.g. POCO X7 Pro). Tasks 3.10/3.11 added post-verify to close WARNING-1. The channel-silence fix (CRITICAL-1) is code-inspection-verified already; 3.6/3.10 are its on-device confirmation.

Task Summary: 26 tasks complete ([x]) across Phases 12; 11 tasks pending ([ ]) in Phase 3 (human gate, cannot be executed by any agent). Total: 37 checkboxes, 0 unaccounted (35 original + 2 post-verify additions).


Test Results

Unit/Widget Tests (Dart)

Suite Result
pantalla_alarma_sonando_test.dart PASS
pantalla_alarma_sonando_dismiss_guard_test.dart PASS
pantalla_alarma_sonando_scaffold_test.dart PASS
servicio_alarmas_android_test.dart PASS
Targeted total (verify session, re-run independently) 16/16 (26 baseline 10 deleted audio-mechanism tests)

Full-directory flutter test was intentionally NOT run — known pre-existing hang unrelated to this change (estado_alarmas_ejecuciones_test.dart, servicio_grabacion_radio_test.dart, per sdd-init/pluriwave cache and prior precedent in eq-audiofocus-reapply/persistence-corruption-guard).

Static Analysis

Tool Result
flutter analyze 0 issues, full repo (re-run independently by sdd-verify)
rg repo-wide symbol sweep 0 matches for every removed Dart symbol (confirmarAudioFlutter, forzarVolumenMediaParaAlarma, restaurarVolumenMedia, audioPrearrancado, _volumenInicialFadeInAlarmas, _prearrancarAudioAlarma) and every removed Kotlin symbol (flutterOwnsRing, confirmFlutterAudio, overrideMediaVolumeForRing, restoreMediaVolume*, startFadeIn, initialVolume(); setStreamVolume — 0 matches anywhere; pluriwave_alarm_fire_v2 — exactly 1 match (the new LEGACY_CHANNEL_FIRE_V2 migration constant)
This archive pass — re-verified setSound(null, null) presence directly Confirmed at PluriWaveAlarmService.kt line 691, inside ensureChannel()'s NotificationChannel(...).apply { ... } block, alongside enableVibration(true). The surrounding comment explicitly documents why the call is required (matches design D4 and the pre-notice channel precedent).

Code Coverage

  • No coverage tool configured in this project — informational only, not blocking (consistent with Strict TDD Mode rules).
  • Kotlin: no JVM/instrumented test harness exists for this module (computeFadeVolume, the one pure exactly-specified function in this change, has zero automated coverage — SUGGESTION-2, accepted).

Verification Summary

Verdict and Post-Verify Closures

Pass Result Finding
Verify (sdd-verify, obs #2393) FAIL (1 CRITICAL, 1 WARNING, 3 SUGGESTION) CRITICAL-1: the fire notification channel pluriwave_alarm_fire_v3 was constructed without setSound(null, null) — WU2 deleted the old setSound(DEFAULT_ALARM_ALERT_URI, ...) block but never added the silence-producing replacement Design ADR D4 explicitly requires. Root cause traced to tasks.md task 2.7's own flawed parenthetical ("no setSound call = silent"), which apply-progress and the code both inherited unquestioned. Android plays the platform default notification sound when setSound() is never called — proven within this codebase by the pre-notice channel's own explicit setSound(null, null) call. Practical effect: every alarm fire would have played a brief platform "ding" on top of the native ring's near-silent fade-in start, reintroducing a second audible source — the exact failure class this whole redesign exists to eliminate. WARNING-1: the Phase 3 QA checklist (3.33.9) did not explicitly itemize a real v2→v3 channel migration test on an upgrade device, nor a dumpsys audio focus-abandon check, even though the spec names both as valid testability methods. SUGGESTIONS (non-blocking): 3 dead l10n getters (expected per design D8); no JVM/Kotlin unit-test harness for computeFadeVolume; a minor teardown-order nitpick vs. the design's ASCII diagram (no functional effect).
Post-verify fix (orchestrator) CRITICAL-1 CLOSED Commit 3f0267f (pushed as 6f07e27): added .setSound(null, null) inside ensureChannel()'s NotificationChannel(...).apply { ... } block, next to the existing enableVibration(true). One-line fix, isolated, no other code affected — matches the verify report's own recommended fix exactly. Independently re-confirmed present at line 691 during this archive pass via direct file read. Re-verified via rg + flutter analyze (0 issues) per the orchestrator's post-verify note.
Post-verify fix (orchestrator) WARNING-1 CLOSED Tasks 3.10 (real v2→v3 upgrade-path migration test on a device with a pre-existing v2 channel) and 3.11 (adb shell dumpsys audio focus-abandon check) added to the Phase 3 checklist in tasks.md. Not reflected in engram observation #2391 (see "Tasks/Engram Divergence" above) — documented here and in state.yaml for the audit trail.
SUGGESTIONS Accepted-documented, not fixed Non-blocking per the verify report's own assessment; no code action taken.

Finding Breakdown

Severity Count Status
CRITICAL 1 CLOSED (commit 3f0267f/6f07e27)
WARNING 1 CLOSED (tasks 3.10/3.11 added)
SUGGESTION 3 OPEN/ACCEPTED (non-blocking: dead l10n getters, no Kotlin test harness, teardown-order documentation nitpick)

Compliance Matrix Snapshot (from verify-report, pre-fix)

Requirement Scenarios Compliant Partial Failing
Sole native ring-audio ownership 3 2 1 (audibility deferred to QA) 0 (was 1 — CRITICAL-1, since closed)
Exponential dB fade-in ceiling 2 2 0 0
Manual transient focus; no system volume writes 2 2 0 0
Notification channel migration v2 -> v3 1 1 0 0
Ring screen is pure UI 2 2 0 0
Total 10 9 1 0 post-fix

The one PARTIAL scenario ("Media volume 0 does not silence the ring") is correctly deferred to Phase 3 on-device QA by design (audibility cannot be verified by code-inspection alone), not a defect.

Both REMOVED requirements (ring-scoped device-volume override, single fade-in driver across handoff) confirmed mechanism-absent via exhaustive rg sweeps at 0 matches.


File Changes Summary

Estimated Changed Lines: WU1 ~550650 forecast, actual 714 (76+/638-); WU2 ~300390 forecast, actual 406 (159+/247-). Total actual: 1,120 changed lines, net deletion of ~403 lines. 400-Line Budget Risk: High, pre-approved exception (design Sec.8 proves WU1 cannot be split further — flutter test compiles the whole suite as one unit, so lib+tests must land in the same commit for a green PR). Chain Strategy: stacked-to-main, Dart-first (WU1 base main, WU2 base WU1 branch) — explicit reversal of the proposal's original Kotlin-first 3-unit suggestion, justified in design Sec.8 (Dart-first intermediate state is shippable with no double audio; Kotlin-first would have been broken).

Kotlin (Code-Inspection Verified, WU2 + post-verify fix)

  • android/app/src/main/kotlin/es/freetimelab/pluriwave/PluriWaveAlarmService.kt: new imports (SystemClock, AudioManager, AudioFocusRequest); computeFadeVolume pure fn + FADE_TICK_MILLIS/FADE_RANGE_DB consts; fadeAnchorElapsedMs/fadeLoopRunnable fields; startFadeLoop()/cancelFadeLoop() (replacing startFadeIn/initialVolume/fadeInRunnable); requestAlarmAudioFocus()/abandonAlarmAudioFocus(); channel renamed _v2_v3, setSound(null, null) added (post-verify fix, line 691), migrateLegacyChannels() deletes a 3rd legacy id; deleted flutterOwnsRing + both backstop sites; class doc comment rewritten away from split-ownership description
  • android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt: deleted confirmFlutterAudio/overrideMediaVolumeForRing/restoreMediaVolume method-channel cases + private fns + companion state (mediaVolumeOverridden, capturedMediaVolume, restoreMediaVolumeBestEffort())

Dart Production (WU1)

  • lib/pantallas/pantalla_alarma_sonando.dart: reduced to pure UI — deleted all audio orchestration (fade timer, fallback player, handoff confirmation, volume force/restore); status line re-sourced from static widget.alarma config per design D8
  • lib/app.dart: deleted _prearrancarAudioAlarma + _volumenInicialFadeInAlarmas + call site + audioPrearrancado arg
  • lib/servicios/servicio_alarmas_android.dart: removed confirmarAudioFlutter/forzarVolumenMediaParaAlarma/restaurarVolumenMedia from PuertoAlarmasAndroid + impl

Dart Tests (WU1, compile-coupled with production)

  • test/helpers/fakes_alarmas.dart: removed the 3 override methods + related fields
  • test/servicios/servicio_alarmas_android_test.dart: deleted 2 tests for the removed methods
  • test/pantallas/pantalla_alarma_sonando_test.dart: deleted 4 groups/tests (ramp-anchored, handoff-regression, mid-ramp-restore, dispose-restore); trimmed 2 survivors to drop obsolete audio assertions
  • test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart: deleted the media-volume-override-restore group (3 tests); rewrote the decoupling regression test with a grounding correction vs. design (programadas is non-empty at test start because _buildEnv() already seeds one android.programar call)
  • test/pantallas/pantalla_alarma_sonando_scaffold_test.dart: dropped the audioPrearrancado arg, no other change

Localization

  • lib/l10n/*.arb (13 files): 0 changes — no new user-visible strings. Three getters (alarmRingingFallbackActive, alarmRingingTryingStation, alarmRingingPreparingFallback) become dead code, expected per design D8 (SUGGESTION-1, accepted).

Deviations from Design

Documented and Signed Off:

  1. CRITICAL-1 root cause (task-phrasing deviation, corrected post-verify): tasks.md task 2.7 introduced a flawed premise ("delete the setSound(...) block entirely — no setSound call = silent") that was NOT present in design.md ADR D4, which correctly specifies setSound(null, null). Both the implementation and apply-progress's "silent by omission" framing inherited the flawed premise from the task, not from the design. The design itself was correct throughout; only the task breakdown deviated. Fixed post-verify by adding the missing call, matching design D4 exactly.
  2. Work-unit reversal (documented in design, not a deviation): the proposal originally suggested 3 units in Kotlin-first order; design Sec.8 consolidated to 2 units and reversed to Dart-first, with an explicit shippability justification for both intermediate states. This was a design-time decision, not an apply-time deviation.
  3. WU2 slightly over forecast (expected, documented): WU2 landed at 406 changed lines vs. the ~300390 forecast — attributed to tasks 2.8/2.9 being compile-coupled (deleting flutterOwnsRing in one file while its only setter still existed in the other would not compile), so no further split was possible.

Product Semantics (communicated to user, from exploration)

  • "Volumen 50%" = 50% relative to the device STREAM_ALARM knob (like Google Clock), not an absolute level.
  • Media volume 0 → the alarm still rings (native plays on STREAM_ALARM, independent of STREAM_MUSIC).
  • STREAM_ALARM at 0 → silent, same as the system clock (accepted; no override adopted).
  • The EQ pipeline does NOT apply during the ring (native MediaPlayer has no EQ pipeline); normal radio playback is unaffected.
  • Dismiss restores nothing, by construction — no system volume was ever written, so there is nothing to restore.

Risks and Mitigations

Risk Likelihood Status
Fire notification channel not actually silent (CRITICAL-1) Was High (proven defect, not speculative) CLOSED post-verify via commit 3f0267f/6f07e27; independently re-confirmed present in code during this archive pass
Kotlin not agent-compilable, no test harness exists High (structural) Code-inspection + static rg sweeps substituted throughout WU2; Phase 3 on-device QA is the real behavioral gate for fade audibility, volume ceiling, pop-free start, and focus resume
Ring screen visual regression Low Behavior unchanged by design; assertions kept, only setup rewritten; 16/16 tests green
_v3 channel bump resets ring-channel notification settings for existing users Low (expected, documented) Release note; pre-notice channel untouched; a reverted _v3 recreates _v2 harmlessly
Phase 3 QA checklist gaps (v2→v3 real-device migration test, dumpsys focus-abandon check) Was Medium CLOSED via tasks 3.10/3.11 addition (WARNING-1)
Assumption: OS focus policy replaces manual radio pause on all target devices Medium To be validated on POCO X7 Pro during Phase 3; a fallback would be a follow-up, not a blocker

Next Steps

Mandatory Before Shipping

  1. Phase 3 Manual/On-Device QA (pending, tasks 3.33.11, 9 substantive checklist items): Requires a real device (e.g. POCO X7 Pro).
    • Audible exponential dB fade at a configured fade of 1530s (3.3), volume ceiling correctness (3.4), no start pop on any of the 3 sources (3.5)
    • No second audible source — Dart plays nothing, fire notification posts silently (3.6) — this is the on-device confirmation of the now-fixed CRITICAL-1
    • Dismiss/snooze/back stop the ring, abandon focus, other-app audio resumes (3.7)
    • Media volume 0 still rings; STREAM_ALARM 0 silently accepted (3.8)
    • Screen-off full-screen-intent delivery from a killed app after HyperOS Autostart enabled (3.9)
    • Real v2→v3 channel migration on an upgrade device (3.10, added post-verify)
    • dumpsys audio focus-abandon hygiene check (3.11, added post-verify)
  2. flutter test/flutter analyze final sweep (3.13.2) — targeted 4-file suite already green; full-suite run subject to the known pre-existing unrelated hang.

Follow-Up Opportunities (Out of Scope, explicitly deferred by the proposal)

  • oem-reliability-guidance (planned follow-up change): in-app permission-status surface, MIUI/OEM autostart guidance + deep link, receiver-level PARTIAL_WAKE_LOCK. Not a prerequisite for this change; independently plannable.
  • SUGGESTION-1: prune the 3 now-dead l10n getters from the 13 .arb files if a future cleanup pass wants to.
  • SUGGESTION-2: add a JVM/Kotlin unit-test harness (android/app/src/test/ + JUnit) so computeFadeVolume — the one pure, exactly-specified function introduced by this change — gets automated regression coverage instead of relying on code-inspection + human QA indefinitely. Repo-wide gap, pre-dates this change.
  • SUGGESTION-3: align design.md Sec.2's ASCII diagram teardown order (documentation-only, no functional effect).

Archive Integrity

Copy method: this sdd-archive executor was launched with only Read, Edit, Write, Glob, and mem_* tools available — no Bash/shell tool. 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, then independently re-verified: for each file, the tail of the freshly re-read SOURCE file was compared line-by-line against the tail of the freshly re-read ARCHIVE COPY, confirming identical final line numbers and identical final-line content for all 6 files (explore.md: 56/56, proposal.md: 77/77, design.md: 324/324, tasks.md: 148/148, verify-report.md: 193/193, specs/native-alarms/spec.md: 114/114). Content preserved in full, not summarized or paraphrased.

Source folder NOT deleted by this executor — per this archive run's explicit instructions, the orchestrator (which has shell access) will delete openspec/changes/native-alarm-ring/ and diff-verify the two folders afterward. This differs from the alarm-volume-ramp-restore precedent (where the caveat was an unplanned tooling limitation); here it is the intended division of labor for this run.

Spec Merged: openspec/specs/native-alarms/spec.md — existing domain, merged (not created). Applied both REMOVALs (Ring-scoped device-volume override, 7 scenarios; Single fade-in driver across native-to-Flutter handoff, 1 scenario) and all 5 ADDED requirements (10 scenarios) verbatim from the delta. The untouched requirement (Manifest declares alarm-eligible foreground service) was preserved byte-for-byte. The ## Purpose paragraph was rewritten to describe the post-change architecture (sole native ownership, dB fade, manual focus, silent channel, pure-UI screen) — this is the only prose in the main spec that was authored rather than copied verbatim, and it is grounded strictly in the delta's own framing sentence and the 5 new requirement titles, adding no new claims.

Artifacts Preserved: all 6 openspec files (explore, proposal, spec delta, 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 3 human on-device QA sign-off (tasks 3.33.11). Blocking Issues: None for the archive operation itself. Phase 3 is a known, expected human gate — not a defect, not a blocker to archiving, consistent with this project's established pattern for native/Kotlin changes with no test harness. Recommendation: Proceed to Phase 3 manual QA on a real Android device (POCO X7 Pro or equivalent); separately, consider oem-reliability-guidance as the next planned change once Phase 3 sign-off is recorded.


Appendix: Observation IDs for Traceability

  • #2388: sdd/native-alarm-ring/proposal (scope, do-not-touch list, risks, rollback, review-workload estimate)
  • #2389: sdd/native-alarm-ring/spec (5 ADDED / 2 REMOVED requirements, 10 ADDED / 8 REMOVED scenarios)
  • #2390: sdd/native-alarm-ring/design (11 ADRs D1D11, 2-unit Dart-first work split with reversal justification)
  • #2391: sdd/native-alarm-ring/tasks (35 tasks at time of save; on-disk copy has 37 — see "Tasks/Engram Divergence" above)
  • #2392: sdd/native-alarm-ring/apply-progress (WU1 + WU2 complete, TDD Cycle Evidence table, commit hashes)
  • #2393: sdd/native-alarm-ring/verify-report (FAIL — 1 CRITICAL, 1 WARNING, 3 SUGGESTION; preserved as historical pre-fix record)
  • (new) sdd/native-alarm-ring/archive-report — this document, persisted to Engram project pluriwave

All artifacts archived and accessible at openspec/changes/archive/2026-07-12-native-alarm-ring/ for audit trail.