# 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 7) ## 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 | | 3 | S3a + S3b — Test seams (statics/prefs/cache/mutex/dirty-guard/bounded set) + audio_session | COMPLETE (Dart-only batch; call-pause on-device verification deferred to user) | 2026-06-11 | | 4 | S7 — Streaming resilience (buffer config, reconnect state machine, UI wiring) | COMPLETE (Dart-only batch; stream-drop on-device verification deferred to user) | 2026-06-11 | | 5 | S4a — ServicioExportImport + EstadoEcualizador extraction + compat getters | COMPLETE (Dart-only batch) | 2026-06-11 | | 6 | S4b — EstadoGrabacion + EstadoBusqueda + scoped rebuilds + compat-getter removal | COMPLETE (Dart-only batch) | 2026-06-11 | | 7 | S5 — Design system, a11y, i18n, polish | COMPLETE (Dart-only batch) | 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 | ### Slice S3a — Test seams — 15/15 complete | Task | Status | Notes | |------|--------|-------| | T-S3a-01 | [x] | RED: `servicio_alarmas_android_instance_test.dart` — two channels, simulated `alarmFired` via `handlePlatformMessage`, isolation asserted both ways | | T-S3a-02 | [x] | RED: `servicio_alarmas_cache_test.dart` — `_PrefsEspia implements SharedPreferences` (setString/getString counters); no-write-when-clean, exactly-one-write-when-dirty, concurrent-no-lost-write (+ single cache hydration) | | T-S3a-03 | [x] | RED: `estado_alarmas_ejecuciones_test.dart` — 100 stale entries pruned (1 fresh survives) + 250-entry cap test | | T-S3a-04 | [x] | RED: `mini_reproductor_configurar_test.dart` — 10 rebuilds → 1 configurar; locale es→en → 2 | | T-S3a-05 | [x] | GREEN: `ServicioAlarmasAndroid` statics → instance fields; handler installed per instance in ctor. **DEVIATION:** no deprecated static shim (Dart name clash + only call site rewired in same change); `configurarLocalizaciones` added to `PuertoAlarmasAndroid` interface instead | | T-S3a-06 | [x] | GREEN: `MiniReproductor` → StatefulWidget, locale-guarded `didChangeDependencies`; alarm-bridge l10n hoisted to `app.dart` `_PaginaPrincipalState.didChangeDependencies` (design 3.3 alternative), before the early-return | | T-S3a-07 | [x] | GREEN: `main.dart` resolves prefs ONCE; `PluriWaveApp(prefs:)` → `EstadoRadio`/`EstadoAlarmas`(→`ServicioAlarmas`)/`EstadoIdioma` | | T-S3a-08 | [x] | GREEN: injected-with-fallback `_resolverPrefs()` in `estado_radio` (10 sites), `servicio_ecualizador` (6), `servicio_grabacion_radio` (4), `servicio_contenido_app` (3). rg check: only main.dart + one fallback per class remain | | T-S3a-09 | [x] | GREEN: `recalcularTodas` dirty-guard — serialized comparison vs `_cacheRaw`, skips write when identical | | T-S3a-10 | [x] | GREEN: `_cache`/`_cacheRaw` + `_enCola` writer queue; all 8 mutation methods queued over `_configActual()`. **DEVIATION:** public `cargar()` still re-reads prefs (queued cache reset) because `EstadoRadio.importarConfig` writes the raw alarms key directly — a fully cached cargar would hide imports until restart | | T-S3a-11 | [x] | GREEN: bounded `_ejecucionesEmitidas` — cap 200 + 24 h retention, pruned on every add and each `_vigilarAlarmasVencidas` pass; `@visibleForTesting` length getter | | T-S3a-12 | [x] | Targeted S3a tests green (RED first: 1 passed / 6 failed across the batch) | | T-S3a-13 | [x] | Full suite 89/89 (77 baseline + 12 new) | | T-S3a-14 | [x] | `flutter analyze` — No issues found | | T-S3a-15 | [x] | `dart format` on 19 touched files | ### Slice S3b — audio_session + intent flag — 7/7 complete | Task | Status | Notes | |------|--------|-------| | T-S3b-01 | [x] | RED: `servicio_audio_session_test.dart` — 5 tests (pause-begin, resume-end, no-resume-without-prior-pause, duck begin/end, becoming-noisy hard pause) over fake `ObjetivoAudioInterrumpible` | | T-S3b-02 | [x] | GREEN: `lib/servicios/servicio_audio_session.dart` — `music().copyWith(androidWillPauseWhenDucked: true)`; interruption + becoming-noisy subscriptions; `_pausadoPorInterrupcion` gate for auto-resume; defines `ObjetivoAudioInterrumpible` (test seam) | | T-S3b-03 | [x] | GREEN: `PluriWaveAudioHandler implements ObjetivoAudioInterrumpible`; `_intencionReproducir` true in `play()`/`playMediaItem()`, false in `pause()`/`stop()` (S7 seam); duck via `setAtenuado` ×0.3 (`_volumenEfectivo`); `configurar()` wired in `main.dart` | | T-S3b-04 | [x] | Targeted run 5/5 green (RED first: load failure) | | T-S3b-05 | [x] | Full suite 89/89 | | T-S3b-06 | [x] | `flutter analyze` — No issues found | | T-S3b-07 | [x] | `dart format` applied | ### Slice S7 — Streaming resilience — 13/13 complete | Task | Status | Notes | |------|--------|-------| | T-S7-01 | [x] | RED: `servicio_audio_reconnect_test.dart` — 8 tests over `ControladorReconexion` (backoff sequence + cap, retry scheduled with intent=true, NO retry with intent=false, exhaustion → agotado, restablecer resets counter + backoff base, cancelar kills pending timer) via injectable fake-timer factory | | T-S7-02 | [x] | RED: buffer-config test asserts `PluriWaveAudioHandler.configuracionCargaAndroid` values (15s/50s/2.5s/5s, prioritizeTime=true) — construction wiring not unit-testable without platform channels (see deviations) | | T-S7-03 | [x] | RED: `reconnect_ui_test.dart` — reconectando shows spinner + "Reconectando..." label, NO AlertDialog/SnackBar, NO manual-retry button; second test locks retry button to error state only | | T-S7-04 | [x] | GREEN: `_crearPlayer` passes `audioLoadConfiguration: configuracionCargaAndroid`; named `static const` durations. just_audio 0.9.46 API verified in pub-cache source — all design params exist, NO deviation | | T-S7-05 | [x] | GREEN: `EstadoReproduccion.reconectando` added; `estadoStream` maps the handler's `reconectando` flag (error wins, then reconectando, then cargando) | | T-S7-06 | [x] | GREEN: NEW `lib/servicios/controlador_reconexion.dart` (pure logic: maxReintentos=5, base=1s, cap=30s); handler enters reconnect on network-class errors (PlayerException 2xxx OR TimeoutException) with intent=play; retry re-issues source via revision-guarded `_cambiarFuente`; ready+playing resets; pause/stop/playMediaItem cancel/reset; exhaustion → single terminal error; `_cambiarFuente` returns normally when reconnect engaged so `EstadoRadio.reproducir` doesn't snackbar mid-retry | | T-S7-07 | [x] | GREEN: mini player (spinner + `playbackStatusReconnecting` in `_labelEstado`), full player (`_WaveHero`/`_Controles`: reconectando = loading, not error), visualizer stays active; l10n key in ALL 13 .arb locales + gen-l10n | | T-S7-08 | [x] | GREEN: S7-R4 boundary comment at `_estadoSub` listener — only `reproduciendo` cancels the alarm's 12s fallback timer; reconectando never counts as playing (code already correct, now documented + locked by enum distinctness) | | T-S7-09 | [x] | GREEN: `ServicioGrabacionRadio` untouched except S7-R5 invariant comment above `_fallar` | | T-S7-10 | [x] | Targeted run 10/10 green (RED first: `+0 -2` load failures) | | T-S7-11 | [x] | Full suite 99/99 (89 baseline + 10 new) | | T-S7-12 | [x] | `flutter analyze` — No issues found | | T-S7-13 | [x] | `dart format` on 9 touched files (2 reflowed); re-ran suite + analyze after format | ### Slice S4a — ServicioExportImport + EstadoEcualizador — 10/10 complete | Task | Status | Notes | |------|--------|-------| | T-S4a-01 | [x] | RED: `servicio_export_import_test.dart` — v2 round-trip deep-equal (favorites/groups/EQ/alarms/vacations, alarmas raw passthrough, "sin asignar" excluded) + malformed JSON → null (invalid/empty/non-object) | | T-S4a-02 | [x] | RED: `estado_ecualizador_test.dart` — `cambiarPreset` notifies EQ listeners; EQ change does NOT notify EstadoRadio listeners (radio = 0) | | T-S4a-03 | [x] | GREEN: `lib/servicios/servicio_export_import.dart` — `construirExportacion` (v2 envelope, exact legacy key set), `exportar` (pretty JSON), `importar` (graceful `Map?`). pantalla_ajustes lost `dart:convert`; EstadoRadio gained `exportarConfigJson`/`parsearConfigJson` | | T-S4a-04 | [x] | GREEN: `lib/estado/estado_ecualizador.dart` — full EQ state + persistence + audio application; `emisoraActualUuid` callback seam; `ListenableProvider` registration in app.dart (owned/disposed by EstadoRadio) | | T-S4a-05 | [x] | GREEN: EstadoRadio keeps 15 delegating members, all tagged `// TODO(S4b): remove getter`; EQ fields and private helpers removed | | T-S4a-06 | [x] | GREEN: `_SeccionEcualizador` → `Consumer2`; `ecualizador_widget.dart` already presentational (no change); pantalla_reproductor EQ toggle also rewired (deviation 3) | | T-S4a-07 | [x] | Targeted run 4/4 green (RED first: `+0 -2` load failures) | | T-S4a-08 | [x] | Full suite 103/103 (99 baseline + 4 new) | | T-S4a-09 | [x] | `flutter analyze` — No issues found | | T-S4a-10 | [x] | `dart format` on 8 touched files (4 reflowed); analyze + suite re-run after format | ### Slice S4b — EstadoGrabacion + EstadoBusqueda + scoped rebuilds — 13/13 complete | Task | Status | Notes | |------|--------|-------| | T-S4b-01 | [x] | RED: `estado_grabacion_test.dart` — 4 tests (notify on state change, iniciar delegates with current station, no-station → alError without service call, error state → alError) over a controlled ServicioGrabacionRadio fake | | T-S4b-02 | [x] | RED: `estado_busqueda_test.dart` — 3 tests (notify on buscar, pagination/memory cap MOVED from estado_radio_test, identity-stable `resultados` getter) | | T-S4b-03 | [x] | RED: `pantalla_inicio_rebuild_test.dart` — EQ preset change does NOT rebuild PantallaInicio (S4-R5-A), `debugPrintRebuildDirtyWidgets` probe + positive control (cargarPopulares DOES rebuild) | | T-S4b-04 | [x] | GREEN: `lib/estado/estado_grabacion.dart` — owns service, subscription, dir/maxBytes/open actions, `pluriwave/file_actions` channel; `emisoraActual`+`alError` seams; ListenableProvider in app.dart | | T-S4b-05 | [x] | GREEN: `lib/estado/estado_busqueda.dart` — search + nearby (cercanas) + min-bitrate filter; `ordenListas`/`textos`/`alError` seams; ListenableProvider in app.dart | | T-S4b-06 | [x] | GREEN: pantalla_inicio — no root watch; selects over identity-memoized getters (NEW `lib/estado/orden_emisoras.dart`: enum + sorter + MemoLista); cercanas/genre sections on EstadoBusqueda | | T-S4b-07 | [x] | GREEN: pantalla_ajustes 6 watch sites — Grabaciones → watch; Timer/Orden/Grupos/Preferida/Emisoras → context.select; _SeccionInfo keeps scoped Consumer | | T-S4b-08 | [x] | GREEN: pantalla_favoritos → selects; ALSO pantalla_buscar root watch → EstadoBusqueda and pantalla_reproductor `_GrabacionWidget` → EstadoGrabacion (mandatory: EstadoRadio no longer notifies on recording/search) | | T-S4b-09 | [x] | GREEN: estado_radio.dart — 15 compat members removed (zero `TODO(S4b)` in lib/), recording + search state extracted; 1121 (pre-split) → **753 lines** | | T-S4b-10 | [x] | Targeted run 8/8 green (RED first: `+0 -3` load failures) | | T-S4b-11 | [x] | Full suite 110/110 (103 baseline − 1 moved test + 8 new) | | T-S4b-12 | [x] | `flutter analyze` — No issues found | | T-S4b-13 | [x] | `dart format` on 15 touched files (10 reflowed); analyze + suite re-run after | ### Slice S5 — Design system, a11y, i18n — 18/18 complete | Task | Status | Notes | |------|--------|-------| | T-S5-01 | [x] | RED: `tarjeta_emisora_a11y_test.dart` — semantics label/button/toggled + ≥48dp. RED exposed a REAL bug: the card InkWell merged the favorite into ONE semantics node (screen readers could not reach the action independently) | | T-S5-02 | [x] | RED: `pluri_animate_test.dart` — fadeIn/scaleIn lock-in + NEW `pluriFadeSlideIn` (honest RED: method missing) | | T-S5-03 | [x] | RED: `pantalla_alarmas_fecha_test.dart` — en-US `6/11/2026`, NOT `11/06/2026`; es day/month | | T-S5-04 | [x] | RED: `pantalla_favoritos_plural_test.dart` — `stationCount` singular ≠ plural (en, es) | | T-S5-05 | [x] | RED: `pantalla_buscar_shimmer_test.dart` — shimmer during loading, NO spinner (cargando-true EstadoBusqueda subclass seam) | | T-S5-06 | [x] | RED: `notification_color_test.dart` — `configuracionAudioService.notificationColor == PluriWaveTokens.brand` | | T-S5-07 | [x] | GREEN: zero `Color(0x...)` outside lib/tema (rg audit). New static tokens `brand`/`brightCyan`/`auroraTeal`/`skyBlue`; scaffold gradient/orbs, alarmas glows, visualizer stop, premium orbs, tarjeta sweep mapped; Colors.grey/green/white semantic uses → colorScheme/tokens | | T-S5-08 | [x] | GREEN: favorite `Semantics(container: true, ...)` + 48×48 in both variants; `_AssetIcon.semanticLabel` (+`excludeFromSemantics` when decorative); alarm/vacation/ringing images labelled via NEW `alarmIconLabel`/`vacationIconLabel` | | T-S5-09 | [x] | GREEN: `pluriFadeSlideIn` added; ALL `flutter_animate` call sites routed through PluriAnimate (inicio chips/grid, buscar results, reproductor ×8); zero direct `.animate()` in lib/ | | T-S5-10 | [x] | GREEN: NEW `lib/l10n/formato_fechas.dart` (`fechaCortaLocalizada`); `_fechaCorta(l10n, fecha)` delegates via `l10n.localeName` (6 call sites) | | T-S5-11 | [x] | GREEN: `stationCount` ICU plural in ALL 13 locales (ru/ar full category sets); NEW `streamUrlHint` replaces hardcoded `stream.ejemplo.com`; gen-l10n run | | T-S5-12 | [x] | GREEN: shimmer rounded (radiusLg/6) + NEW `esCompacta` row variant; PantallaBuscar loading → 4 compact shimmer rows | | T-S5-13 | [x] | GREEN: 9 ajustes icons → `_rounded`; `*_outlined` family left (no rounded-outline variant exists); `_FormularioEmisora` sheet → `useSafeArea` + `showDragHandle` | | T-S5-14 | [x] | GREEN: `configuracionAudioService` top-level const; `notificationColor: PluriWaveTokens.brand` | | T-S5-15 | [x] | Targeted run 11/11 green (RED first: `+0 -6`) | | T-S5-16 | [x] | Full suite 121/121 (110 baseline + 11 new) | | T-S5-17 | [x] | `flutter analyze` — No issues found; color-literal audit ZERO | | T-S5-18 | [x] | `dart format` on 20 touched Dart files (7 reflowed); analyze + suite re-run after | ### Remaining slices (not started) S6, cross-cutting (T-CC-01, T-CC-02) — pending. S6 is now UNBLOCKED (depends on S4b + S5, both complete). ## 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: `; S2b run `+0 -7` before implementation. GREEN: targeted 23/23 then 7/7; full suite `00:24 +77: All tests passed!`. ### Batch 3 TDD Cycle Evidence (S3a + S3b) | Task | RED (test written first, failing) | GREEN (implementation passes) | REFACTOR | |------|-----------------------------------|-------------------------------|----------| | T-S3a-01/T-S3a-05 | Static controller shared events: `expect(eventosB, isEmpty)` FAILED ('solo-a' leaked into B) | Statics → instance fields; isolation test passes | Comment documenting handler re-bind semantics | | T-S3a-02-A/T-S3a-09 | `recalcularTodas` always wrote: `Expected: <1> Actual: <2>` writes | Dirty-guard skips clean writes | `_serializar` extracted, shared with `_guardar` | | T-S3a-02-B | Passed pre-fix (exactly-once lock-in guard) | Still passes (regression lock) | — | | T-S3a-02-C/T-S3a-10 | Lost write: final config had 1 of 2 alarms; 2 hydration reads | Cache + `_enCola` queue: both alarms persisted, 1 hydration read | Mutation bodies kept verbatim, only wrapped | | T-S3a-03/T-S3a-11 | Load failure: `ejecucionesEmitidasLength`/`maxEjecucionesEmitidas` undefined | Bounded set: 1 survivor of 101, cap respected | Prune helper shared by add-path and watch-pass | | T-S3a-04/T-S3a-06 | `Expected: <1> Actual: <11>` (configurar on every rebuild) | StatefulWidget + locale guard: 1 then 2 | FakeServicioAudio gained l10n override (assert fix) | | T-S3b-01/02/03 | Load failure: `servicio_audio_session.dart` missing | 5/5 green against fake objetivo | — | RED run evidence (Batch 3): `00:06 +1 -6` before implementation (the single pass is the exactly-once write lock-in). GREEN: targeted 12/12; full suite `00:12 +89: All tests passed!`. ### Batch 4 TDD Cycle Evidence (S7) | Task | RED (test written first, failing) | GREEN (implementation passes) | REFACTOR | |------|-----------------------------------|-------------------------------|----------| | T-S7-01/T-S7-06 | Load failure: `controlador_reconexion.dart` missing (`+0 -2` run) | `ControladorReconexion` created; 6 decision/backoff tests pass | Doc comments tying defaults to the ~60-90s design window | | T-S7-02/T-S7-04 | Same RED run: `configuracionCargaAndroid` undefined | Const config + `_crearPlayer` wiring; values test passes | Durations extracted as named `static const` | | T-S7-03/T-S7-05/T-S7-07 | Compile failure: `EstadoReproduccion.reconectando` missing | Enum + stream mapping + UI wiring; both widget tests pass | Test fixed to double-pump (stream event delivery + frame); diag run proved impl correct before the fix | RED run evidence (Batch 4): `00:00 +0 -2` (both files fail to load). GREEN: targeted `00:01 +10: All tests passed!`; full suite `00:08 +99: All tests passed!` (89 baseline + 10 new). ### Batch 5 TDD Cycle Evidence (S4a) | Task | RED (test written first, failing) | GREEN (implementation passes) | REFACTOR | |------|-----------------------------------|-------------------------------|----------| | T-S4a-01/T-S4a-03 | Load failure: `servicio_export_import.dart` missing (`+0 -2` run) | Service created; round-trip + malformed tests pass | Envelope comments tied to legacy format compatibility | | T-S4a-02/T-S4a-04/05 | Same RED run: `estado_ecualizador.dart` missing, `estado.ecualizador` undefined | EQ notifier + EstadoRadio delegation; both tests pass | `dart format` reflow; delegation kept expression-bodied | RED run evidence (Batch 5): `00:00 +0 -2` (both files fail to load — captured before any lib code). GREEN: targeted `00:00 +4: All tests passed!`; full suite `00:12 +103: All tests passed!` (99 baseline + 4 new); analyze + suite re-run after format. ### Batch 6 TDD Cycle Evidence (S4b) | Task | RED (test written first, failing) | GREEN (implementation passes) | REFACTOR | |------|-----------------------------------|-------------------------------|----------| | T-S4b-01/T-S4b-04 | Load failure: `estado_grabacion.dart` missing (`+0 -3` run) | EstadoGrabacion created; 4 tests pass | Comment ties callbacks to the S4a seam pattern | | T-S4b-02/T-S4b-05 | Same RED run: `estado_busqueda.dart` missing | EstadoBusqueda created; 3 tests pass | Pagination test deduplicated out of estado_radio_test | | T-S4b-03/T-S4b-06..09 | Same RED run: `estado.busqueda` undefined; then first GREEN attempt FAILED honestly (`Expected: true Actual: `) because the `element.dirty` probe cannot observe provider's deferred dependent notification | Probe rewritten over `debugPrintRebuildDirtyWidgets`; EQ change → screen NOT in rebuild log; cargarPopulares control → screen IS in log | Memo identity test added to estado_busqueda_test locking the select-enabler invariant | RED run evidence (Batch 6): `00:00 +0 -3` (all three files fail to load — captured before any lib code). GREEN: targeted 8/8; full suite `00:11 +110: All tests passed!` (103 baseline − 1 moved + 8 new); analyze + suite re-run after format. ### Batch 7 TDD Cycle Evidence (S5) | Task | RED (test written first, failing) | GREEN (implementation passes) | REFACTOR | |------|-----------------------------------|-------------------------------|----------| | T-S5-01/T-S5-08 | `find.bySemanticsLabel` found 0 nodes — the card InkWell MERGED the favorite into one node (real a11y defect, probed with a semantics dump) | `Semantics(container: true)` + 48dp target; node found, button/toggled flags pass | `hasFlag` (deprecated) → `flagsCollection` + `Tristate`; explicit `semantics.dispose()` (addTearDown fires too late for the handle check) | | T-S5-02/T-S5-09 | Compile failure: `pluriFadeSlideIn` undefined | Helper added; 4/4 animate tests green | — | | T-S5-03/T-S5-10 | Load failure: `formato_fechas.dart` missing | `fechaCortaLocalizada` + delegation; en-US/es tests pass | — | | T-S5-04/T-S5-11 | Compile failure: `stationCount` missing from AppLocalizations | ARB plural ×13 + gen-l10n; both locale tests pass | — | | T-S5-05/T-S5-12 | `TarjetaEmisoraShimmer` found 0, spinner found 1 | Compact shimmer variant + buscar loading swap | Shimmer block builder extracted (`bloque`) shared by both variants | | T-S5-06/T-S5-14 | Compile failure: `configuracionAudioService`/`PluriWaveTokens.brand` undefined | Const extraction + brand token; test passes | `electricMagenta` re-pointed at `brand` (no duplicate literal) | RED run evidence (Batch 7): `00:02 +0 -6` (4 compile/load failures + 2 honest assertion failures) captured before any lib code. GREEN: targeted 11/11; full suite `00:14 +121: All tests passed!`; analyze + suite re-run after `dart format`. ## Files changed (Batch 7) | File | Action | ~Lines | |------|--------|--------| | `lib/tema/pluriwave_tokens.dart` | Modified | +12 (static `brand`/`brightCyan`/`auroraTeal`/`skyBlue` token definitions; format reflow) | | `lib/tema/pluri_animate.dart` | Modified | +14 (`pluriFadeSlideIn`) | | `lib/l10n/formato_fechas.dart` | Created | +14 (locale-aware short date) | | `lib/main.dart` | Modified | +13/-8 (`configuracionAudioService` const, brand notification color) | | `lib/widgets/pluri_wave_scaffold.dart` | Modified | +8/-8 (gradient + orbs → tokens/colorScheme) | | `lib/widgets/tarjeta_emisora.dart` | Modified | +60/-30 (brightCyan, favorite container semantics + 48dp, rounded shimmer + compact variant) | | `lib/widgets/visualizador_audio.dart` | Modified | +2/-1 (warmCoral token) | | `lib/widgets/pluri_premium_widgets.dart` | Modified | +3/-2 (brightCyan) | | `lib/pantallas/pantalla_alarmas.dart` | Modified | +55/-30 (glow tokens, `_AssetIcon` semanticLabel, locale-aware `_fechaCorta`, vacaciones initState l10n fix) | | `lib/pantallas/pantalla_favoritos.dart` | Modified | +2/-1 (plural counter) | | `lib/pantallas/pantalla_buscar.dart` | Modified | +18/-7 (shimmer loading, pluriFadeSlideIn) | | `lib/pantallas/pantalla_ajustes.dart` | Modified | +20/-12 (rounded icons, semantic colors, l10n hint, sheet conventions) | | `lib/pantallas/pantalla_reproductor.dart` | Modified | +25/-20 (PluriAnimate routing ×8, white overlays → colorScheme/tokens) | | `lib/pantallas/pantalla_inicio.dart` | Modified | +6/-3 (PluriAnimate routing) | | `lib/l10n/app_*.arb` (13 files) | Modified | +4-5 each (`stationCount` plural, `alarmIconLabel`, `vacationIconLabel`, `streamUrlHint`) | | `lib/l10n/gen/*` (14 files) | Regenerated | by `flutter gen-l10n` | | `test/widgets/tarjeta_emisora_a11y_test.dart` | Created | +72 (1 test) | | `test/tema/pluri_animate_test.dart` | Created | +80 (4 tests) | | `test/tema/notification_color_test.dart` | Created | +17 (1 test) | | `test/pantallas/pantalla_alarmas_fecha_test.dart` | Created | +29 (2 tests) | | `test/pantallas/pantalla_favoritos_plural_test.dart` | Created | +21 (2 tests) | | `test/pantallas/pantalla_buscar_shimmer_test.dart` | Created | +42 (1 test) | Total Batch 7 lib diff: ~240 insertions / ~120 deletions (incl. ARB, excl. gen/), plus ~260 lines of new tests. Within the ~210-line slice estimate for hand-written lib changes. No Kotlin/native files touched. ## Deviations from design (Batch 7) 1. **`Semantics(container: true)` on the favorite button (not in task text).** The task only asked for `Semantics(button, label)` — which ALREADY existed. The RED test proved the real defect: the card-level InkWell merged the favorite into a single semantics node, so screen readers could not reach the action. `container: true` forces an own node; this is the actual S5-R2 fix. 2. **`fechaCortaLocalizada` lives in NEW `lib/l10n/formato_fechas.dart`** (task said edit `_fechaCorta` in place). The private top-level function is untestable from `flutter test`; the public helper takes a locale tag (testable without widgets) and `_fechaCorta(l10n, fecha)` delegates via `l10n.localeName`. Spec scenario S5-R4-A is met verbatim. 3. **Brand token is `PluriWaveTokens.brand`, not `brandColor`** (task text guessed the name). Defined as a `static const` so it works in the `const AudioServiceConfig` context; `electricMagenta` now references it (single source for 0xFF21D4D9). `brightCyan`/`auroraTeal`/`skyBlue` added the same way for palette colors that had no token. 4. **`Colors.white` overlays mapped to `colorScheme.onSurface`/`onPrimary` and `tokens.glassBorder`** — the scheme's `onSurface` is 0xFFF2F7FA (near-white), so visuals are intentionally near-identical while becoming theme-driven. 5. **`pluriScaleIn` on the hero adds a fade** the old bare `.scale()` did not have (the shared helper pairs fade+scale). Visually negligible at 420 ms; keeping one canonical scale-entry beats a third helper. 6. **Icon variants: only base-name icons got `_rounded`** (9 sites). The `*_outlined` Material family (folder_outlined, backup_outlined, upload/download_outlined, verified_outlined, music_note_outlined) has NO rounded-outline variant; switching them to filled `_rounded` would change their visual weight, so they stay outlined. 7. **`streamUrlHint` added as an l10n key** (the prompt's "handled per design"): the hardcoded hint leaked a Spanish-looking host (`stream.ejemplo.com`) into all 13 locales; the key ships the same neutral `stream.example.com` URL everywhere but is now localizable. 8. **`_EditorVacacionesSheet` initState l10n crash fixed** (flagged in Batch 2 as a latent debug-mode crash, explicitly brought into this batch's scope): controller now created lazily in `didChangeDependencies`, mirroring the alarm-editor fix. 9. **Pagination spinner in PantallaBuscar kept** — S5-R6 covers the initial loading state (explore C11, lines 241-245); the small inline load-more spinner is a different affordance and was left untouched. 10. **`flagsCollection`/`Tristate` instead of `SemanticsFlag.hasFlag`** in the a11y test — `hasFlag` is deprecated in the current SDK and `flutter analyze` flags it; also `tester.ensureSemantics()` must be disposed in the test body (an `addTearDown` runs after the framework's handle-leak check). ## 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) | ## Files changed (Batch 3) | File | Action | ~Lines | |------|--------|--------| | `lib/servicios/servicio_audio_session.dart` | Created | +115 (session config, interruption/noisy handling, `ObjetivoAudioInterrumpible`) | | `lib/servicios/servicio_alarmas.dart` | Modified | +106/-64 (cache, `_enCola` queue, dirty-guard, `_parsear`/`_serializar`) | | `lib/estado/estado_alarmas.dart` | Modified | +55/-2 (bounded set, `configurarLocalizaciones`, prefs→servicio default) | | `lib/servicios/servicio_alarmas_android.dart` | Modified | +18/-11 (statics → instance, interface method) | | `lib/servicios/servicio_audio.dart` | Modified | +52/-3 (intent flag, `ObjetivoAudioInterrumpible` impl, duck volume) | | `lib/estado/estado_radio.dart` | Modified | +16/-15 (prefs param + `_resolverPrefs`, static alarm-l10n call removed) | | `lib/widgets/mini_reproductor.dart` | Modified | +22/-2 (StatefulWidget, locale-guarded didChangeDependencies) | | `lib/main.dart` | Modified | +14/-2 (prefs once, ServicioAudioSession wiring) | | `lib/app.dart` | Modified | +22/-3 (PluriWaveApp.prefs, alarm l10n locale guard) | | `lib/servicios/servicio_ecualizador.dart` | Modified | +14/-6 (prefs injection) | | `lib/servicios/servicio_grabacion_radio.dart` | Modified | +13/-4 (prefs injection) | | `lib/servicios/servicio_contenido_app.dart` | Modified | +11/-3 (prefs injection) | | `test/helpers/fakes.dart` | Modified | +8 (`configurarLocalizaciones` override on FakeServicioAudio) | | `test/helpers/fakes_alarmas.dart` | Modified | +4 (interface no-op) | | `test/servicios/servicio_alarmas_android_instance_test.dart` | Created | +53 | | `test/servicios/servicio_alarmas_cache_test.dart` | Created | +105 | | `test/estado/estado_alarmas_ejecuciones_test.dart` | Created | +85 | | `test/widgets/mini_reproductor_configurar_test.dart` | Created | +85 | | `test/servicios/servicio_audio_session_test.dart` | Created | +130 | Total Batch 3 diff: ~362 insertions / ~122 deletions in lib + helpers, plus ~458 lines of new tests. ## Files changed (Batch 4) | File | Action | ~Lines | |------|--------|--------| | `lib/servicios/controlador_reconexion.dart` | Created | +100 (decision enum + backoff controller, injectable timer factory) | | `lib/servicios/servicio_audio.dart` | Modified | +148/-7 (enum `reconectando`, buffer config consts, reconnect integration, TimeoutException routing, pause/stop/play resets) | | `lib/widgets/mini_reproductor.dart` | Modified | +6/-2 (spinner for reconectando, label case) | | `lib/pantallas/pantalla_reproductor.dart` | Modified | +8/-2 (reconectando = loading in `_WaveHero` + `_Controles`) | | `lib/widgets/visualizador_audio.dart` | Modified | +2/-1 (reconectando keeps visualizer active) | | `lib/pantallas/pantalla_alarma_sonando.dart` | Modified | +5 (S7-R4 boundary comment) | | `lib/servicios/servicio_grabacion_radio.dart` | Modified | +4 (S7-R5 invariant comment) | | `lib/l10n/app_*.arb` (13 files) | Modified | +1 each (`playbackStatusReconnecting`) | | `lib/l10n/gen/*` (14 files) | Regenerated | by `flutter gen-l10n` | | `test/servicios/servicio_audio_reconnect_test.dart` | Created | +210 (8 tests) | | `test/widgets/reconnect_ui_test.dart` | Created | +100 (2 tests) | Total Batch 4 diff: ~235 insertions / ~13 deletions in lib (incl. ARB/gen), plus ~310 lines of new tests. Within the ~285-line slice estimate. No Kotlin/native files touched. ## Files changed (Batch 5) | File | Action | ~Lines | |------|--------|--------| | `lib/servicios/servicio_export_import.dart` | Created | +80 (v2 envelope builder, pretty export, graceful parse) | | `lib/estado/estado_ecualizador.dart` | Created | +205 (EQ ChangeNotifier: presets, per-station map, activo, persistence, audio application, import path) | | `lib/estado/estado_radio.dart` | Modified | +122/-208 (EQ state/methods extracted; 15 `// TODO(S4b)` delegating members; exportarConfig delegates envelope; importarConfig delegates EQ; exportarConfigJson/parsearConfigJson; ecualizador owned + disposed) | | `lib/pantallas/pantalla_ajustes.dart` | Modified | +33/-23 (backup section delegates JSON to service, `dart:convert` removed; `_SeccionEcualizador` → Consumer2 with EstadoEcualizador) | | `lib/pantallas/pantalla_reproductor.dart` | Modified | +8/-11 (EQ toggle watches EstadoEcualizador) | | `lib/app.dart` | Modified | +7 (ListenableProvider exposing EstadoRadio's instance) | | `test/servicios/servicio_export_import_test.dart` | Created | +85 (2 tests) | | `test/estado/estado_ecualizador_test.dart` | Created | +52 (2 tests) | Total Batch 5 diff: ~455 insertions / ~242 deletions in lib, plus ~137 lines of new tests. Slightly over the ~350-line slice estimate because the EQ method bodies moved (not duplicated) into the new notifier — net lib growth is ~+213. No Kotlin/native files touched. ## Files changed (Batch 6) | File | Action | ~Lines | |------|--------|--------| | `lib/estado/orden_emisoras.dart` | Created | +55 (OrdenEmisoras enum moved here + `ordenarEmisoras` + `MemoLista` identity memo; estado_radio re-exports the enum so existing imports keep compiling) | | `lib/estado/estado_grabacion.dart` | Created | +144 (recording notifier: service ownership, subscription, dir/maxBytes, open-file/dir actions, file_actions channel) | | `lib/estado/estado_busqueda.dart` | Created | +222 (search notifier: query/filters/pagination, cercanas + geolocation, min-bitrate filter, memoized sorted views) | | `lib/estado/estado_radio.dart` | Modified | +154/-375 net (recording/search/EQ-compat removed; memoized list getters; creates+disposes the 3 notifiers; custom-station mutations now reassign for memo identity) — **753 lines final (was ~1121 pre-split)** | | `lib/app.dart` | Modified | +10/-4 (ListenableProviders for EstadoGrabacion + EstadoBusqueda) | | `lib/pantallas/pantalla_inicio.dart` | Modified | ~+55/-41 (root watch removed; selects + EstadoBusqueda sections) | | `lib/pantallas/pantalla_buscar.dart` | Modified | ~+30/-27 (root watch → watch; renamed members) | | `lib/pantallas/pantalla_favoritos.dart` | Modified | ~+12/-3 (root watch → selects) | | `lib/pantallas/pantalla_ajustes.dart` | Modified | ~+50/-32 (Grabaciones → EstadoGrabacion; 5 sections → selects) | | `lib/pantallas/pantalla_reproductor.dart` | Modified | ~+18/-10 (_GrabacionWidget → watch) | | `test/estado/estado_grabacion_test.dart` | Created | +122 (4 tests) | | `test/estado/estado_busqueda_test.dart` | Created | +67 (3 tests) | | `test/pantallas/pantalla_inicio_rebuild_test.dart` | Created | +97 (1 test, S4-R5-A) | | `test/estado/estado_radio_test.dart` | Modified | EQ call sites → `estado.ecualizador.*`; pagination test moved out | | `test/pantallas/pantalla_inicio_test.dart` | Modified | `_conProviders` helper mirrors app.dart wiring (3 pump sites) | Total Batch 6 lib diff: ~386 insertions / ~625 deletions across 9 pre-existing files plus 3 new lib files (+421) and 3 new test files (+286). Net lib growth ≈ +180; EstadoRadio shrank by ~260 lines this batch. No Kotlin/native, .arb or gen/ files touched. ## Deviations from design (Batch 6) 1. **Provider ownership NOT inverted — documented as accepted.** Design 217 allows "pass the shared service instances at construction"; EstadoEcualizador/EstadoGrabacion/EstadoBusqueda need EstadoRadio's services AND callbacks (`emisoraActual`, `alError`, `ordenListas`, `textos`) at construction, so EstadoRadio creates and disposes all three and the ListenableProviders only expose the instances (S4a deviation 2 pattern, now final). Inverting would require lifting ServicioAudio/ServicioRadio creation into app.dart — out of slice budget and blast radius. 2. **NEW `lib/estado/orden_emisoras.dart` (not in task text).** Two reasons: (a) the `OrdenEmisoras` enum is needed by both EstadoRadio and EstadoBusqueda without a circular import (estado_radio re-exports it, so consumers compile unchanged); (b) `MemoLista` — derived-list getters used to return a fresh copy per read, which would make every `context.select` degrade to watch behavior (lists compare by identity). Identity-memoized getters are the enabler that makes S4-R5's "stop rebuilding on buffer events" REAL, not just formal. 3. **EstadoBusqueda also owns the nearby-stations (cercanas) flow** (task text only said query/results/loading). cercanas shares the min-bitrate filter and `radio.buscar` plumbing with search; leaving it in EstadoRadio would have kept a search-state remnant there against S4-R3's intent. 4. **`pantalla_buscar` and `pantalla_reproductor` rewired beyond the task list** (tasks named inicio/ajustes/favoritos). Mandatory, not optional: EstadoRadio no longer notifies on search or recording changes, so any screen still reading them through EstadoRadio would go permanently stale. Buscar now watches EstadoBusqueda; the player's `_GrabacionWidget` watches EstadoGrabacion. 5. **Custom-station mutations reassign the backing list** instead of mutating in place — required so the identity memo (and therefore `select`) sees the change. Behavior identical. 6. **`element.dirty` is NOT a valid rebuild probe with provider** — provider defers dependent notification to the next build phase (`markNeedsNotifyDependents` → inherited element rebuild → dependents marked during build). The widget test uses `debugPrintRebuildDirtyWidgets` log capture with a positive control instead. Worth remembering for future rebuild-scope tests. 7. **`emisorasDisponiblesPreferencia` staleness window (minor, accepted):** the preferred-station dropdown's option list now refreshes when favoritos/custom/populares/tendencias change identity, but a pure search/cercanas update no longer rebuilds the section (EstadoRadio does not notify on those anymore). The options re-derive on the section's next rebuild; preferred-station resolution itself prefers favorites, so impact is cosmetic. ## Deviations from design (Batch 5) 1. **`importar()` returns `Map?`, not a `ConfiguracionCompleta` model** (task text suggested one). EstadoRadio's `importarConfig(Map)` is the existing application API with v1/v2 branching and a localized version-guard error; introducing a typed model would force re-validating/re-mapping every section twice in a slice that must stay under budget. The service's contract (graceful null on malformed, version inside the map) covers S4-R4; a typed model can land with S4b/S6 if wanted. 2. **`ListenableProvider` instead of `ProxyProvider`** for EstadoEcualizador registration. The notifier needs `ServicioAudio` from EstadoRadio at CONSTRUCTION; EstadoRadio therefore constructs and disposes it (transition ownership), and the provider only exposes the instance (`create: ctx.read().ecualizador`, no dispose callback — avoids double-dispose). In S4b, when EstadoRadio sheds the remaining EQ surface, ownership can be inverted if desired. 3. **`pantalla_reproductor.dart` EQ toggle rewired in S4a** (task listed only ecualizador_widget + pantalla_ajustes). EstadoRadio no longer notifies on EQ changes (required by S4-R1-A test B), so any screen still reading EQ through EstadoRadio's compat getters under `watch` would go STALE on toggle. The reproductor button was the only such site; 8-line fix beats shipping a known visual bug until S4b. 4. **`ecualizador_widget.dart` needed NO change**: both widgets in it are presentational (preset/onCambio props, no provider reads), so T-S4a-06's intent (scoped consumption) is satisfied at the call sites in pantalla_ajustes. 5. **Compat getters do NOT relay EQ notifications to EstadoRadio listeners** — intentional and spec-mandated (S4-R1-A scenario). EQ-displaying UI was rewired in this same slice precisely because of this; S4b removes the getters entirely. 6. **`emisoraActualUuid` callback seam** on EstadoEcualizador (not in task text): per-station preset decisions need the currently playing station; a `String? Function()` injected by EstadoRadio keeps the notifier free of station-list coupling and trivially testable. ## Deviations from design (Batch 3) 1. **No deprecated static shim for `ServicioAlarmasAndroid.configurarLocalizaciones`** (task text asked for one). Dart forbids a static and an instance member with the same name in one class, and the ONLY external call site (`estado_radio.dart:74`) was rewired in this same slice — a renamed shim would be unreachable dead code. Instead `configurarLocalizaciones` joined the `PuertoAlarmasAndroid` interface (fakes no-op it). 2. **Alarm-bridge l10n configured from `app.dart`, not from `MiniReproductor`** — design 3.3 offered both options; the mini player now only configures its real dependency (`EstadoRadio`), and `_PaginaPrincipalState.didChangeDependencies` (locale-guarded, placed BEFORE the existing early-return) forwards l10n to `EstadoAlarmas`. 3. **Public `ServicioAlarmas.cargar()` re-reads from prefs instead of serving the cache.** `EstadoRadio.importarConfig` writes the raw `alarmas_musicales_v1` key directly to SharedPreferences; a fully cached `cargar()` would make a settings import invisible until app restart. Mutations DO use the cache (`_configActual`), which is what S3-R7's race fix and "one cargar per mutation burst" require. The re-read is queued, so it cannot interleave with a mutation. 4. **Duck handling added beyond the task text**: `setAtenuado` on the handler scales effective volume ×0.3 (restored on interruption end). With `androidWillPauseWhenDucked: true` Android delivers duck as pause, so this is mostly the iOS/edge path; kept minimal. 5. **`_PrefsEspia` implements SharedPreferences via noSuchMethod** rather than pulling `shared_preferences_platform_interface` into the tests — avoids a `depend_on_referenced_packages` lint on a transitive dep. 6. **`servicio_contenido_app.dart` also migrated** (3 getInstance sites; not named in the task). Its only construction site is `static final` in `pluri_onboarding_dialog.dart`, which keeps the fallback path at runtime — acceptable under the injected-with-fallback compat net; full injection there would require a dialog refactor out of S3 scope. 7. **Two-instances-same-channel semantics documented, not prevented**: with instance handlers, constructing a second `ServicioAlarmasAndroid` over the SAME MethodChannel re-binds the platform handler to the newest instance. Production creates exactly one per channel (provider singleton); tests use distinct channels. ## Deviations from design (Batch 4) 1. **Reconnect logic lives in a NEW file `lib/servicios/controlador_reconexion.dart`**, not inline in `servicio_audio.dart` (task text said "edit servicio_audio.dart"). Pure decision/backoff logic must be testable without platform channels (S7-R7); the handler cannot be constructed in tests (`AudioPlayer` hits MethodChannels at construction). The handler keeps only the integration glue. 2. **Buffer-config test asserts the config VALUES, not the construction call.** Asserting that `AudioPlayer(...)` received the config would require constructing the real player (platform channels). The config is a `static const` on the handler; `_crearPlayer` passes it (one-line wiring, verified by review + on-device item 9). Honest scope of S7-R1's `[flutter test]` portion. 3. **`TimeoutException` treated as network-class** in addition to the spec'd PlayerException 2xxx range. A real network drop usually surfaces as the existing 12s source-change timeout, NOT as a 2xxx PlayerException — without this, the most common stall would bypass reconnect entirely. The generic-Exception terminal path is otherwise unchanged. 4. **Stall detection is error-driven only** (per task T-S7-06 text); the design's optional "buffering > 8-10s watchdog" was NOT implemented. ExoPlayer/just_audio surfaces dead live streams as errors or our timeout; a buffering watchdog would add a timer racing the buffer config for marginal gain. Flagged for on-device validation (item 9): if a silent endless-buffering hang is observed, add the watchdog in a follow-up. 5. **`_cambiarFuente` completes normally when reconnect is engaged** (returns instead of rethrowing). Previously every failure rejected the `playMediaItem` future and `EstadoRadio.reproducir`'s catch pushed an error snackbar — that would show an error on the FIRST failure even while reconnecting, violating S7-R3. User-facing rejection still happens when reconnect does NOT engage (non-network error, no intent, exhausted). 6. **`restablecer()` on `playMediaItem`** (fresh user play/source switch restarts the backoff budget). Not explicit in the task text but required so the retry path (which goes through `_cambiarFuente` internally, not `playMediaItem`) can exhaust while user-initiated switches always get a full budget. 7. **Widget tests need a double `tester.pump()`** after a broadcast-stream emission (one pump delivers the event, the second rebuilds). Verified with a diagnostic harness that the implementation was correct and only the test needed the fix. ## 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) ## Verification summary (Batch 3) - `flutter test`: 89/89 passing (77 baseline + 12 new across 5 files) - `flutter analyze`: No issues found (identical to baseline) - `dart format`: applied to all 19 touched Dart files (5 reflowed) - `rg 'SharedPreferences.getInstance()' lib/`: only `main.dart` startup resolution + one injected-with-fallback expression per class (estado_alarmas, estado_idioma, estado_radio, servicio_alarmas, servicio_ecualizador, servicio_grabacion_radio, servicio_contenido_app) - `flutter build`: NOT run (forbidden) - No Kotlin/native files touched in this batch ### On-device verification items added by Batch 3 (user — Android device) 1. **Phone call pauses radio (S3-R1, checklist item 10):** while the radio plays, receive a call → radio pauses (or ducks); after the call ends it resumes automatically (transient loss). 2. **Headphones unplugged pauses radio (S3-R1):** unplug wired headphones / disconnect BT while playing → radio pauses and does NOT auto-resume. 3. **Another media app takes focus:** start playback in another app → PluriWave pauses; it must not resume on its own when focus is permanent loss. 4. **Locale switch sanity:** change app language in Ajustes → alarm titles/station names sent to new native schedules use the new language (l10n now configured per locale change, not per rebuild). 5. **Settings import still reflects alarms immediately** (cache bypass in `cargar()`): import a backup with alarms → the alarms list shows them without restarting the app. ## Verification summary (Batch 4) - `flutter test`: 99/99 passing (89 baseline + 10 new across 2 files); re-run after `dart format` - `flutter analyze`: No issues found (identical to baseline); re-run after format - `dart format`: applied to all 9 touched Dart files (2 reflowed); gen/ untouched by hand - `flutter gen-l10n`: run once after the 13 .arb edits - `flutter build`: NOT run (forbidden) - No Kotlin/native files touched in this batch (S7-R4: native alarm audio path untouched by construction) ### What the buffer actually buys (honest expectations, Design 7.1) - Configured: ExoPlayer keeps a 15-50s forward buffer; playback (re)starts after 2.5s buffered (5s after a rebuffer); time prioritized over byte thresholds. - Real drop ≲ buffered cushion (typically a few seconds up to ~15-30s depending on bitrate and how full the buffer was): audio keeps playing through the cushion, no UI change. - Drop longer than the cushion: playback stalls → "Reconectando..." spinner state (no error dialog/snackbar) → up to 5 backoff retries (1/2/4/8/16s delays + 12s attempt timeout each, total window ≈ up to ~90s) → on recovery the player rejoins the LIVE edge (live radio has no rewind — the missed audio is gone, not replayed) → on exhaustion, the single existing friendly error with manual retry. ### On-device verification items added by Batch 4 (user — Android device) 1. **Short drop plays through (S7-R1, checklist item 9):** while the radio plays (let it run ~1 min so the buffer fills), disable WiFi/LTE for ~10s → audio continues without interruption and no UI state change. 2. **Long drop reconnects (S7-R2/R3, checklist item 9):** disable connectivity ~45s → mini player and full player show "Reconectando..." with spinner (NO error dialog/snackbar); re-enable within ~90s → playback resumes at the live edge automatically. 3. **Exhaustion surfaces one error (S7-R2-C):** leave connectivity off >2 min → exactly ONE error state with the manual retry button appears after retries exhaust; no error spam during the retry window. 4. **User pause/stop during reconnect (S7-R6):** trigger a drop, then tap pause/stop while "Reconectando..." → playback stays stopped; it must NOT restart on its own when connectivity returns. 5. **Alarm fallback not delayed (S7-R4, checklist item 11):** alarm with a non-responding station URL → bundled WAV fires within the existing ~12-15s window, NOT extended by reconnect attempts. 6. **Recording during a drop (S7-R5):** record while forcing a drop → recording fails/stops cleanly per existing behavior; a subsequent recording start works. 7. **Sleep timer during a drop (S7-R6):** sleep timer expiring during "Reconectando..." stops audio for good. ## Verification summary (Batch 5) - `flutter test`: 103/103 passing (99 baseline + 4 new across 2 files); re-run after `dart format` - `flutter analyze`: No issues found (identical to baseline); re-run after format - `dart format`: applied to all 8 touched Dart files (4 reflowed) - `flutter build`: NOT run (forbidden) - No Kotlin/native, .arb or gen/ files touched in this batch ### Manual verification items added by Batch 5 (user) 1. **Backup round-trip on device (S4-R4):** export a backup from Ajustes, wipe/reinstall (or import on a second device), import the file → favorites, groups, custom stations, EQ presets, alarms (incl. vacations) and preferences all restored. Old (pre-S4a) backup files must import identically — the v2 envelope is byte-compatible. 2. **EQ controls still live-update (S4-R1):** toggle EQ from the player screen and from Ajustes; chip/switch/preset selector reflect changes immediately (these now rebuild from EstadoEcualizador, not EstadoRadio). 3. **Per-station preset on playback switch:** play a station with its own preset, switch to one without → main preset re-applies (path now goes through EstadoEcualizador). ## Verification summary (Batch 6) - `flutter test`: 110/110 passing (103 baseline − 1 pagination test moved to estado_busqueda_test + 8 new across 3 files); re-run after `dart format` - `flutter analyze`: No issues found (identical to baseline) — used as the safety net for missed call sites after removing the 15 compat members; re-run after format - `dart format`: applied to all 15 touched Dart files (10 reflowed) - `rg 'TODO\(S4b\)' lib/`: ZERO occurrences (only historical mentions in tasks.md/apply-progress.md remain) - EstadoRadio final size: **753 lines** (was ~1121 pre-split, ~1010 after S4a) - `flutter build`: NOT run (forbidden) - No Kotlin/native, .arb or gen/ files touched in this batch ### Manual verification items added by Batch 6 (user) 1. **Search screen (S4-R3):** search by name/country/language/quality, infinite scroll, genre chips on home — results and spinners behave as before (now driven by EstadoBusqueda). 2. **Nearby stations (S4-R3):** "Detect" on home requests location and fills the nearby strip; error text when undetectable. 3. **Recording (S4-R2):** start/stop from the player (indefinite, fixed and custom durations), live duration/bytes counter updates, open-folder/open-last-file buttons, recordings settings section (change/restore dir, max size) — all now via EstadoGrabacion. 4. **Scoped rebuilds (S4-R5):** while audio plays/buffers, home/favorites/settings should feel identical (no visual change expected — the win is fewer rebuilds); list reordering in Ajustes still re-sorts home, search results and favorites. 5. **Stop recording on pause/stop/station switch:** unchanged orchestration in EstadoRadio — verify recording stops when playback pauses/stops or station changes. ## Verification summary (Batch 7) - `flutter test`: 121/121 passing (110 baseline + 11 new across 6 files); re-run after `dart format` - `flutter analyze`: No issues found (identical to baseline); re-run after format - `dart format`: applied to all 20 touched Dart files (7 reflowed); gen/ untouched by hand - `flutter gen-l10n`: run once after the 13 .arb edits - Color-literal audit: `rg 'Color(0x' lib` excluding `lib/tema/` and `lib/l10n/gen/` → **0 matches** (was 14) - `rg '.animate()' lib` → 0 direct flutter_animate call sites (all via PluriAnimate) - `flutter build`: NOT run (forbidden) - No Kotlin/native files touched in this batch ### Manual verification items added by Batch 7 (user) 1. **Visual parity sweep (S5-R1):** home/buscar/favoritos/ajustes/alarmas/reproductor look unchanged — token mapping was value-preserving (onSurface ≈ white; electricMagenta = brand). The settings "check" icons are now mint (`colorScheme.secondary`) instead of Material green — intentional. 2. **TalkBack (S5-R2):** on a station card, the favorite toggle is now reachable as its OWN button ("Añadir a favoritos / Quitar de favoritos") separate from the card; alarm/vacation images announce their labels. 3. **Reduced motion (S5-R3):** with "remove animations" enabled, home grid/search results/player screens render instantly with no entry animations. 4. **Locale dates (S5-R4):** switch app language to English → alarm editor and vacation ranges show M/D/Y order; Japanese shows Y/M/D. 5. **Media notification (S5-R8):** the playback notification accent is teal (brand), not purple, on devices that honor `notificationColor`. ## Workload / boundary - Mode: auto-chain local slices (no PRs) - Current work units: S1, S2a, S2b, S3a, S3b, S7, S4a, S4b (committed, latest 52855e7), S5 (complete, in working tree) - Boundary (Batch 7): starts from the clean post-52855e7 tree; ends with S5 fully checked off, suite green (121/121). Rollback = revert the 27 modified lib files (incl. 13 ARB + gen/) + delete the 7 new files (Dart-only; no native edits). - Next batch: S6 (quality gates — lint hardening + remaining top-5 tests) then cross-cutting T-CC-01/T-CC-02. S6 is the LAST slice.