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

230 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.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 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.