# PLAN — eje focus-first (firmado 2026-08-26) **Firma del autor**: «lo mejor para la arquitectura y diseño del framework independientemente de lo que cueste» + «ejecuta el eje focus-first completo». La amputación de `Morfo.focus` queda descartada; la dirección es la inversa: **completar el contrato y darle ejecutor y censo**. **Origen**: informe de auditoría (hallazgo verificado nº8) + contraste del corpus (eidos.md:472 falso; regla 2-de-3 a cero) + la inversión del autor: «el error quizás sean los componentes que deberían declararlo». Censo que funda el eje: **~21+ componentes con comportamiento de foco** (7 con `FocusScope`: dialog, drawer, popover, context-menu, dropdown-menu, float-panel, menubar · **14 de familia roving** con tab-stop único reimplementado a mano: toolbar, tabs, listbox, radio-group, stepper, tag-group, tags-input, toggle-group, carousel, drag-drop, tree-view, tree-grid, grid-list, chronos) contra 9 declaraciones históricas — y la forma vieja `{initial, trap, return, restore}` **no podía expresar roving**. ## La tesis del eje 1. **La declaración es el DEFAULT de especie; la prop por-instancia manda** (patrón `dir`). `` sigue decidiendo en el consumidor. 2. **Dos familias, una unión discriminada** — la forma vieja era un fragmento para overlays; la nueva nombra el mecanismo: ```ts type MorfoFocus = | { kind: 'trap'; trap: boolean; // default de especie initial?: 'first-focusable' | 'trigger' | { partRef: string }; return?: 'trigger' | 'previous' | { partRef: string }; restore?: boolean; } | { kind: 'roving'; // ENSANCHADO en F2: toolbar rueda sobre VARIAS partes // (button, link, group-item) — un partRef singular mentiría. parts: readonly string[]; orientation: 'horizontal' | 'vertical' | 'both' | 'grid'; loop?: boolean; executor?: { custom: string }; }; // excepción FIRMADA (censo) ``` Validación en schema: `partRef` cruzado contra las partes; `custom` exige razón no vacía; `grid` sin `executor.custom` es inválido mientras el primitivo sea 1D (la mentira estructural se hace imposible, no prohibida). 3. **Dos ejecutores, uno por familia**: - `trap` → `FocusScope` gana una entrada `policy` (el `compiled.focus`) y la usa como fallback real: `trap = trapFocus ?? modal ?? policy.trap`. - `roving` → **renace `RovingFocusGroup`** (`$adom`, hoy muerto): se corrige su defecto medido (tab-stop por id → por elemento) y pasa a ser EL ejecutor del patrón «exactamente un tab-stop»; las reimplementaciones a mano colapsan por olas (abajo). 4. **El censo hace imposible mentir** (patrón `direction-census.test.ts`, `focus-census.test.ts`): - morfo `trap` ⇒ su provider usa `FocusScope` Y el default de especie del wrapper (`modal`/`trapFocus`) coincide con `declaration.trap` — la clase Popover (declaraba true, embarcaba false) muere aquí; - morfo `roving` ⇒ el provider consume `RovingFocusGroup` O lleva `executor.custom` firmado; - **la inversa del autor**: componente que usa FocusScope/roving SIN declaración ⇒ FALLO. Excepciones, solo firmadas en el propio censo. 5. **Segundo consumidor = docs-site**: la pestaña A11y de cada componente renderiza la política de foco desde la declaración (como ya hace con `keyboard[]`) → la regla 2-de-3 se cumple con dos consumidores REALES (ejecutores + docs), no por gracia. ## Fases (cada una = commit(s) + verificación) - **F0 · árbol limpio** — vicen-42 revierte su amputación sin commitear (types/schema/compile + 9 morfos). Nadie más toca su delta. → verificar: `git status` limpio en ese frente; `MorfoFocus` presente. - **F1 · el contrato** — la unión discriminada en `types.ts`, validación en `schema.ts` (partRef cruzado, custom con razón, grid⇒custom), paso por `compile.ts` (`CompiledMorfo.focus`), y `SomaRuntime` expone `runtime.focus`. → verificar: `schema.test` + `compile.test` con casos nuevos (inválidos incluidos); `npm run check` src/ a 0. - **F2 · las declaraciones (~21+)** — los 9 históricos se reescriben a `kind: 'trap'` **con la verdad embarcada** (popover: `trap: false`); los 14 roving declaran `kind: 'roving'` con su orientación real; barrido del resto del catálogo por si hay foco sin censar. → verificar: sweep de `validateMorfo` (161+) verde; el censo F5 en rojo CONTROLADO (aún sin ejecutores) documenta el estado intermedio. - **F3 · ejecutor trap** — `FocusScope.use` acepta `policy`; los 7 providers pasan `runtime.focus`; ningún default de wrapper contradice la declaración. → verificar: suites de los 7 overlays; test nuevo: policy como fallback (sin props → manda la declaración; con `modal` → manda la prop). - **F4 · ejecutor roving** — (a) `RovingFocusGroup` corregido (tab-stop por elemento, `''` inválido, membresía viva) + su suite; (b) **ola 1** de migración (los 1D limpios: toolbar, tabs, radio-group, toggle-group, tag-group, menubar); (c) **ola 2** (listbox, stepper, carousel, tags-input — cada uno con su matiz: typeahead, linear-gate…); (d) los 2D/complejos (tree-view, tree-grid, grid-list, chronos, drag-drop) declaran `executor: { custom: }` — firmados, no migrados a la fuerza a un primitivo 1D. → verificar por ola: suite del componente + `perm:check` de sus rutas + interacción de flechas/Home/End en navegador. - **F5 · el censo** — `focus-census.test.ts` con las tres direcciones y las excepciones firmadas en el fichero. → verificar: censo verde; plantar una mentira (declarar trap:true en popover) y ver el censo en ROJO — un guard que no sabe fallar no vale. - **F6 · doctrina y docs-site** — morfo.md (fila de `focus` = ejecutada POR FIN, con sus dos ejecutores), eidos.md:472 corregida, soma-architecture, glossary; la pestaña A11y del docs-site renderiza la política. → verificar: `docs:check` 0; página de un trap y un roving en navegador. - **F7 · cierre** — `npm run gate` entero + `morfo:check` + actualización de CONTINUE-audit-p0.md y memoria. ## Leyes que este eje debe respetar - Árbol compartido: add por lista explícita, verificar+commitear en la misma invocación; los deltas de otros ejes no se tocan ni se stagean. - La declaración NUNCA contradice lo embarcado: donde difieran, la verdad embarcada manda y la declaración se corrige (Popover es el precedente). - Un componente 2D no se fuerza al primitivo 1D: excepción firmada > mentira estructural. - `runed`/`tabbable` port: `continue-runed-tabbable-port.md` existe en process/ — leerlo antes de F4a por si el port pactó decisiones sobre el tab-stop.