Files
pluriwave/openspec/changes/rediseno-funcional/tasks.md
T
FreeTLab 610016e935 test(auto): confirm Android Auto tree matches the redesign, no code changes
Re-ran test/servicios/navegacion_auto_test.dart (129 tests) unmodified —
all pass. git diff for lib/servicios/navegacion_auto.dart is empty; this
commit makes zero code changes.

Formally closes A1-A5 from the proposal's Android Auto surface review. The
app uses the classic MediaBrowserService model (automotive_app_desc.xml,
no androidx.car.app); Android Auto's system templates render everything,
and the only controllable surface is MediaItem metadata, which already
matches the redesign's intent. A4's waveform visualization is confirmed
non-implementable on this platform — MediaBrowserService cannot render
custom widgets, only system-templated browse/playback UI.
2026-07-28 20:06:33 +02:00

614 lines
47 KiB
Markdown

# 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`
- [x] 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.
- [x] 1.2 RED — extend the tokens test asserting `PluriWaveTokens.listSurface`, `.liveGreen`, `.offlineAccent` exist
and participate in `lerp`.
- [x] 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()`.
- [x] 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.
- [x] 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`).
- [x] 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.
- [x] 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(...)`.
- [x] 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.
- [x] 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).
- [x] 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).
- [x] 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
- [x] 2.1 Run `flutter test test/servicios/navegacion_auto_test.dart`; confirm every case passes unmodified.
- [x] 2.2 Confirm `git diff lib/servicios/navegacion_auto.dart` is empty — no code change in this commit.
- [x] 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).
- [x] 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<EstadoNavegacionRaiz>().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<EstadoRadio, T>` 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.