diff --git a/openspec/changes/rediseno-funcional/tasks.md b/openspec/changes/rediseno-funcional/tasks.md new file mode 100644 index 0000000..3b7a521 --- /dev/null +++ b/openspec/changes/rediseno-funcional/tasks.md @@ -0,0 +1,613 @@ +# Tasks: Functional Redesign (`rediseno-funcional`) + +> Reads (authoritative): `proposal.md`, `design.md`, `specs/*/spec.md` in this folder. +> Binding corrections from the design phase are applied throughout (see table below) — they override the proposal +> where they conflict with it. +> **Delivery model**: 18 work-unit **commits** on one branch, `feat/rediseno-funcional` — NOT chained PRs (Engram +> `sdd/rediseno-funcional/delivery-strategy`, id 2504, a user decision that supersedes the proposal's +> feature-branch-chain recommendation). Conventional commits only. Never `Co-Authored-By` or AI attribution. Never +> push to `main`. The user opens one PR in the Gitea web UI at the end; this agent never opens PRs. +> **This artifact intentionally exceeds the default ~530-word task-budget**, for the same reason `proposal.md` and +> `design.md` do: 18 work units each need a commit message, a RED/GREEN/REFACTOR breakdown, named test files, spec +> traceability, and a verification command. Collapsing that to fit a budget would hide exactly the detail a reviewer +> needs. Tables and checklists are used throughout to keep it scannable despite the length. +> **WU12 does not exist** — the native EQ band-count spike was resolved before planning closed (Engram id 2498, "keep +> 5 bands"). Numbering skips 11→13 on purpose; this is not a gap. +> Strict TDD is ON. Runner: `flutter test`. `flutter analyze` and `dart format .` gate every commit. +> **`flutter build` is never run.** + +## Review Workload Forecast (per commit, not per PR) + +| WU | Commit | Depends on | Est. changed lines | Budget risk | Exception | +|----|--------|-----------|---------------------|-------------|-----------| +| 1 | `feat(tokens): add design tokens, type scale, push scaffold, and root nav state` | — | 220-330 | Low | No | +| 2 | `test(auto): confirm Android Auto tree matches the redesign, no code changes` | — | 0 | None | No | +| 3a | `refactor(ajustes): split Settings AUDIO/EMISORAS into pushed detail screens` | 1 | **800-1000** | High | **Yes — move-only diff** | +| 3b | `refactor(ajustes): split remaining Settings sections into pushed screens` | 3a | **500-700**\* | Medium-High | **Yes — move-only diff** | +| 4 | `feat(favoritos): replace stacked group panels with chip-filtered reorderable list` | 1 | 300-400 | Medium | Monitor | +| 5 | `feat(escuchar): replace discovery browser with embedded player and favorites grid` | 1, 4 | 350-450 | Medium | Monitor | +| 6 | `feat(buscar): add discovery landing state, filter pills, counter, and sort` | 5 | 350-500 | Medium-High | Monitor | +| 7 | `feat(paises): add country browser and extract shared radio transport` | 6 | 300-400 | Medium (test-heavy) | Monitor | +| 8 | `feat(alarmas): simplify alarm cards and add vacation summary row` | 1 | 400-550 | Medium-High | Monitor† | +| 9 | `feat(vacaciones): add vacation range manager screen` | 8 | 350-450 | Medium | Monitor | +| 10 | `feat(alarmas): rewrite alarm editor with inline time widget` | 8 | 500-650 | High | **Yes — indivisible new widget**‡ | +| 11 | `feat(alarma-sonando): restyle ringing screen, drop live countdown label` | 1 | 200-300 | Low (safety-critical review attention: High) | No | +| 13 | `feat(eq): restyle equalizer screen and add custom presets` | 3a | 400-550 | Medium-High | Monitor | +| 14 | `feat(reproductor): restructure full player with tool-tray and EQ sheet` | 13 | 450-600 | Medium-High | Monitor | +| 15 | `feat(grabaciones): add recordings library screen` | 3b | 300-400 | Medium | Monitor | +| 16 | `feat(connectivity): restyle offline and reconnect banners` | 1 | 150-250 | Low | No | +| 17 | `feat(bienvenida): add monetization-free welcome screen` | 1 | 150-200 | Low | No | +| 18 | `feat(i18n): add redesign strings and translate Escuchar rename to 11 locales` | all | ~0 eng. / 400-600 data | Medium (data volume, low logic risk) | No | + +\* Not an explicit design-stated figure — derived pro-rata from WU3a's verified 800-1000/7-sections ratio, scaled to +WU3b's 5 sections (no new shared widget file needed there, since `fila_ajuste.dart` already exists from WU3a). +† Record `size:exception` at apply time only if the realized diff exceeds ~500 lines; justification: "largest single +alarm-card + hero + vacation-summary restyle, not divisible without breaking the one-commit-per-work-unit rule." +‡ The proposal already isolates WU10 as "its own PR, never bundled" — splitting further would leave an unintegrated +commit (a widget with no consumer, or a sheet rewrite with no new editor). + +```text +Decision needed before apply: No +Chained PRs recommended: No +Chain strategy: size-exception +400-line budget risk: High +``` + +**Why these values, not a fresh ask.** The delivery model was already decided by the user (Engram 2504), which +supersedes the proposal's own `feature-branch-chain` recommendation once the host was confirmed as Gitea (no `gh`, +agent cannot open PRs). That decision is functionally equivalent to an already-accepted `size:exception` at the +aggregate level — the one eventual PR will be reviewed commit-by-commit, which is why this forecast is expressed per +commit. Three commits (WU3a, WU3b, WU10) get an explicit `size:exception` because they cannot be reduced under 400 +lines without violating the one-commit-per-work-unit rule; the rest are within or near budget individually. + +**Parallelization note.** Delivery is sequential commits on one branch, so WUs land in the order below regardless of +true independence. For planning only: WU2 has no dependencies and may be pulled to any point (already noted in the +proposal). WU11, WU16, and WU17 each depend only on WU1 and are mutually independent of the WU3a-WU10 and WU13-15 +chains — they could be reordered among themselves without breaking anything, but the sequence below is kept to match +the proposal's narrative and dependency table. + +## Corrections From the Design Phase (applied throughout) + +| # | Correction | Where applied | +|---|------------|---------------| +| 1 | `EstadoNavegacionRaiz` moves from WU5 into WU1 | WU1 tasks 1.6-1.7; WU5 task 5.3 only *consumes* `irA(...)` | +| 2 | WU3a/WU3b line estimates were structurally low (verbatim move = add+delete in `git diff`) | Forecast table above; `size:exception` recorded for both, justification "move-only diff" | +| 3 | Settings detail-screen count is **12**, not "~8" | WU3a (7 screens) + WU3b (5 screens) = 12, listed by name in tasks 3a.4-3a.5, 3b.2-3b.3 | +| 4a | Custom EQ preset persistence must live in a **new** file, never in `servicio_ecualizador.dart` | WU13 task 13.6, hazard box restated inline | +| 4b | Mini player must hide **visually** (`visible: false`), never leave the tree, to preserve the S3-R3 `didChangeDependencies` side effect | WU5 task 5.2, 5.6 | +| 5 | `MiniReproductor.altura` must be derived from laid-out height at apply time, not guessed | WU5 task 5.6 | + +## Non-Negotiable Guards (master checklist — verify before the branch is considered done) + +- [ ] `test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart` passes **unmodified** (touched by WU9, WU11 — if + a restyle appears to require editing it, STOP and escalate; do not edit the test). +- [ ] `git diff` is **empty** for `lib/servicios/navegacion_auto.dart` (WU2), `lib/servicios/servicio_ecualizador.dart` + (WU13), and the band-application block at `lib/servicios/servicio_audio.dart:749-762` (WU13). +- [ ] The equalizer renders exactly **5** sliders (WU13, task 13.1). +- [ ] No shipped string contains "PRO", a trial duration, or a price (WU17 task 17.2/17.6; WU18 task 18.6 final + whole-app grep). +- [ ] All 13 ARB files keep identical key sets (WU18 tasks 18.1, 18.5 — final parity check; WU1 only touches values + in 2 of the 13 files, so it cannot break parity on its own). + +--- + +## WU1 — Tokens, push scaffold, Escuchar rename, root nav provider + +**Commit**: `feat(tokens): add design tokens, type scale, push scaffold, and root nav state` +**Depends on**: — (first commit) +**Spec refs**: `app-navigation-shell` — Five-Tab Root Navigation, Push-Chrome on Second-Level Screens, Root-to-Root +Switching Without Push (provider only; consumption is WU5), Escuchar Tab Rename Preserves the ARB Key +**Verify**: `flutter test test/tema/ test/widgets/pluri_push_scaffold_test.dart test/estado/estado_navegacion_test.dart test/widget_test.dart && flutter analyze && dart format --set-exit-if-changed .` +**New tests**: `test/tema/pluriwave_typography_test.dart`, `test/widgets/pluri_push_scaffold_test.dart`, `test/estado/estado_navegacion_test.dart` +**Modified tests**: `test/tema/pluriwave_tokens_test.dart` (or equivalent), `test/widget_test.dart` + +- [ ] 1.1 RED — write `pluriwave_typography_test.dart` asserting the 6 named styles (`heroTime`, `sectionTitle`, + `screenTitle`, `cardTitle`, `bodyStrong`, `eyebrowLabel`) exist with their documented size/weight/letter-spacing + and that `PluriWaveTheme.dark()` registers the extension. +- [ ] 1.2 RED — extend the tokens test asserting `PluriWaveTokens.listSurface`, `.liveGreen`, `.offlineAccent` exist + and participate in `lerp`. +- [ ] 1.3 GREEN — add the 3 colour fields to `lib/tema/pluriwave_tokens.dart` (constructor, `copyWith`, `lerp`); + de-literalise `pluriwave_theme.dart:12,15`; create `lib/tema/pluriwave_typography.dart` built inside + `PluriWaveTheme.dark()`. +- [ ] 1.4 RED — write `pluri_push_scaffold_test.dart`: each of the 5 root screens mounted bare builds zero + `Scaffold`; a `PluriPushScaffold` renders exactly one 56px `AppBar` + back affordance and exposes no + `bottomNavigationBar` parameter on its constructor. +- [ ] 1.5 GREEN — implement `lib/widgets/pluri_push_scaffold.dart` per the ADR-2 API (`title`, `body`, + `titleOverride`, `leadingIcon`, `onBack`, `actions`, `bottom`, `floatingActionButton`, static `push`). +- [ ] 1.6 RED — write `estado_navegacion_test.dart`: `irA()` transitions `RaizPluriWave`, no-ops and does not notify + on same-root, `indice` matches enum declaration order. +- [ ] 1.7 GREEN — implement `lib/estado/estado_navegacion.dart` (`RaizPluriWave` enum, `EstadoNavegacionRaiz`); + register it in `app.dart`'s `MultiProvider`; convert the 3 `setState(() => _indice = ...)` alarm-deep-link + sites (`app.dart:310,333,350`) to `irA(...)`. +- [ ] 1.8 RED — update `test/widget_test.dart` to expect "Escuchar" (not "Inicio") as the first tab's label, with + `PluriIconGlyph.home` unchanged as its icon. +- [ ] 1.9 GREEN — change `app_en.arb` line 4 `navHome` value to `"Listen"` and `app_es.arb` line 4 to `"Escuchar"`; + retitle the first tab in `lib/app.dart`. Leave the other 11 ARB files untouched (WU18's job). +- [ ] 1.10 REFACTOR — sweep `lib/tema/`, `app.dart` for now-dead literals; confirm zero consumer screens were + touched (WU1 converts zero screens to `PluriPushScaffold` — that is what keeps this commit at its line budget). +- [ ] 1.11 Verify — run the command above; confirm `git diff --stat` touches only tokens/theme/typography files, the + new push-scaffold and nav-state files, `app.dart`, `app_en.arb`, `app_es.arb`, and their tests. + +## WU2 — Android Auto verification (zero code) + +**Commit**: `test(auto): confirm Android Auto tree matches the redesign, no code changes` +**Depends on**: — +**Spec refs**: `android-auto-media` — no requirement delta; this WU records the confirmation +**Verify**: `flutter test test/servicios/navegacion_auto_test.dart && git diff --stat lib/servicios/navegacion_auto.dart` +**Tests**: none created or modified — existing suite re-run only + +- [ ] 2.1 Run `flutter test test/servicios/navegacion_auto_test.dart`; confirm every case passes unmodified. +- [ ] 2.2 Confirm `git diff lib/servicios/navegacion_auto.dart` is empty — no code change in this commit. +- [ ] 2.3 Record in the commit body: A1-A5 closed, zero code changes, `MediaBrowserService` model confirmed + non-custom-renderable (the waveform in A4 is not implementable on this platform). +- [ ] 2.4 No REFACTOR step — this WU is verification-only by design. + +## WU3a — Settings shell + AUDIO/EMISORAS groups + +**Commit**: `refactor(ajustes): split Settings AUDIO/EMISORAS into pushed detail screens` +**Depends on**: WU1 +**Spec refs**: `app-navigation-shell` — Push-Chrome on Second-Level Screens (Settings detail screen scenario) +**Verify**: `flutter test test/pantallas/pantalla_ajustes_test.dart test/pantallas/ajustes/ && flutter analyze && dart format .` +**New tests**: one per new screen under `test/pantallas/ajustes/` (7 files, AUDIO + EMISORAS) +**Modified tests**: `test/pantallas/pantalla_ajustes_test.dart` (near-total rewrite) + +- [ ] 3a.1 RED — rewrite `pantalla_ajustes_test.dart`: root renders exactly 4 grouped nav lists, zero inline + controls, file stays under 400 lines. +- [ ] 3a.2 RED — write one test per AUDIO/EMISORAS detail screen (Ecualizador, Salida de audio, Temporizador de + sueño, Grupos de favoritos, Emisora preferida, Emisoras personalizadas, Orden de listas) asserting it renders + inside a `PluriPushScaffold` and its moved controls still respond. +- [ ] 3a.3 GREEN — create `lib/pantallas/ajustes/widgets/fila_ajuste.dart` (`FilaAjuste` / `GrupoAjustes` primitives). +- [ ] 3a.4 GREEN — cut `_SeccionEcualizador` (694-782), `_SeccionEcualizadorAvanzado` + `_FilaDispositivo` + + `_DialogoEdicionDispositivo` (782-1013), `_SeccionTimerSueno` + `_FormularioDuracionTimer` (399-604) into + `pantalla_ajustes_ecualizador.dart`, `pantalla_ajustes_salida_audio.dart`, `pantalla_ajustes_timer_sueno.dart`. + Verbatim-move rule: delete only the panel header row; body is untouched. +- [ ] 3a.5 GREEN — cut `_SeccionGruposFavoritos` (1187), `_SeccionEmisoraPreferida` (1342), `_SeccionEmisoras` + + `_FormularioEmisora` (1459-1669), `_SeccionOrdenListas` (1133) into `pantalla_ajustes_grupos_favoritos.dart`, + `pantalla_ajustes_emisora_preferida.dart`, `pantalla_ajustes_emisoras_personalizadas.dart`, + `pantalla_ajustes_orden_listas.dart`. +- [ ] 3a.6 GREEN — in the root, replace the 7 moved sections with `FilaAjuste` rows under two `GrupoAjustes` cards + (AUDIO, EMISORAS); each row pushes its screen via `PluriPushScaffold.push`. +- [ ] 3a.7 REFACTOR — confirm zero business-logic edits inside any `_CuerpoX` (diff reads "header removed, body + identical"); confirm `app.dart`'s import of `pantalla_ajustes.dart` is unchanged. +- [ ] 3a.8 Verify — root file line count < 400; `git diff` touches only screen files (no service/state file). + +**`size:exception` — "move-only diff".** ~800-1000 changed lines (design-verified figure), ~85% relocated not +modified. Do not attempt to slice under 450. + +## WU3b — Settings GRABACIONES Y MÚSICA / APLICACIÓN groups + +**Commit**: `refactor(ajustes): split remaining Settings sections into pushed screens` +**Depends on**: WU3a +**Spec refs**: `app-navigation-shell` — Push-Chrome on Second-Level Screens +**Verify**: `flutter test test/pantallas/pantalla_ajustes_test.dart test/pantallas/ajustes/ && flutter analyze && dart format .` +**New tests**: one per new screen under `test/pantallas/ajustes/` (5 files: Grabaciones, Música local, Idioma, +Backup, Info) +**Modified tests**: `test/pantallas/pantalla_ajustes_test.dart` (extended) + +- [ ] 3b.1 RED — extend detail-screen tests for the remaining 5 sections, asserting `PluriPushScaffold` usage and + preserved controls. +- [ ] 3b.2 GREEN — cut `_SeccionGrabaciones` (92-291), `_SeccionMusicaLocal` (291-399) into + `pantalla_ajustes_grabaciones.dart`, `pantalla_ajustes_musica_local.dart`. +- [ ] 3b.3 GREEN — cut `_SeccionIdioma` + `_IdiomaDisponible` (492-694), `_SeccionBackup` (1669-1791), + `_SeccionInfo` (1791-1896) into `pantalla_ajustes_idioma.dart`, `pantalla_ajustes_backup.dart`, + `pantalla_ajustes_info.dart`. +- [ ] 3b.4 GREEN — in the root, replace the remaining sections with `FilaAjuste` rows under two more `GrupoAjustes` + cards (GRABACIONES Y MÚSICA, APLICACIÓN); confirm sleep timer and backup rows are present — corollary ruling, + nothing dropped. +- [ ] 3b.5 REFACTOR — confirm `pantalla_ajustes.dart` now contains only 4 `GrupoAjustes` cards and is under 400 + lines; delete now-unused private section widgets. +- [ ] 3b.6 Verify — full `pantalla_ajustes_test.dart` suite green; all 12 detail screens present and reachable; no + service/state file touched. + +**`size:exception` — "move-only diff".** ~500-700 changed lines (derived; see forecast table footnote). Same +justification as WU3a. + +## WU4 — Favoritos restyle + +**Commit**: `feat(favoritos): replace stacked group panels with chip-filtered reorderable list` +**Depends on**: WU1 +**Spec refs**: `favorites-organization` — Chip-Filtered Flat List, Drag-to-Reorder Within the Active Filter, Sort +Action Using Existing Criteria, Group Management Reachable from Favoritos, Custom-Station CTA Preserved +**Verify**: `flutter test test/pantallas/pantalla_favoritos_plural_test.dart && flutter analyze && dart format .` +**Modified tests**: `test/pantallas/pantalla_favoritos_plural_test.dart` (+ new reorder scenario) + +- [ ] 4.1 RED — update the test file: chip filter narrows the list ("Todas · N" + one chip per group); drag-to-reorder + persists across a simulated restart; `swap_vert` applies `OrdenEmisoras`; create-group action reachable; + custom-station CTA reachable. +- [ ] 4.2 GREEN — replace `_GrupoFavoritosPanel` / `_FavoritoItem` with a horizontally-scrollable chip row + a single + `ReorderableListView` filtered by the active chip. +- [ ] 4.3 GREEN — wire persistence for the new order and the `swap_vert` action to `OrdenEmisoras`. +- [ ] 4.4 GREEN — surface the create-group action; keep the dashed custom-station CTA. +- [ ] 4.5 REFACTOR — extract the chip row / reorderable row into private widgets if the file grows unwieldy; confirm + push-chrome is **not** applied here (Favoritos keeps its tab bar — the one exemption). +- [ ] 4.6 Verify — reorder-persists scenario green after simulated restart; sort scenario green. + +## WU5 — Escuchar restructure + +**Commit**: `feat(escuchar): replace discovery browser with embedded player and favorites grid` +**Depends on**: WU1, WU4 +**Spec refs**: `app-navigation-shell` — Root-to-Root Switching Without Push +**Verify**: `flutter test test/pantallas/pantalla_inicio_test.dart test/pantallas/pantalla_inicio_rebuild_test.dart test/widgets/mini_reproductor_configurar_test.dart && flutter analyze && dart format .` +**Modified tests**: `pantalla_inicio_test.dart`, `pantalla_inicio_rebuild_test.dart`, `mini_reproductor_configurar_test.dart` + +- [ ] 5.1 RED — write the ADR-7 anti-cache test: mutate `EstadoRadio` **from outside the widget tree** (simulating + Android Auto / notification-driven playback change) and assert the Escuchar hero follows, with zero cached + fields in `State`. +- [ ] 5.2 RED — **hazard test**: assert `MiniReproductor` is `visible: false` (renders `SizedBox.shrink()`) while on + Escuchar, AND that `configurarLocalizaciones` still ran in `didChangeDependencies` + (`mini_reproductor.dart:27-38`, the S3-R3 contract). Removing the widget from the tree would silently break + this — the hazard is hiding it structurally, not visually. +- [ ] 5.3 RED — assert "Ver todas" calls `context.read().irA(RaizPluriWave.favoritos)` and that + `Navigator` depth is unchanged (switches tab, does not push). This only *consumes* WU1's provider. +- [ ] 5.4 RED — update `pantalla_inicio_test.dart` / `pantalla_inicio_rebuild_test.dart` for the new content model + (hero + "Tus emisoras" grid replaces the discovery grid); confirm `context.select` scoping keeps rebuild count + low (existing `MemoLista` pattern). +- [ ] 5.5 GREEN — build the Escuchar hero as a `StatelessWidget` reusing `_WaveHero` / `VisualizadorAudio` patterns + from `pantalla_reproductor.dart` (square art, `barras: 30`, `altura: 26`, `color: liveGreen`), transport row + with sleep as the 5th action, tool-tray entry chip; reads via `context.select` per scalar. +- [ ] 5.6 GREEN — add `MiniReproductor.altura` as a value **measured from its actual laid-out height at apply time** + (e.g. via a `GlobalKey`/`RenderBox` read at build), never a guessed constant; add its `visible` parameter. + `_PaginaPrincipal` sets `visible: false` on Escuchar only, keeping `State` mounted. +- [ ] 5.7 GREEN — add the derived `PluriLayout` constant (`bottomChromeInset - MiniReproductor.altura`, + `pluri_layout.dart:10` currently hardcodes `146` assuming the mini player is present) for Escuchar's content + padding. +- [ ] 5.8 GREEN — swap the "Tus emisoras" grid data source to `listaFavoritos` (capped) with "Ver todas" wired to + `EstadoNavegacionRaiz.irA(RaizPluriWave.favoritos)`. +- [ ] 5.9 REFACTOR — leave the discovery-section widgets (`_seccionCercanas`/`_chipGeneros`/etc.) in place for now; + WU6 relocates and then deletes them — do not delete here to avoid an intermediate commit with the content + nowhere. Note this handoff explicitly in the commit body. +- [ ] 5.10 Verify — anti-cache test green; mini-player-hidden-but-side-effect-ran test green; tab-switch-not-push + test green; `visualizador_audio.dart` and `estado_radio.dart` show empty `git diff`. + +## WU6 — Buscar landing state + filters + +**Commit**: `feat(buscar): add discovery landing state, filter pills, counter, and sort` +**Depends on**: WU5 +**Spec refs**: `station-discovery-browse` — Buscar Landing State Shows Discovery Content, Active-Filter Pills and +Results Counter, One-Tap Clear-All-Filters on Empty Results, Client-Side Search Sort Only +**Verify**: `flutter test test/pantallas/pantalla_buscar_shimmer_test.dart test/estado/estado_busqueda_test.dart && flutter analyze && dart format .` +**Modified tests**: `pantalla_buscar_shimmer_test.dart`, `estado_busqueda_test.dart` (non-sort paths unmodified) + +- [ ] 6.1 No spike needed here — the sort ruling is already verified (Engram 2505): server-side `order`/`reverse` + exist but are rejected; `OrdenEmisoras` is the only extension point. Do not re-derive. +- [ ] 6.2 RED — empty query renders discovery content (near-you / genres / trending / Países entry); non-empty query + replaces it with the results view. +- [ ] 6.3 RED — active-filter pills (country/language/quality) with a close affordance; results counter text; the + one-tap "Quitar los 2 filtros" action on a 2-filter/0-result fixture. +- [ ] 6.4 RED — regression guard: every rendered "Ordenar" option maps to a real `OrdenEmisoras` case with its own + test. +- [ ] 6.5 GREEN — move the discovery content (near-you/genres/trending, Países entry point) from + `pantalla_inicio.dart`'s old sections into `pantalla_buscar.dart`'s empty-query landing state; **delete** the + moved sections from `pantalla_inicio.dart` now (completes WU5's deferred cleanup, task 5.9). +- [ ] 6.6 GREEN — collapse the 3 always-visible `FilterChip` rows into an active-pills bar with bottom-sheet + pickers; add the results counter and clear-all-filters action. +- [ ] 6.7 GREEN — extend `enum OrdenEmisoras` (`orden_emisoras.dart:4`) only with criteria backed by existing + `Emisora` fields (`bitrate`, `votes`, `clickcount`); wire "Ordenar" client-side only — no `order`/`reverse` + param added to any request. +- [ ] 6.8 REFACTOR — confirm `estado_busqueda_test.dart`'s non-sort paths still pass unmodified. +- [ ] 6.9 Verify — no test in this WU asserts an `order` param on any outgoing request; sort-option regression guard + green. + +## WU7 — Países browser + `_get` transport extraction + +**Commit**: `feat(paises): add country browser and extract shared radio transport` +**Depends on**: WU6 +**Spec refs**: `station-discovery-browse` — Países Browser Over the Verified Countries Contract, Existing Station +Calls Unchanged by Transport Extraction +**Verify**: `flutter test test/servicios/servicio_radio_test.dart test/servicios/servicio_radio_transporte_test.dart test/estado/estado_busqueda_test.dart && flutter analyze && dart format .` +**New tests**: `test/servicios/servicio_radio_transporte_test.dart`, country-model test, `pantalla_paises_test.dart` +**Modified tests**: `estado_busqueda_test.dart` (additions only); `servicio_radio_test.dart` is **not modified** + +> **Special ordering rule (non-negotiable): task 7.1 must be green before task 7.4 begins.** Its shuffled-fixture +> UUID-sequence assertion is what makes the extraction safe — skipping it lets a sort silently sink into transport. + +- [ ] 7.1 RED→GREEN (characterisation, green by construction against the **unmodified** `_get`) — create + `servicio_radio_transporte_test.dart` pinning, for each of the 7 `_get`-based methods (`obtenerPopulares`, + `obtenerTendencias`, `buscarPorNombre`, `buscarPorPais`, `buscarPorIdioma`, `buscarPorTag`, `buscar`): request + path, `lastcheckok=1` present, `hidebroken=true` present, non-empty `User-Agent`, exact + `order`/`reverse`/`limit`/`offset`, and the exact returned UUID sequence from a fixture with deliberately + shuffled `bitrate`/`clickcount`/`votes`. Pin `registrarClick`'s path and that it sends *some* `User-Agent` + (do **not** pin its known-stale literal at line 325 — that would convert a bug into a contract). +- [ ] 7.2 RED — `PaisRadio.fromApi` parses `stationcount` via `int.tryParse` from a fixture where it arrives as a + JSON **string**; defaults safely on missing fields. +- [ ] 7.3 RED — `estado_busqueda_test.dart` additions for `paises`, `cargandoPaises`, `cargarPaises()` (in-memory + cache guard); `pantalla_paises_test.dart` for "Tus idiomas" + full alphabetical list with counts. +- [ ] 7.4 GREEN — extract `_getJson` (transport only: `_descubrirServidores`, host rotation, bounded retries, + User-Agent, timeout, status check, `json.decode`, sticky-host bookkeeping) out of `_get`, moved as **one + block, no logic edits**; `_get` re-implemented on top, still applying `lastcheckok`, `Emisora.fromApi`, + `_compararCalidad`. +- [ ] 7.5 GREEN — implement `lib/modelos/pais_radio.dart`, `obtenerPaises()` (calls + `_getJson('/json/countries', const {})`, no `lastcheckok`, no `order`), and `lib/pantallas/pantalla_paises.dart`. +- [ ] 7.6 RE-RUN (proof step) — re-run task 7.1's tests against the extracted code; must be byte-identical green, + including the shuffled-fixture UUID-sequence assertion. +- [ ] 7.7 REFACTOR — document in the commit body that a successful `/json/countries` call now warms + `_servidorActual` for subsequent station calls — an accepted, intentional behaviour delta, not a regression. +- [ ] 7.8 Verify — grep confirms `countrycodes` appears nowhere in `lib/`; the string-`stationcount` fixture renders + `482` without a cast error; the countries request has no `lastcheckok`; all 8 original methods' tests pass + identically before and after. + +## WU8 — Alarmas root restyle + +**Commit**: `feat(alarmas): simplify alarm cards and add vacation summary row` +**Depends on**: WU1 +**Spec refs**: `native-alarms` (delta) — Alarm Card Tap-to-Edit Swipe-to-Delete, Hero Banner Inline Skip; +`alarm-vacation-ranges` — Vacation Summary Row Replaces the Inline Panel +**Verify**: `flutter test test/pantallas/pantalla_alarmas_editor_test.dart test/estado/estado_alarmas_test.dart test/estado/estado_alarmas_snooze_test.dart && flutter analyze && dart format .` +**Modified tests**: `pantalla_alarmas_editor_test.dart` (+ new card-gesture scenarios); `estado_alarmas_test.dart`, +`estado_alarmas_snooze_test.dart` must pass unmodified + +- [ ] 8.1 RED — tapping a simplified alarm card (giant time + station + switch) opens the editor pre-filled; + swiping triggers delete with confirmation; no capability from the old always-visible button row is lost. +- [ ] 8.2 RED — the hero banner's inline "Saltar" action skips the featured (soonest-firing) alarm, consistent with + existing skip-next behaviour. +- [ ] 8.3 RED — the vacation panel is now a summary row (range count + next-range countdown) that pushes a screen + on tap (destination screen not yet built — assert `PluriPushScaffold.push` is invoked; WU9 builds it). +- [ ] 8.4 GREEN — simplify `_TarjetaAlarma` to giant time/name/switch; move Edit/Skip/Delete behind tap/swipe. +- [ ] 8.5 GREEN — add the hero banner's inline "Saltar" pill calling the existing skip-next path. +- [ ] 8.6 GREEN — replace `_PanelVacaciones`'s inline list with a summary row wired to push the Vacaciones manager + screen. +- [ ] 8.7 REFACTOR — confirm no scheduling/dismiss-guard code path was touched; `estado_alarmas_test.dart` and + `estado_alarmas_snooze_test.dart` pass unmodified. +- [ ] 8.8 Verify — tap/swipe/hero-skip scenarios all green; confirm + `pantalla_alarma_sonando_dismiss_guard_test.dart` was not touched by this WU (it only edits the root list + screen). + +## WU9 — Vacaciones manager (new screen) + +**Commit**: `feat(vacaciones): add vacation range manager screen` +**Depends on**: WU8 +**Spec refs**: `alarm-vacation-ranges` — Active Range Detection, Per-Alarm Pause-Impact Computation, Upcoming Ranges +Query, Past-Ranges History Query, Vacation Summary Row Replaces the Inline Panel (destination screen) +**Verify**: `flutter test test/estado/estado_alarmas_test.dart test/pantallas/pantalla_vacaciones_test.dart && flutter analyze && dart format .` +**New tests**: `pantalla_vacaciones_test.dart` +**Modified tests**: `estado_alarmas_test.dart` (additions only) + +- [ ] 9.1 RED — with a **fixed `ahora`**: `rangoVacacionesActivo` returns the active range + days-remaining, or null + when none matches; `vacacionesProximas` returns future ranges ascending by start; `vacacionesPasadas` returns + ended ranges descending by end, disjoint from active/upcoming. +- [ ] 9.2 RED — `impactoDeRango`: given 3 alarms (2 with `sonarEnVacaciones == false`, 1 with `true`) during an + active range, assert exactly those 2 in `pausadas` and that 1 in `noAfectadas`, mirroring + `servicio_programacion_alarmas.dart:150`'s predicate exactly. +- [ ] 9.3 RED — `pantalla_vacaciones_test.dart`: active-range hero with progress + per-alarm impact line; + "PROGRAMADOS" upcoming list; dashed "Añadir rango" CTA; "Rangos pasados" history row/screen. +- [ ] 9.4 GREEN — add the 4 pure query members to `EstadoAlarmas` (`{DateTime? ahora}` defaulting to + `DateTime.now()`), delegating `rangoVacacionesActivo` to the existing `RangoVacaciones.contiene(fecha)`. +- [ ] 9.5 GREEN — add `ImpactoVacaciones` beside `RangoVacaciones` in `lib/modelos/alarma_musical.dart`. +- [ ] 9.6 GREEN — build `lib/pantallas/pantalla_vacaciones.dart` as a `PluriPushScaffold`; wire WU8's summary-row + tap to it. +- [ ] 9.7 REFACTOR — confirm none of the 4 new methods call `guardarVacaciones` or any reprogramming path + (read-only over `_alarmas`/`_vacaciones`); confirm no ticker/periodic timer was added. +- [ ] 9.8 Verify — `pantalla_alarma_sonando_dismiss_guard_test.dart` passes **unmodified** (hard rule — if a query + addition appears to require touching it, STOP and escalate; do not edit the test). + +## WU10 — Alarm editor sheet rewrite + +**Commit**: `feat(alarmas): rewrite alarm editor with inline time widget` +**Depends on**: WU8 +**Spec refs**: `native-alarms` (delta) — Alarm Editor Preserves Date, Fallback Station, and Sound Fields +**Verify**: `flutter test test/widgets/editor_hora_inline_test.dart test/pantallas/pantalla_alarmas_fecha_test.dart && flutter analyze && dart format .` +**New tests**: `test/widgets/editor_hora_inline_test.dart` (standalone) +**Modified tests**: `pantalla_alarmas_fecha_test.dart` + +- [ ] 10.1 RED — `editor_hora_inline_test.dart`, **standalone, independent of the sheet**: drag/tap adjusts hour and + minute, wraps at 23:59→00:00, exposes correct a11y labels. +- [ ] 10.2 GREEN — implement `lib/widgets/editor_hora_inline.dart` (giant inline HH:MM display, drag/tap-to-adjust). +- [ ] 10.3 RED — update `pantalla_alarmas_fecha_test.dart`: one-time date alarm still creatable via the (now + collapsed) Advanced section; fallback-station picker and sound dropdown still settable and persisted. +- [ ] 10.4 GREEN — replace the native `showTimePicker` dialog with `EditorHoraInline`; keep weekday circles always + visible; move the date field, fallback-station picker, and sound dropdown into a collapsed "Advanced" + section rather than dropping them. +- [ ] 10.5 GREEN — restyle the volume/fade-in sliders (cosmetic only, no behaviour change). +- [ ] 10.6 REFACTOR — confirm the dismiss-guard test is untouched by this WU (editor sheet only, not the ringing + screen). +- [ ] 10.7 Verify — standalone widget test green independent of the sheet; date/fallback/sound round-trip test + green. + +**`size:exception` recommended.** ~500-650 lines — the largest genuinely-new widget in the plan; already isolated +as its own unit per the proposal ("never bundled"). See forecast table footnote ‡. + +## WU11 — Alarm ringing restyle + +**Commit**: `feat(alarma-sonando): restyle ringing screen, drop live countdown label` +**Depends on**: WU1 +**Spec refs**: `native-alarms` (delta) — Ringing Screen Shows a Static Status Label, Dismiss Guard and Force-Stop +Banner Survive the Restyle +**Verify**: `flutter test test/pantallas/pantalla_alarma_sonando_test.dart test/pantallas/pantalla_alarma_sonando_scaffold_test.dart test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart && flutter analyze && dart format .` +**Modified tests**: `pantalla_alarma_sonando_test.dart`, `pantalla_alarma_sonando_scaffold_test.dart`. +**`pantalla_alarma_sonando_dismiss_guard_test.dart` is run but MUST NOT be edited.** + +> **Hard rule, restated:** if the restyle appears to require changing the dismiss-guard test, the task is to STOP +> and escalate — not to edit the test. + +- [ ] 11.1 RED — update the two non-guard test files for the full-bleed blurred-art layout, giant 88px time + (`heroTime`, wrapped in `FittedBox(fit: BoxFit.scaleDown)` per the text-scaling rule), 3 fixed snooze tiles + (3/5/10 min, 10 highlighted), full-width stop pill. +- [ ] 11.2 RED — the status label renders "Subiendo volumen" (or localized equivalent) with no accompanying + numeric value that changes over time. +- [ ] 11.3 GREEN — restyle to a full-bleed art background; replace the `Wrap` of snooze chips with 3 fixed tiles; + replace the glass-card container. +- [ ] 11.4 GREEN — keep the status label static, sourced only from `widget.alarma` (unchanged from the existing + documented constraint); preserve the force-stop retry banner in the new layout. +- [ ] 11.5 REFACTOR — run `pantalla_alarma_sonando_dismiss_guard_test.dart` **unchanged**; if it fails, STOP — do + not edit it, escalate instead. +- [ ] 11.6 Verify — force-stop banner still appears on a simulated stop failure; dismiss-guard test file diff is + empty. + +## WU13 — Ecualizador settings screen (5-band restyle) + +**Commit**: `feat(eq): restyle equalizer screen and add custom presets` +**Depends on**: WU3a +**Spec refs**: `eq-custom-presets` — Five-Band Equalizer (Regression Guard), Custom Preset Save, Custom Preset +Naming Validates Non-Empty Input, Per-Station EQ Entry Point (component only; consumption is WU14), +Stations-With-Own-EQ Drill-Down, Base-vs-Per-Station Explainer Preserved, Active Output Surfaced on the Main Screen +**Verify**: `flutter test test/widgets/ecualizador_widget_test.dart test/servicios/servicio_presets_personalizados_test.dart test/servicios/servicio_ecualizador_test.dart test/estado/estado_ecualizador_test.dart test/servicios/servicio_audio_eq_reapply_test.dart && flutter analyze && dart format .` +**New tests**: `servicio_presets_personalizados_test.dart`, `pantalla_ajustes_ecualizador_test.dart` +**Modified tests**: `ecualizador_widget_test.dart`. `servicio_ecualizador_test.dart`, `estado_ecualizador_test.dart`, +`servicio_audio_eq_reapply_test.dart` must pass **unmodified**. + +> **Hazard, restated: persistence for custom presets must NOT go in `servicio_ecualizador.dart`.** That file has an +> empty-`git diff` success criterion. + +- [ ] 13.1 RED — the Ecualizador screen renders exactly **5** vertical sliders (regression guard — reject any + future 7-slider change on sight). +- [ ] 13.2 RED — `servicio_presets_personalizados_test.dart`: save/list/delete round-trip for a named custom preset + via its **own** SharedPreferences key `eq_custom_presets_v1`. +- [ ] 13.3 RED — submitting an empty/whitespace-only preset name shows a validation message and persists nothing. +- [ ] 13.4 RED — the "Emisoras con ajuste propio" drill-down lists exactly the stations present in + `presetsPorEmisora`. +- [ ] 13.5 RED — the "Salida activa" row is visible on the main screen (not only Advanced) and updates on a + simulated device change; the base-vs-per-station explainer banner is visible. +- [ ] 13.6 GREEN — create `lib/servicios/servicio_presets_personalizados.dart` (**new file**, own SharedPreferences + key). **Do NOT add any member to `servicio_ecualizador.dart`.** `lib/modelos/preset_ecualizador.dart` stays + unchanged — a user preset is a `PresetEcualizador` with a user-supplied `nombre`; `toJson`/`desdeJson` + already exist. +- [ ] 13.7 GREEN — extend `EstadoEcualizador` with `presetsPersonalizados`, `guardarPresetPersonalizado(nombre)`, + `eliminarPresetPersonalizado(nombre)`, reading/writing only through the new service. +- [ ] 13.8 GREEN — restyle `lib/widgets/ecualizador_widget.dart` in place: strip the internal title + preset chip + row (lines 59-77 — the pushed header now carries the title), restyle the 5 `Card`/`RotatedBox`/`Slider` + tiles to the vertical-track look, add the `habilitado` parameter. Keep the `for (int i = 0; i < 5; i++)` + loop bound literally `5`. +- [ ] 13.9 GREEN — build `lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart` wiring + `EstadoEcualizador.presetPrincipal` / `cambiarPresetPrincipal`, surfacing "Salida activa" and the drill-down + row. +- [ ] 13.10 REFACTOR — reviewer check: confirm `git diff` is **empty** for `lib/servicios/servicio_ecualizador.dart`, + `lib/modelos/preset_ecualizador.dart`, and the band-application block at + `lib/servicios/servicio_audio.dart:749-762`. +- [ ] 13.11 Verify — the three EQ test files pass **unmodified**; exactly 5 sliders render. + +## WU14 — Reproductor completo + per-station EQ sheet + +**Commit**: `feat(reproductor): restructure full player with tool-tray and EQ sheet` +**Depends on**: WU13 +**Spec refs**: `eq-custom-presets` — Per-Station EQ Entry Point From the Player; `multi-device-eq` (delta) — +Per-Station EQ Entry Relocates, Resolution Logic Does Not +**Verify**: `flutter test test/pantallas/pantalla_reproductor_test.dart test/servicios/servicio_ecualizador_test.dart test/estado/estado_ecualizador_test.dart test/servicios/servicio_audio_eq_reapply_test.dart && flutter analyze && dart format .` +**Modified tests**: `pantalla_reproductor_test.dart` (or equivalent widget test). Three EQ test files must pass +**unmodified**. + +- [ ] 14.1 RED — the per-station EQ bottom sheet renders **the same `EcualizadorWidget` type** WU13 restyled + (assert by `runtimeType`, so a duplicate implementation fails the test, not just a visual review). +- [ ] 14.2 RED — the 4 tool-tray tiles ("EQ propio", "Grabar", sleep-timer value, "Compartir") each open their own + bottom sheet. +- [ ] 14.3 RED — opening "EQ propio" for a playing station shows 5 sliders bound to that station's resolved preset, + and a change round-trips through the existing per-station persistence path (`presetsPorEmisora` / + `presetsMatriz`). +- [ ] 14.4 GREEN — restructure `pantalla_reproductor.dart`: square art, favorite moved into the transport row, + single subtitle line (collapse the current `_InfoChips` `Wrap`), 4-tile tool-tray grid replacing the + always-expanded recording panel + separate sleep-timer button + EQ toggle. +- [ ] 14.5 GREEN — wire "EQ propio" to a bottom sheet hosting `EcualizadorWidget` bound via + `EstadoEcualizador.presetParaEmisora(uuid)` / `guardarPresetPorEmisora(uuid, ...)`. +- [ ] 14.6 GREEN — add the quality row + "Cambiar" action and the "Compartir" tool-tray tile. +- [ ] 14.7 REFACTOR — confirm no second EQ editor file was created; confirm the `multi-device-eq` regression + scenarios (device-event resolve-and-apply, first-seen bootstrap, cold start, connect/disconnect/reconnect, + toggle-off) still pass unmodified. +- [ ] 14.8 Verify — the 3 EQ test files remain green and unmodified; `EcualizadorWidget` type-identity assertion + passes. + +## WU15 — Grabaciones library (new list) + +**Commit**: `feat(grabaciones): add recordings library screen` +**Depends on**: WU3b +**Spec refs**: `recordings-library` — Storage Usage Display, Browsable Recordings List, Per-Recording Menu +Constrained to Rename/Share/Delete +**Verify**: `flutter test test/pantallas/pantalla_grabaciones_test.dart test/servicios/servicio_grabacion_radio_test.dart && flutter analyze && dart format .` +**New tests**: `pantalla_grabaciones_test.dart` +**Modified tests**: `servicio_grabacion_radio_test.dart` (only if a listing method is added) + +- [ ] 15.1 RED — storage progress-bar fill reflects used/total (e.g. 84/200 MB fixture) with a caption stating both + values. +- [ ] 15.2 RED — 3 recording-file fixtures render as 3 rows (name/date/duration/size); an empty folder renders an + empty state, not an error; tapping play starts/stops playback. +- [ ] 15.3 RED — the "⋮" menu exposes exactly Rename/Share/Delete — constrained to what + `servicio_grabacion_radio.dart` already exposes, no extra action. +- [ ] 15.4 RED — Delete (file + row removed), Rename (persists across reload), Share (invokes platform share sheet) + scenarios. +- [ ] 15.5 GREEN — add any missing listing method to `servicio_grabacion_radio.dart`, strictly limited to what it + already conceptually supports. +- [ ] 15.6 GREEN — build `lib/pantallas/pantalla_grabaciones.dart` (storage bar, per-recording rows with inline + playback, "⋮" menu). +- [ ] 15.7 REFACTOR — confirm the menu cannot expose a 4th action; confirm the screen degrades to an empty state, + not a crash, on an empty folder. +- [ ] 15.8 Verify — all 4 menu-action scenarios green; empty-folder scenario green. + +## WU16 — Connectivity states + +**Commit**: `feat(connectivity): restyle offline and reconnect banners` +**Depends on**: WU1 +**Spec refs**: crosscutting risk register (reconnect attempt counter is verify-first, not assumed) +**Verify**: `flutter test test/widgets/reconnect_ui_test.dart test/servicios/servicio_audio_reconnect_test.dart && flutter analyze && dart format .` +**Modified tests**: `reconnect_ui_test.dart`. `servicio_audio_reconnect_test.dart` must pass unmodified. + +- [ ] 16.1 Verify-first — run `servicio_audio_reconnect_test.dart` and inspect the reconnect controller: confirm + whether it tracks an attempt count. +- [ ] 16.2 RED — restyle test for the offline banner (visual only) and, **only if 16.1 confirms an attempt count + exists**, a test for the "Reconectando · intento N de M" label. +- [ ] 16.3 GREEN — restyle the offline banner to the new visual language. +- [ ] 16.4 GREEN — **conditionally**: wire the attempt-count label only if the controller exposes one; otherwise + ship the restyle without the counter — do not add new plumbing to invent one. +- [ ] 16.5 REFACTOR — confirm shimmer skeletons (`TarjetaEmisoraShimmer`) are unchanged. +- [ ] 16.6 Verify — `reconnect_ui_test.dart` updated and green; `servicio_audio_reconnect_test.dart` unmodified and + green. + +## WU17 — Welcome / onboarding screen + +**Commit**: `feat(bienvenida): add monetization-free welcome screen` +**Depends on**: WU1 +**Spec refs**: `onboarding-welcome` — Full-Screen Welcome Route, No Monetization Content, CTA Dismisses to Escuchar +**Verify**: `flutter test test/pantallas/pantalla_bienvenida_test.dart && flutter analyze && dart format .` +**New tests**: `pantalla_bienvenida_test.dart` + +- [ ] 17.1 RED — the welcome screen renders as a full-screen route (not a modal `Dialog`) with a logo, headline, + body copy, exactly 3 feature bullets, and exactly 1 primary CTA. +- [ ] 17.2 RED — binding no-monetization guard: scan the rendered widget tree's text content and assert it contains + none of: the substring "PRO", a currency amount, or a day-count trial phrase; no secondary "free version" + link. +- [ ] 17.3 RED — tapping "Empezar a escuchar" lands on the Escuchar tab (index 0) with the welcome route removed + from the back stack. +- [ ] 17.4 GREEN — build `lib/pantallas/pantalla_bienvenida.dart` as a full-screen route; content only (logo / + headline / body / 3 bullets / 1 CTA) — no PRO pill, no trial duration, no price, no secondary link. +- [ ] 17.5 GREEN — wire the CTA to dismiss-and-navigate to Escuchar with the route removed. +- [ ] 17.6 REFACTOR — grep `lib/l10n/app_*.arb` for "PRO"/price/day-count trial strings introduced by this WU; + confirm none exist. +- [ ] 17.7 Verify — all 3 scenario tests green; grep scan clean. + +## WU18 — i18n batch (all 13 locales) + +**Commit**: `feat(i18n): add redesign strings and translate Escuchar rename to 11 locales` +**Depends on**: WU1-WU17 (runs last, after `en`/`es` land across all slices) +**Spec refs**: `app-navigation-shell` — Escuchar Tab Rename Preserves the ARB Key (11-locale completion); backs the +UI copy every other capability introduced +**Verify**: `flutter test && flutter analyze && dart format .` +**Modified tests**: ARB key-parity guard test (extend existing or add one if none exists) + +- [ ] 18.1 RED — an ARB-parity test asserting all 13 `lib/l10n/app_*.arb` files contain an identical key set. +- [ ] 18.2 RED — `navHome` exists as a key in all 13 locales, and `git diff` for this commit touches exactly 11 + `navHome` **value** lines (en/es were WU1's job, task 1.9). +- [ ] 18.3 GREEN — add the ~35-45 new keys (accumulated from WU4-WU17's English/Spanish-only strings) to all 13 + ARB files, sourcing translations for the 11 non-en/es locales. +- [ ] 18.4 GREEN — set the `navHome` value in the remaining 11 locales to a real translation of "Listen" (not a + mechanical copy of the English/Spanish pair). +- [ ] 18.5 REFACTOR — run the ARB-parity check across all 13 files; confirm zero missing/extra keys. +- [ ] 18.6 Verify — full `flutter test` suite green; `flutter analyze` clean; `dart format .` produces no diff; + grep confirms no "PRO"/price/trial-duration string exists in any ARB file (final whole-app Success Criteria + check). + +--- + +## Final Verification Sequence (run once, after WU18's commit) + +1. `flutter test` — full suite green. +2. `flutter analyze` — clean. +3. `dart format .` — no diff. +4. `git diff --stat lib/servicios/navegacion_auto.dart lib/servicios/servicio_ecualizador.dart` — both empty. +5. `git diff lib/servicios/servicio_audio.dart` — band-application block (`749-762`) unchanged. +6. Grep `lib/` for `countrycodes` — zero hits. +7. Grep `lib/l10n/app_*.arb` for "PRO", a currency amount, or a day-count trial phrase — zero hits. +8. Confirm all 13 ARB files have identical key sets and `navHome` present in each. +9. Confirm the equalizer screen renders exactly 5 sliders. +10. Run `test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart` in isolation — passes, file diff empty. + +## Next Step + +`sdd-apply` — implement WU1 first (unblocks everything else); honor the `size:exception` commits (WU3a, WU3b, WU10) +as recorded above rather than attempting to split them further.