docs(openspec): archive alarm-volume-ramp-restore and promote native-alarms spec
Build & Deploy PluriWave / Análisis de código (push) Successful in 39s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m48s

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.
This commit is contained in:
2026-07-11 10:32:07 +02:00
parent 0804a612ec
commit efbf289f6b
9 changed files with 551 additions and 0 deletions
@@ -0,0 +1,229 @@
# 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.
@@ -0,0 +1,87 @@
change: alarm-volume-ramp-restore
archived_date: 2026-07-11
status: archived
archived_from: openspec/changes/alarm-volume-ramp-restore
verification_verdict: "PASS WITH WARNINGS"
verify_report_observations:
- id: 2342
type: verify-report
critical_issues: 0
warnings: 2 (WARNING-1 closed post-verify via commit 79f6f8e; WARNING-2 accepted, documented deviation)
suggestions: 4 (non-blocking)
artifacts_migrated:
- explore.md (root cause analysis A/B/C; B later withdrawn during apply-stage SDK verification)
- proposal.md (obs #2302)
- spec.md (obs #2308, on-disk delta merged to openspec/specs/native-alarms/spec.md as new domain)
- design.md (obs #2310, carries Slice 1 cancellation correction banner)
- tasks.md (obs #2316, 44 checkboxes: 29 done, 7 N/A Phase 1 cancelled by design, 8 pending Phase 5 manual QA)
- apply-progress.md (obs #2332, Batches 1/2/2b/3 complete; discovery #2335 RESOLVED)
- verify-report.md (obs #2342, PASS WITH WARNINGS)
phase_status:
Phase 1 (FGS Manifest + Runtime Type Fix): CANCELLED - 0/7 tasks, by design (target SDK constants confirmed fictional via javap -constants against android-34/35/36 platform jars + api-versions.xml sweep; existing mediaPlayback|systemExempted confirmed correct; Root Cause B withdrawn)
Phase 2 (Kotlin Volume Override Channel Methods): COMPLETE - 9/9 tasks [x]
Phase 3 (Dart Volume Override Port + Wiring): COMPLETE - 12/12 tasks [x]
Phase 4 (Fade-In Dedup at Handoff): COMPLETE - 5/5 tasks [x]
Phase 5 (Manual/On-Device QA): PENDING - 8 tasks [ ] (human gate, not executable by any agent)
Phase 6 (Final Static Sweep): COMPLETE - 3/3 tasks [x]
test_results:
total_changed_lines: "~260-360 estimated (Kotlin 2 files, Dart 3 prod files, 3 Dart test files); Slice 1 shipped 0 lines (cancelled by design)"
flutter_analyze: "0 issues (verify session, independently re-run)"
targeted_suites:
- pantalla_alarma_sonando_test.dart
- pantalla_alarma_sonando_dismiss_guard_test.dart
- pantalla_alarma_sonando_scaffold_test.dart
- servicio_alarmas_android_test.dart
- estado_alarmas_snooze_test.dart
total_tests_passing: "34/34 targeted (verify session, 2026-07-11); 279/279 full-directory batched run (apply Batch 3, task 6.2, hang-fallback split); 35/35 alarm suites after post-verify fix"
post_verify_fix:
commit: 79f6f8e
what: "WARNING-1 closed: EstadoAlarmas captured in initState() instead of a fresh context.read() inside dispose(), which was throwing (caught, silently logged) on a defunct-widget-context ancestor lookup for any teardown that bypasses _detener()/_posponer(); plus a new dispose-as-sole-caller regression test"
evidence: "35/35 alarm suites green, flutter analyze 0 issues"
reflected_in_tasks_md: false
notes: |
Phase 5 (manual/on-device QA) is a mandatory human gate before shipping, 8 tasks (5.1-5.8),
requires real or emulated Android 14+ device access. Cannot be completed by any agent.
Phase 1 (FGS manifest/runtime type fix) is CANCELLED by design, not a defect: sdd-apply
independently verified via `javap -constants` against local android-34/35/36 platform jars
plus a full FOREGROUND_SERVICE_TYPE_* sweep of api-versions.xml that FOREGROUND_SERVICE_TYPE_ALARM
and android.permission.FOREGROUND_SERVICE_ALARM do not exist anywhere in the Android SDK. The
existing mediaPlayback|systemExempted declaration is confirmed the correct, intentional pattern
(matches the prior app-quality-and-native-alarms T-S1-03/04 deviation). Root Cause B from
exploration is withdrawn; design.md and specs/native-alarms/spec.md both carry correction
banners documenting this. Slices 2 and 3 are unaffected and both code-complete.
WARNING-1 (dispose()'s restore call site non-functional as a standalone safety net, due to a
defunct-widget-context ancestor lookup thrown — and silently caught — whenever dispose() is the
first/only caller) was found during sdd-verify and CLOSED post-verify by the orchestrator via
commit 79f6f8e: EstadoAlarmas is now captured in initState() rather than re-read via
context.read() inside dispose(), plus a new dispose-as-sole-caller regression test. 35/35 alarm
suites green, flutter analyze 0 issues. This fix is NOT reflected as a new checkbox in tasks.md
(it is an orchestrator-level post-verify fix, not a numbered task) — documented here and in
archive-report.md instead.
WARNING-2 (task 3.9 has no automated RED/GREEN pair; app.dart's PluriWaveApp entry point
hardcodes non-injectable EstadoRadio/EstadoAlarmas instances, so _prearrancarAudioAlarma cannot
be exercised via flutter test in isolation) is accepted-documented, not fixed. Verified via
source inspection instead (forzarVolumenMediaParaAlarma(1.0) confirmed the first statement in
_prearrancarAudioAlarma). Recommended follow-up: add a testable DI seam to PluriWaveApp if
genuine automated coverage of this seam is required later.
Kotlin work (Phase 2 channel methods, Phase 2b flutterOwnsRing handoff-aware backstop) is
code-inspection-verified only (no Android test harness in this repo, flutter build/gradle
forbidden throughout). Native handoff-timing behavior will be validated during Phase 5 manual
QA, specifically tasks 5.1 and 5.5.
ARCHIVE TOOLING CAVEAT: the sdd-archive executor for this change had no Bash/shell tool
available (Read/Edit/Write/Glob/mem_* only) and could not run `cp` or delete the source folder.
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, preserving full content. The original `openspec/changes/alarm-volume-ramp-restore/`
folder was NOT deleted and still exists alongside this archive copy — a human or an agent with
shell access must remove it to complete the move (see archive-report.md "Archive Integrity").
@@ -0,0 +1,140 @@
# Verification Report
**Change**: alarm-volume-ramp-restore
**Version**: on-disk spec (`openspec/changes/alarm-volume-ramp-restore/specs/native-alarms/spec.md`), post-cancellation-correction, 2026-07-11
**Mode**: Strict TDD (Dart slices) + Standard/code-inspection (Kotlin slices, `flutter build` forbidden)
**Verified**: 2026-07-11, against commits `acd903d` (Slice 2) and `66a1952` (Slice 3); Slice 1 CANCELLED (see design.md correction banner)
## Completeness
| Metric | Value |
|--------|-------|
| Tasks total | 44 (Phase1:7 + Phase2:9 + Phase3:12 + Phase4:5 + Phase5:8 + Phase6:3) |
| Tasks complete | 29 (Phase2 9/9, Phase3 12/12, Phase4 5/5, Phase6 3/3) |
| Tasks incomplete | 15 (Phase1: 7 cancelled by design; Phase5: 8 mandatory human QA gate) |
| Automatable tasks (excl. Phase1-cancelled, Phase5-human-gate) | 29/29 = 100% |
Note: apply-progress #2332 self-reported "29/37 automatable" -- actual denominator recounts to 29/29 (100%) excluding Phase1 cancelled tasks, or 29/36 including them. Cosmetic arithmetic drift only, not a code issue.
## Build & Tests Execution
**Build**: N/A -- flutter build / gradle explicitly forbidden this pass (Kotlin = code-inspection only per design); not executed.
**Tests**: PASSED 34/34 (independently re-run this verify session)
```text
flutter test test/pantallas/pantalla_alarma_sonando_test.dart test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart test/pantallas/pantalla_alarma_sonando_scaffold_test.dart test/servicios/servicio_alarmas_android_test.dart test/estado/estado_alarmas_snooze_test.dart
...
00:04 +34: All tests passed!
```
Full-suite run intentionally not attempted -- documented, reproducible environment hang (estado_alarmas_ejecuciones_test.dart / servicio_grabacion_radio_test.dart under full-suite load only, unrelated to this change, already root-caused across Batches 2/2b/3). Targeted runs are the correct strategy here.
flutter analyze: 0 issues (verified independently this session).
**Coverage**: Not executed this pass (informational/non-blocking per strict-tdd-verify.md; task checklist specifies targeted tests + analyze only).
## Spec Compliance Matrix
(10 scenarios found on-disk; task briefing said 9, minor miscount, SUGGESTION only)
| Requirement | Scenario | Test | Result |
|-------------|----------|------|--------|
| Manifest declares alarm-eligible FGS | Manifest keeps the verified-correct FGS declaration | static grep, verified directly: AndroidManifest.xml:58 mediaPlayback\|systemExempted, PluriWaveAlarmService.kt:123-124 MEDIA_PLAYBACK or SYSTEM_EXEMPTED | COMPLIANT |
| Manifest declares alarm-eligible FGS | Native service starts from broadcast context on Android 14+ | none (manual QA only) | DEFERRED - Phase 5 task 5.2 |
| Ring-scoped device-volume override | Alarm is audible when device media volume is 0 | none automated (manual QA); code inspection supports plausibility | DEFERRED - Phase 5 task 5.1 |
| Ring-scoped device-volume override | Player fade-in ramps within the overridden level | pantalla_alarma_sonando_test.dart fade-in dedup tests, ramp reaches [0.05, 0.85] | COMPLIANT (pre-existing ramp mechanism, only gating changed; confirmed still reaches target) |
| Ring-scoped device-volume override | Dismiss restores the original captured volume | dismiss_guard_test.dart "detener: restaura el volumen de medios exactamente una vez" | COMPLIANT (Dart side); native exact-value restore deferred - Phase 5 task 5.3 |
| Ring-scoped device-volume override | Snooze restores the original captured volume | dismiss_guard_test.dart "posponer: restaura el volumen de medios exactamente una vez" | COMPLIANT (Dart side); deferred - Phase 5 task 5.4 |
| Ring-scoped device-volume override | Restore is idempotent across double-exit paths | dismiss_guard_test.dart "_silenciarAudio y dispose en secuencia no duplican la restauracion" plus Kotlin mediaVolumeOverridden guard | COMPLIANT (named scenario passes) - see WARNING-1 for a related-but-distinct dispose()-as-sole-caller gap NOT covered by this named scenario |
| Ring-scoped device-volume override | Normal radio playback never triggers the override | dismiss_guard_test.dart regression guard, play/pause never touches channel | COMPLIANT |
| Ring-scoped device-volume override | App killed mid-ring - best-effort restore only | none (accepted residual gap per spec own text) | ACCEPTED GAP; Kotlin backstop code-inspected (flutterOwnsRing-gated) |
| Single fade-in driver across handoff | No double-ramp interleaving at handoff | pantalla_alarma_sonando_test.dart gate pair plus failure-edge test | PARTIAL - Dart-side gating fully tested/green; native cross-process MediaPlayer timing explicitly untestable by this agent per spec own testability note - deferred Phase 5 task 5.5 |
Compliance summary: 6/10 fully COMPLIANT with automated coverage, 1 PARTIAL, 2 DEFERRED (manual-QA-only by spec design), 1 ACCEPTED GAP. Zero FAILING, zero UNTESTED-when-testable.
## Correctness (Static Evidence - Kotlin, code-inspection per design)
| Requirement | Status | Notes |
|------------|--------|-------|
| MainActivity Volatile companion state | Implemented | mediaVolumeOverridden: Boolean, capturedMediaVolume: Int? (L914-918) |
| overrideMediaVolumeForRing idempotent capture+max | Implemented | guard at top (L324-327), capture+set(max, flag 0) (L328-339) |
| restoreMediaVolume no-op guard | Implemented | early return when not overridden (L349-352), clears state in finally (L362-365) |
| restoreMediaVolumeBestEffort engine-dead-safe | Implemented | null-checks activeInstance, no-op+log when dead (L952-963) |
| PluriWaveAlarmService backstop gated on flutterOwnsRing | Implemented | stopAlarm() L384-386, onDestroy() L528-530 |
| flutterOwnsRing set BEFORE native stop in confirmFlutterAudio | Implemented | MainActivity.kt L169-170 |
| flutterOwnsRing reset in startAlarm() | Implemented | PluriWaveAlarmService.kt L97, after re-entrancy guard |
| Manifest/runtime FGS type unchanged (Slice 1 cancelled) | Confirmed unchanged | manifest:6 permission plus :58 type both still systemExempted; Kotlin:123-124 matches |
| _prearrancarAudioAlarma override is FIRST statement | Implemented | lib/app.dart L365-372, before mounted/emisora checks; only construction site of PantallaAlarmaSonando (grep-confirmed) |
| Single _iniciarFadeIn() call site | Implemented | lib/pantallas/pantalla_alarma_sonando.dart L152 only, inside _confirmarAudioFlutterListo() finally block |
## Coherence (Design)
| Decision | Followed | Notes |
|----------|-----------|-------|
| Manifest FGS combo (drop systemExempted, add alarm) | N/A - superseded | Correctly reverted/cancelled per design.md own correction banner; both files confirmed unchanged |
| Override ownership/state (Volatile on MainActivity companion) | Yes | |
| Stream reference level = max, fraction reserved | Yes | fraction accepted-but-unused, documented deviation |
| setStreamVolume flags = 0 | Yes | both override (L332) and restore (L357) |
| Restore idempotence (once-guarded) | Yes | both Kotlin (mediaVolumeOverridden) and Dart (_volumenMediaRestaurado) layers |
| Restore call sites: _silenciarAudio() plus dispose() | Partially | both wired as specified; dispose() call is non-functional as a standalone safety net when reached first - see WARNING-1 |
| Native backstop in stopAlarm/onDestroy | Yes, with a superseding refinement | Batch 2b flutterOwnsRing gating resolves a real risk found during implementation (engram #2335 RESOLVED); coherent evolution, not a harmful deviation |
| Override trigger point: app.dart, before early return | Yes | |
| Fade-in dedup gate via existing handoff seam | Yes | |
## TDD Compliance
| Check | Result | Details |
|-------|--------|---------|
| TDD Evidence reported | Yes | apply-progress #2332, Batch 2 (Phase 3) plus Batch 3 (Phase 4) tables |
| All tasks have tests | Mostly | 34/36 automatable tasks (excl. Phase1/Phase5) test-backed; task 3.9 has one explicit, justified deviation (source-inspection only, independently re-verified accurate) |
| RED confirmed (tests exist) | Yes | all referenced test files/cases independently re-read and confirmed present |
| GREEN confirmed (tests pass) | Yes | 34/34 passed on independent re-run this session |
| Triangulation adequate | Yes | 3.5-3.7 = 3 cases for 3 scenarios; 4.1+4.2+failure-edge = 3 cases for gate+failure behavior |
| Safety Net for modified files | Yes | 3.12 and 4.4 reruns reported; independently reconfirmed (34/34, this session) |
TDD Compliance: 5/6 checks fully passed (1 partial - task 3.9 documented, justified deviation)
## Test Layer Distribution (new tests added by this change)
| Layer | Tests | Files | Tools |
|-------|-------|-------|-------|
| Unit | 2 | 1 (servicio_alarmas_android_test.dart) | flutter_test + mock MethodChannel |
| Integration (Widget) | 6 | 2 (dismiss_guard_test.dart: 4, pantalla_alarma_sonando_test.dart: 2) | flutter_test testWidgets + Fakes |
| E2E | 0 | 0 | not installed/applicable |
| Total | 8 | 3 | |
## Assertion Quality
No CRITICAL/WARNING trivial-assertion violations found across the 8 new test cases. All assertions call production code and assert specific, non-trivial values (channel method+args, call counts, volume sequences).
Assertion quality: All assertions verify real behavior.
## Quality Metrics
Linter: No errors (flutter analyze: 0 issues)
Type Checker: No errors (covered by flutter analyze)
## Issues Found
### CRITICAL
None.
### WARNING
1. dispose() restore call site is non-functional as a standalone safety net (verified via actual test execution, not speculation). _restaurarVolumenMediaUnaVez() is called from dispose() via unawaited(...) (pantalla_alarma_sonando.dart L276). When it is the FIRST/ONLY caller (i.e. _volumenMediaRestaurado still false - any widget teardown that bypasses _detener()/_posponer()), its synchronous prefix evaluates context.read EstadoAlarmas - but by the time State.dispose() runs, Flutter Element.unmount() has already flipped the element lifecycle to defunct (this happens before dispose() is invoked, per Flutter framework ordering), so the ancestor lookup throws a FlutterError with the message "Looking up a deactivated widget ancestor is unsafe... save a reference... in didChangeDependencies()" - a well-documented Flutter/Provider anti-pattern. This exact message was observed repeating in the live flutter test output for every test that mounts and unmounts PantallaAlarmaSonando without a prior _silenciarAudio() call. The exception is caught by _restaurarVolumenMediaUnaVez() own try/catch and silently logged (no crash), so app stability is unaffected, and the named spec scenarios (Dismiss restores, Snooze restores, Restore is idempotent) all pass because _silenciarAudio() runs first, while the widget is still mounted, in every code path that reaches it (confirmed: PantallaAlarmaSonando has exactly one construction site, in app.dart, and both _detener()/_posponer() call _silenciarAudio() before triggering navigation pop). However, this means the design stated dual-restore-call-site strategy (invoke restore inside _silenciarAudio() AND again in dispose()) has only ONE functioning Dart-side path in practice; dispose() provides zero actual coverage for any abrupt teardown that bypasses _detener()/_posponer() (e.g. a raw back-button pop if Navigator.canPop() is true - not exercised by any current test). Not a named-spec-scenario failure (WARNING per Decision Gates: design deviation exists implies WARNING unless it breaks a spec), but a real, previously-undiscovered defect worth fixing or explicitly documenting as an accepted residual gap alongside the already-documented process-death gap. Suggested fix direction (not applied - verify does not fix issues): capture the PuertoAlarmasAndroid reference earlier (e.g. in didChangeDependencies()/_iniciarAlarma() into an instance field) so dispose() can invoke it without a fresh context.read().
2. Task 3.9 (override-ordering test in app.dart) has no automated RED/GREEN pair - explicitly documented deviation, verified via source inspection only. Independently re-verified this session: forzarVolumenMediaParaAlarma(1.0) is confirmed the first statement in _prearrancarAudioAlarma, claim is accurate. Recommend the already-noted follow-up (testable DI seam on PluriWaveApp) if genuine automated coverage is desired.
### SUGGESTION
1. apply-progress #2332 self-reported task count ("29/37 automatable") has a minor arithmetic drift vs the recount here (29/29 excl. Phase1, or 29/36 incl.) - cosmetic documentation nit only.
2. Task dispatch briefing said "9 scenarios"; on-disk spec has 10 - minor miscount in the dispatch note, not an artifact defect.
3. 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.
4. Pre-existing full-suite flutter test hang (environment/tooling issue, unrelated to this change) - already documented across 3 batches, recommend separate investigation.
## Verdict
PASS WITH WARNINGS
Slices 2 (ring-scoped volume override) and 3 (fade-in dedup) are code-complete, fully test-covered where automatable, and static-analysis-clean; Slice 1 is correctly cancelled and internally consistent with its corrected spec/design. Zero CRITICAL issues. Two substantive WARNINGs found (one is a genuine, newly-discovered, evidence-backed defect in the dispose() restore safety net - non-crashing, does not break any named spec scenario, but undermines part of the design intent and should be triaged before or shortly after archive). Phase 5 (8 mandatory human on-device QA tasks) remains entirely unchecked by design - required before this change can be considered fully verified in production; no agent can complete it.