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.
24 KiB
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.3–3.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 (D1–D11), 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.3–3.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.1–1.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.1–2.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 ~300–390 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.1–3.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 1–2; 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.3–3.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 ~550–650 forecast, actual 714 (76+/638-); WU2 ~300–390 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);computeFadeVolumepure fn +FADE_TICK_MILLIS/FADE_RANGE_DBconsts;fadeAnchorElapsedMs/fadeLoopRunnablefields;startFadeLoop()/cancelFadeLoop()(replacingstartFadeIn/initialVolume/fadeInRunnable);requestAlarmAudioFocus()/abandonAlarmAudioFocus(); channel renamed_v2→_v3,setSound(null, null)added (post-verify fix, line 691),migrateLegacyChannels()deletes a 3rd legacy id; deletedflutterOwnsRing+ both backstop sites; class doc comment rewritten away from split-ownership descriptionandroid/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt: deletedconfirmFlutterAudio/overrideMediaVolumeForRing/restoreMediaVolumemethod-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 staticwidget.alarmaconfig per design D8lib/app.dart: deleted_prearrancarAudioAlarma+_volumenInicialFadeInAlarmas+ call site +audioPrearrancadoarglib/servicios/servicio_alarmas_android.dart: removedconfirmarAudioFlutter/forzarVolumenMediaParaAlarma/restaurarVolumenMediafromPuertoAlarmasAndroid+ impl
Dart Tests (WU1, compile-coupled with production)
test/helpers/fakes_alarmas.dart: removed the 3 override methods + related fieldstest/servicios/servicio_alarmas_android_test.dart: deleted 2 tests for the removed methodstest/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 assertionstest/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 (programadasis non-empty at test start because_buildEnv()already seeds oneandroid.programarcall)test/pantallas/pantalla_alarma_sonando_scaffold_test.dart: dropped theaudioPrearrancadoarg, 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:
- CRITICAL-1 root cause (task-phrasing deviation, corrected post-verify):
tasks.mdtask 2.7 introduced a flawed premise ("delete the setSound(...) block entirely — no setSound call = silent") that was NOT present indesign.mdADR D4, which correctly specifiessetSound(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. - 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.
- WU2 slightly over forecast (expected, documented): WU2 landed at 406 changed lines vs. the ~300–390 forecast — attributed to tasks 2.8/2.9 being compile-coupled (deleting
flutterOwnsRingin 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_ALARMknob (like Google Clock), not an absolute level. - Media volume 0 → the alarm still rings (native plays on
STREAM_ALARM, independent ofSTREAM_MUSIC). STREAM_ALARMat 0 → silent, same as the system clock (accepted; no override adopted).- The EQ pipeline does NOT apply during the ring (native
MediaPlayerhas 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
- Phase 3 Manual/On-Device QA (pending, tasks 3.3–3.11, 9 substantive checklist items): Requires a real device (e.g. POCO X7 Pro).
- Audible exponential dB fade at a configured fade of 15–30s (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_ALARM0 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 audiofocus-abandon hygiene check (3.11, added post-verify)
flutter test/flutter analyzefinal sweep (3.1–3.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-levelPARTIAL_WAKE_LOCK. Not a prerequisite for this change; independently plannable.- SUGGESTION-1: prune the 3 now-dead l10n getters from the 13
.arbfiles if a future cleanup pass wants to. - SUGGESTION-2: add a JVM/Kotlin unit-test harness (
android/app/src/test/+ JUnit) socomputeFadeVolume— 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.mdSec.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.3–3.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 D1–D11, 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.