f3e9487215
- Use mediaPlayback|systemExempted FGS type with FOREGROUND_SERVICE_SYSTEM_EXEMPTED so alarms fire on Android 14+ (FOREGROUND_SERVICE_ALARM does not exist in the SDK) - Deduplicate fire notifications: the foreground service FSI notification is the single owner; receiver path removed - Notification channel v2 with alarm sound URI and USAGE_ALARM attributes, one-time guarded migration from legacy channels - Pass fallback station through the MethodChannel (NativeAlarmSpec schemaVersion 3) with a three-stage audio chain: primary -> fallback station -> bundled WAV - Native fade-in volume ramp honoring fadeInSegundos when the app is killed - Request battery-optimization exemption once, tracked with a persisted asked-once flag - Fix snooze end-to-end: native ACTION_SNOOZE now reports back to Flutter (snoozed event + cold-start sync), snooze anchor unified to occurrence+minutes on both sides, periodic recalc no longer erases an active snooze - Add snooze buttons (3/5/10/custom) to the ringing screen with shared audio teardown - Redesign ringing screen on PluriWaveScaffold with reduced-motion-aware entry animation (new PluriAnimate helper) - Alarm editor: live next-trigger preview, searchable station pickers (primary and fallback), configurable snooze duration, volume floor down to 0 - New alarm strings localized across all 13 locales - New unit/widget tests for the snooze flow, alarm bridge payloads, ringing screen and editor (77 tests green) - SDD artifacts for the app-quality-and-native-alarms change (explore, proposal, spec, design, tasks, apply progress)
180 lines
19 KiB
Markdown
180 lines
19 KiB
Markdown
# Apply Progress: app-quality-and-native-alarms
|
||
|
||
**Mode**: Strict TDD (test runner: `flutter test`)
|
||
**Artifact store**: openspec (Engram unavailable this session)
|
||
**Delivery**: auto-chain, local apply — no commits, no PRs (user commits at own cadence)
|
||
**Last updated**: 2026-06-11 (Batch 2)
|
||
|
||
## Batch log
|
||
|
||
| Batch | Slice | Status | Date |
|
||
|-------|-------|--------|------|
|
||
| 1 | S1 — Alarm native reliability | COMPLETE (Dart verified; Kotlin/manifest on-device verification deferred to user) | 2026-06-11 |
|
||
| 2 | S2a + S2b — Snooze correctness end-to-end + Alarm UX parity | COMPLETE (Dart verified; Kotlin on-device verification deferred to user) | 2026-06-11 |
|
||
|
||
## Task status (cumulative)
|
||
|
||
### Slice S1 — Alarm native reliability — 17/17 complete
|
||
|
||
| Task | Status | Notes |
|
||
|------|--------|-------|
|
||
| T-S1-01 | [x] | RED test: `programar()` payload carries `fallbackStationName/Url`, `fadeInSegundos`, `fallbackSound` |
|
||
| T-S1-02 | [x] | RED test: `solicitarExencionBateria()` invokes `requestIgnoreBatteryOptimizations` |
|
||
| T-S1-03 | [x] | Manifest FGS type + permission. **DEVIATION** (see below) |
|
||
| T-S1-04 | [x] | API ≥ 34 3-arg `startForeground` with type bitmask. **DEVIATION** (see below) |
|
||
| T-S1-05 | [x] | Receiver `showFireNotification` + `ensureFireChannel` removed; service notification (id 92841) is sole FSI owner; `fireNotificationIdForAlarm` kept for cancel-migration safety |
|
||
| T-S1-06 | [x] | `setFullScreenIntent` was ALREADY present in service `buildNotification`; `stopAlarm` already cancels legacy fire id + `stopForeground(STOP_FOREGROUND_REMOVE)`; FSI-before-audio ordering documented with comment |
|
||
| T-S1-07 | [x] | Channel `pluriwave_alarm_fire_v2` (IMPORTANCE_HIGH, `DEFAULT_ALARM_ALERT_URI` + USAGE_ALARM attrs, vibration); one-time deletion of `pluriwave_alarm_native` + `pluriwave_alarm_fire` guarded by `channels_migrated_v2` flag in device-protected prefs `pluriwave_alarm_channels`; channel count consolidated to 2 (fire_v2 + pre-notice) |
|
||
| T-S1-08 | [x] | `NativeAlarmSpec` + `fallbackStationName/Url`, `fadeInSegundos`; schemaVersion 2→3; `fromJson` backward-compatible (null/0 defaults); wired through `scheduleAlarm`, `MainActivity` handler, `EXTRA_*` consts, `fireIntent` extras |
|
||
| T-S1-09 | [x] | Three-stage chain primary(15s) → fallback station(15s) → bundled WAV via continuation lambdas (`onStageFailed`); `scheduleStationFallback` per stage with independent timeout windows |
|
||
| T-S1-10 | [x] | `startFadeIn` ramp: start 0.05×target, 250 ms steps over `fadeInSegundos`; applied to station and bundled-WAV players; `cancelFadeIn()` in `stopAlarm` (snooze path goes through `stopAlarm`) |
|
||
| T-S1-11 | [x] | `requestIgnoreBatteryOptimizations` MethodChannel handler + private method in `MainActivity.kt` mirroring `requestExactAlarmPermission` |
|
||
| T-S1-12 | [x] | GREEN: payload fields added to `programar()` args map |
|
||
| T-S1-13 | [x] | GREEN: `solicitarExencionBateria()` on `PuertoAlarmasAndroid` + impl |
|
||
| T-S1-14 | [x] | GREEN: asked-once guard in `_solicitarPermisosNecesariosParaAlarma` — calls only when `!diag.ignoraOptimizacionBateria` AND `bateria_exencion_solicitada` unset; optional `SharedPreferences? prefs` ctor param added to `EstadoAlarmas` (forward-compatible with S3 injection) |
|
||
| T-S1-15 | [x] | `flutter test` — full suite 54 tests, all passing (5 new) |
|
||
| T-S1-16 | [x] | `flutter analyze` — `No issues found!` (baseline before S1 was also clean) |
|
||
| T-S1-17 | [x] | `dart format` applied to the 4 touched Dart files |
|
||
|
||
### Slice S2a — Snooze correctness — 20/20 complete
|
||
|
||
| Task | Status | Notes |
|
||
|------|--------|-------|
|
||
| T-S2a-01 | [x] | RED: `test/estado/estado_alarmas_snooze_test.dart` — anchor, snoozed-event, recalc-preserve + extras (cold-start import, stop-cancels-snooze, finalizar clears) |
|
||
| T-S2a-02 | [x] | RED: `test/servicios/servicio_alarmas_snooze_test.dart` — anchor future/clamped/custom(7), payload `snoozeUntilMillis`+`snoozeOriginMillis`, `getNativeSnoozeState` parse/empty. Test C (finalizar) lives in the estado file |
|
||
| T-S2a-03 | [x] | RED: synchronous list update after `posponerAlarma` (no poll) |
|
||
| T-S2a-04 | [x] | Kotlin: `ACTION_SNOOZE` reports back via `MainActivity.notifyAlarmEvent` with `alarmAction="snoozed"` (`PluriWaveAlarmService.kt:56-80`). *On-device verify* |
|
||
| T-S2a-05 | [x] | Kotlin: `MainActivity.notifyAlarmEvent` companion (lines ~610-635), `@Volatile activeInstance` set in `configureFlutterEngine`, cleared in `onDestroy`; main-thread post; no-op when engine dead. *On-device verify* |
|
||
| T-S2a-06 | [x] | Kotlin: `AlarmScheduler.snooze()` (lines 266-292) unified to `occurrenceAt + minutes` clamped to `now + minutes`; persists `snoozeMinutes`; returns `NativeSnoozeResult(until, origin, title)`. *On-device verify* |
|
||
| T-S2a-07 | [x] | Kotlin: `AlarmScheduler.nativeSnoozeStates()` (lines 366-385) + `getNativeSnoozeState` handler (`MainActivity.kt:192`). *On-device verify* |
|
||
| T-S2a-08 | [x] | GREEN: `EventoAlarmaAndroid.snoozeUntilMillis` + `accionSnoozed`; `app.dart` ignores `snoozed` events in `_abrirAlarmaSonando` |
|
||
| T-S2a-09 | [x] | `snoozeUntilMillis` was ALREADY in the `programar()` payload — locked by new test, no code change |
|
||
| T-S2a-10 | [x] | GREEN: `_alRecibirEventoNativo` (estado_alarmas.dart:266) — `posponerEjecucionHasta` + `_aplicar` + `notifyListeners`, NO second `programar`. Subscribed in the CONSTRUCTOR (deviation, see below); cancelled in `dispose` |
|
||
| T-S2a-11 | [x] | GREEN: `_importarSnoozesNativosActivos` (estado_alarmas.dart:312) called at the end of `_sincronizarEjecucionesGestionadasPorAndroid`; imports active future snoozes for active alarms when they differ |
|
||
| T-S2a-12 | [x] | GREEN: `obtenerEstadoSnoozeNativo()` on `PuertoAlarmasAndroid` + impl + `EstadoSnoozeNativo` model |
|
||
| T-S2a-13 | [x] | GREEN: `_recalcular` `snoozeActivo` now requires `alarma.activa` (servicio_alarmas.dart:395) — disabling clears the snooze; finalizar path already cleared + re-programs without snooze (bridge cancels natively when inactive) |
|
||
| T-S2a-14 | [x] | RED: `test/pantallas/pantalla_alarma_sonando_test.dart` — buttons 3/5/10(+7), no-dup, tap-5 behavior |
|
||
| T-S2a-15 | [x] | GREEN: `_liberarAudioLocal()` + `_posponer(int)` + `_detener` refactor (pantalla_alarma_sonando.dart:138,161). `_estadoSub.cancel()` is fire-and-forget (deviation, see below) |
|
||
| T-S2a-16 | [x] | GREEN: snooze button row via `_opcionesSnooze()` (sorted {3,5,10,custom}); NEW l10n keys in ALL 13 .arb files: `alarmSnoozeOptionLabel`, `snoozeAction`, `alarmSnoozeDurationTitle`, `alarmFallbackStationLabel`, `alarmStationPickerSearchHint` (+ `flutter gen-l10n` regenerated) |
|
||
| T-S2a-17 | [x] | Targeted snooze tests green |
|
||
| T-S2a-18 | [x] | Full suite 77/77 |
|
||
| T-S2a-19 | [x] | `flutter analyze` — No issues found |
|
||
| T-S2a-20 | [x] | `dart format` on touched files |
|
||
|
||
### Slice S2b — Editor + visual redesign — 12/12 complete
|
||
|
||
| Task | Status | Notes |
|
||
|------|--------|-------|
|
||
| T-S2b-01 | [x] | RED: scaffold test — `PluriWaveScaffold` present, no `Color(0xFF061722)` Scaffold, `Animate` present / absent under `disableAnimations` |
|
||
| T-S2b-02 | [x] | RED: 5 editor tests (preview + live update, primary picker + filtering, fallback picker, snooze duration persists, volume floor 0.0) |
|
||
| T-S2b-03 | [x] | GREEN: ringing screen on `PluriWaveScaffold`; `0xFFFFB86B` → `tokens.warmCoral`; `blurSigma: 10` + cold-GPU comment (Design 2.4) |
|
||
| T-S2b-04 | [x] | GREEN: `lib/tema/pluri_animate.dart` — `pluriFadeIn`/`pluriScaleIn` honoring `MediaQuery.maybeDisableAnimationsOf` |
|
||
| T-S2b-05 | [x] | GREEN: glass surface wrapped in `.pluriFadeIn(context)` |
|
||
| T-S2b-06 | [x] | GREEN: `_vistaProximaEjecucion` (draft → `calcularProxima`, respects vacations/exceptions; recomputed on every setState) |
|
||
| T-S2b-07 | [x] | GREEN: `_CampoSelectorEmisora` + `_SelectorEmisoraSheet` (SearchBar picker) for primary AND fallback station; `copyWith` clear-flags added (see deviations) |
|
||
| T-S2b-08 | [x] | GREEN: snooze duration SegmentedButton wired to `snoozeMinutos` (editor used to hardcode 5); volume slider min 0.25 → 0.0 (divisions 20) |
|
||
| T-S2b-09 | [x] | S2b targeted tests 7/7 green |
|
||
| T-S2b-10 | [x] | Full suite 77/77 |
|
||
| T-S2b-11 | [x] | `flutter analyze` — No issues found |
|
||
| T-S2b-12 | [x] | `dart format` applied |
|
||
|
||
### Remaining slices (not started)
|
||
|
||
S3a, S3b, S7, S4a, S4b, S5, S6, cross-cutting (T-CC-01, T-CC-02) — all pending.
|
||
|
||
## Snooze defect fixes (design audit D1–D5 / S1–S5)
|
||
|
||
| Defect | Fix | Where |
|
||
|--------|-----|-------|
|
||
| D1 (audit S1) — native snooze never notifies Flutter | `ACTION_SNOOZE` → `MainActivity.notifyAlarmEvent("snoozed", origin, until)`; Flutter records via `posponerEjecucionHasta` WITHOUT re-programming; engine-dead case covered by `getNativeSnoozeState` cold-start import | `PluriWaveAlarmService.kt:56-80`, `MainActivity.kt:627`, `estado_alarmas.dart:266,312` |
|
||
| D2 (audit S2) — two snooze anchors | Unified everywhere to `occurrence + minutes` clamped to `now + minutes`: native `snooze()` adopts `postponeNext` logic; Dart `posponerEjecucion` re-anchored from `now+min` to `ejecucion+min` | `AlarmScheduler.kt:266-292`, `servicio_alarmas.dart:256-274` |
|
||
| D3 (audit S3) — no snooze on ringing screen | 3/5/10 + custom buttons → `_posponer` → shared `_liberarAudioLocal` teardown → canonical `EstadoAlarmas.posponerAlarma` (Flutter-first; hides native notification = same stop path) | `pantalla_alarma_sonando.dart:138-176,242-256` |
|
||
| D4 (audit S4) — recalc erases native-only snooze | Resolved transitively by D1: Flutter now RECORDS every native snooze, so `_recalcular` sees `snoozeActivo` and preserves it; regression-guard test added; additionally snooze cleared when alarm disabled (S2-R5) | `servicio_alarmas.dart:392-401`, test `estado_alarmas_snooze_test.dart` |
|
||
| D5 (audit S5) — preserveNativeSnooze origin mismatch | Resolved transitively by D1/D2: Flutter always sends `snoozeUntilMillis` when snoozed, so the preservation net is no longer load-bearing; kept as belt-and-suspenders per design | `AlarmScheduler.kt:preserveNativeSnooze` (unchanged) |
|
||
|
||
## TDD Cycle Evidence (Strict TDD hard gate)
|
||
|
||
| Task | RED (test written first, failing) | GREEN (implementation passes) | REFACTOR |
|
||
|------|-----------------------------------|-------------------------------|----------|
|
||
| T-S1-01/T-S1-12 | `servicio_alarmas_android_test.dart` written first; load failure + payload keys absent | Payload fields added; 3 tests pass | `dart format` |
|
||
| T-S1-02/T-S1-13 | Same RED run: `solicitarExencionBateria` undefined → compile failure | Interface + impl added; test passes | None needed |
|
||
| T-S1-14 | "solicita exencion una sola vez" FAILED (Expected 1, Actual 0) | Asked-once guard; both tests pass | Fake made configurable |
|
||
| T-S1-03..11 (Kotlin) | N/A — on-device items | N/A | Surgical diffs |
|
||
| T-S2a-01..03 / T-S2a-08..13 | All 3 new test files failed to LOAD (missing `EstadoSnoozeNativo`, `accionSnoozed`, `obtenerEstadoSnoozeNativo`, `alarmSnoozeOptionLabel`) — captured before any implementation. Anchor test would fail under old `now+min` semantics (verified by design: old code returned 7:05 vs expected 7:35) | All bridge/state/service changes added; targeted run 23/23 green | Shared `FakePuertoAlarmasAndroid` extracted to `test/helpers/fakes_alarmas.dart`; existing `estado_alarmas_test.dart` deduplicated |
|
||
| T-S2a-14..16 | Widget test load failure (l10n key missing) then tap-test failure (snoozeHasta null) | Buttons + `_posponer` implemented; 3/3 green | Debug prints removed; `unawaited` cancel documented |
|
||
| T-S2b-01..02 / T-S2b-03..08 | Scaffold tests failed (no `PluriWaveScaffold`/`Animate`); all 5 editor tests failed (key not found) — captured in dedicated RED run (0 passed / 7 failed) | `pluri_animate.dart`, scaffold migration, editor preview/pickers/snooze/volume; 7/7 green | `Material(transparency)` wrappers; initState-l10n fix; `dart format` |
|
||
|
||
RED run evidence: first run `+0 -3` (loading failures, three files); ringing tap test `Expected: DateTime:<07:35> Actual: <null>`; S2b run `+0 -7` before implementation. GREEN: targeted 23/23 then 7/7; full suite `00:24 +77: All tests passed!`.
|
||
|
||
## Files changed (Batch 2)
|
||
|
||
| File | Action | ~Lines |
|
||
|------|--------|--------|
|
||
| `android/.../AlarmScheduler.kt` | Modified | +57/-12 (unified snooze + `NativeSnoozeResult` + `nativeSnoozeStates`) |
|
||
| `android/.../MainActivity.kt` | Modified | +36/-1 (`notifyAlarmEvent` companion, `activeInstance`, `getNativeSnoozeState`) |
|
||
| `android/.../PluriWaveAlarmService.kt` | Modified | +19/-1 (snooze report-back) |
|
||
| `lib/servicios/servicio_alarmas_android.dart` | Modified | +60 (snoozed event, `EstadoSnoozeNativo`, bridge method) |
|
||
| `lib/servicios/servicio_alarmas.dart` | Modified | +20/-3 (anchor, activa-aware snooze clearing) |
|
||
| `lib/estado/estado_alarmas.dart` | Modified | +95/-15 (event subscription, snooze recording, cold-start import) |
|
||
| `lib/pantallas/pantalla_alarma_sonando.dart` | Modified | +70/-15 (snooze buttons, teardown, PluriWaveScaffold, tokens, fade-in) |
|
||
| `lib/pantallas/pantalla_alarmas.dart` | Modified | ~+330/-60 net (preview, pickers, snooze field, volume floor, initState fix, Material wrappers; large diff partly dart-format reflow) |
|
||
| `lib/modelos/alarma_musical.dart` | Modified | +10/-2 (`limpiarEmisora`/`limpiarEmisoraFallback`) |
|
||
| `lib/app.dart` | Modified | +7 (ignore snoozed events) |
|
||
| `lib/tema/pluri_animate.dart` | Created | +39 |
|
||
| `lib/l10n/app_*.arb` (13 files) | Modified | +12 each (5 keys + metadata) |
|
||
| `lib/l10n/gen/*` (15 files) | Regenerated | by `flutter gen-l10n` |
|
||
| `test/helpers/fakes_alarmas.dart` | Created | +120 |
|
||
| `test/helpers/fakes.dart` | Modified | +13 (`pausar`/`setVolumen` on FakeServicioAudio) |
|
||
| `test/estado/estado_alarmas_snooze_test.dart` | Created | +250 |
|
||
| `test/servicios/servicio_alarmas_snooze_test.dart` | Created | +155 |
|
||
| `test/pantallas/pantalla_alarma_sonando_test.dart` | Created | +165 |
|
||
| `test/pantallas/pantalla_alarma_sonando_scaffold_test.dart` | Created | +150 |
|
||
| `test/pantallas/pantalla_alarmas_editor_test.dart` | Created | +210 |
|
||
| `test/estado/estado_alarmas_test.dart` | Modified | -78/+8 (fake deduplicated to helper; anchor expectations 7:36:00 → 7:36:02) |
|
||
|
||
## Deviations from design (Batch 2)
|
||
|
||
1. **Event subscription lives in the `EstadoAlarmas` CONSTRUCTOR**, not `inicializar` (task text said inicializar). Rationale: snoozed events must be recorded even before/without full init, and tests with `iniciarAutomaticamente: false` stay light. Cancelled in `dispose`.
|
||
2. **`posponerEjecucion` clamps minutes to 1..120 instead of coercing to 3/5/10.** The old `calcularSnooze` coercion would have made the custom snooze button (e.g. "7 min", S2-R1-C) silently snooze 5. The native notification path still sanitizes to 3/5/10 (`sanitizeSnoozeMinutes`), unchanged. `calcularSnooze` kept (unused by this path) for API compatibility.
|
||
3. **Existing test expectation updated** (`estado_alarmas_test.dart`): unified anchor makes snooze land at `proximaEjecucion(+inminencia normalization) + 5min` = 7:36:02, not `now+5` = 7:36:00. This is the spec'd behavior change (S2-R6), documented inline.
|
||
4. **`_recalcular` now clears snooze for INACTIVE alarms** — required by S2-R5-A ("snoozeHasta is null in persistent storage" after disable); previously a disabled alarm kept a stale snoozeHasta forever.
|
||
5. **`AlarmaMusical.copyWith` gained `limpiarEmisora`/`limpiarEmisoraFallback`** (not in task text). Without them the picker's "no station" could never clear an existing station (latent pre-existing bug: copyWith null-coalesced).
|
||
6. **Pre-existing debug crash fixed**: `_EditorAlarmaSheetState.initState` called `AppLocalizations.of(context)` → `dependOnInheritedWidgetOfExactType` assert in debug builds. Name controller now created lazily in `didChangeDependencies`. (The sibling `_EditorVacacionesSheet` has the same latent issue — NOT fixed here, out of S2 scope; flag for S5/S6.)
|
||
7. **`Material(type: transparency)` wrappers** added inside `PluriGlassSurface` for the editor sheet and the station picker — ListTiles inside a DecoratedBox trigger a debug assert and invisible ink splashes otherwise.
|
||
8. **`_liberarAudioLocal` does NOT await `_estadoSub.cancel()`** — a broadcast-subscription cancel future may not resolve until the stream closes (observed in tests); cancellation of delivery is synchronous, so fire-and-forget (`unawaited`) is correct and prevents the snooze tap from stalling.
|
||
9. **`AlarmScheduler.snooze` returns `NativeSnoozeResult`** (until/origin/title) so the service can build the report-back payload; `postponeNext` untouched (already had the unified anchor).
|
||
10. **Ringing screen `blurSigma` capped to 10** (PluriGlassSurface default 18) as the Design 2.4 cold-GPU mitigation, plus reduced-motion users skip the entry animation entirely.
|
||
|
||
## Issues found
|
||
|
||
- `flutter test` does NOT auto-run gen-l10n in this setup despite `generate: true`; `flutter gen-l10n` must be run manually after editing .arb files (gen files are committed).
|
||
- `tester.tap` + an awaited broadcast-subscription `cancel()` deadlocks the gesture handler chain in widget tests (see deviation 8) — worth remembering for S3/S7 work.
|
||
|
||
## On-device verification checklist for the user
|
||
|
||
From tasks.md Section 11 — S1 items still pending from Batch 1, plus new S2 items:
|
||
|
||
1. **Kotlin compiles** — native layer edited again without compilation (HIGH risk): build FIRST.
|
||
2. Alarm fires app-killed on Android 14+ (S1-R1/R2); channel v2 on alarm stream (S1-R3); 3-stage fallback (S1-R4); native fade (S1-R6); battery dialog once (S1-R5); reboot persistence.
|
||
3. **Snooze from ringing screen (S2-R1, S2-R4)**: ring → tap "5 min" → notification dismissed, list shows snoozeHasta immediately, re-fires at that time.
|
||
4. **Snooze from notification while app killed (S2-R3)**: tap "Posponer" on the fire notification with the app killed → system alarm icon persists → reopen the app → list shows the snooze WITHOUT waiting for the 60 s poll (cold-start `getNativeSnoozeState` import).
|
||
5. **Snooze from notification while app foregrounded (S2-R3)**: same, but the list updates within the same frame via the `snoozed` MethodChannel event.
|
||
6. **Stop cancels pending snooze (S2-R5)**: snooze → disable the alarm from the list → does NOT re-fire.
|
||
7. **Ringing screen visuals (S2-R7)**: PluriWaveScaffold gradient + entry fade; verify no first-frame stutter on screen-off FSI wake (blur capped); with "remove animations" accessibility setting the entry is instant.
|
||
8. **Editor (S2-R8..R11)**: next-trigger preview updates live; searchable pickers for primary AND backup station; snooze duration control; volume slider reaches 0%.
|
||
|
||
## Verification summary (Batch 2)
|
||
|
||
- `flutter test`: 77/77 passing (54 pre-batch + 23 new)
|
||
- `flutter analyze`: No issues found (identical to baseline)
|
||
- `dart format`: applied to all touched Dart files only (gen/ untouched by hand)
|
||
- `flutter gen-l10n`: run once after .arb edits
|
||
- `flutter build`: NOT run (forbidden)
|
||
|
||
## Workload / boundary
|
||
|
||
- Mode: auto-chain local slices (no PRs)
|
||
- Current work units: S2a + S2b (complete)
|
||
- Boundary: starts from S1-complete tree; ends with S2a+S2b fully checked off, suite green. Rollback = revert the Batch-2 files listed above (S1 files only touched additively in `AlarmScheduler.kt`/`MainActivity.kt`/`PluriWaveAlarmService.kt`).
|
||
- Next batch: S3a (test seams) — prerequisite: user performs on-device verification for S1+S2 Kotlin, especially compile.
|