You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/CONTINUE-direction-runtime.md

495 lines
25 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# CONTINUE — la dirección deja de ser una convención
Handoff para una sesión nueva. **Leer entero antes de tocar nada**: el refactor
es pequeño en líneas y grande en alcance — toca un mecanismo del que cuelga todo
el catálogo.
Contexto previo: `docs/process/CONTINUE-direction.md` (el eje entero) y
`docs/canon/direction-contract.md` (el contrato, que es lo que este refactor
automatiza). **No hay que releerlos para empezar**; lo que hace falta está aquí.
---
## 1. Por qué — los números
El contrato es correcto. El **mecanismo** para cumplirlo es teclear la misma
línea en cada componente, y por eso falla:
| | veces |
| -------------------------------------------------------------- | ------------ |
| Estampado del `dir` escrito a mano en un provider | **54** |
| Enlace al padre compuesto a mano en un envoltorio | **7** |
| `Content` portalizados que re-arrancan la cadena por su cuenta | **9** |
| Componentes que aceptaban `dir` y **no** lo estampaban | **20 de 55** |
Los 20 se arreglaron uno a uno (`21055bd3c`), leyendo receta y envoltorio de
cada uno. Encontrarlos costó una auditoría. **El siguiente componente que
alguien escriba volverá a tenerlo**, porque nada lo impide: la regla vive en la
cabeza del que lo escribe.
Una convención que hay que repetir 54 veces y que **20 de 55 incumplían no es
una convención — es una abstracción que falta**.
---
## 2. Las dos comprobaciones, HECHAS
Las dos condiciones de viabilidad están verificadas. No hay que repetirlas.
### 2.1 · ¿Puede el runtime emitir un atributo que a veces debe estar AUSENTE?
**Sí, ya lo hace.** `src/uix/soma/runtime.svelte.ts:519-522`, en `renderProps()`:
```ts
for (const plan of compiledPart.dynamicAttrs) {
const value = evalAttrPlan(plan, bindings);
if (value !== undefined) props[plan.attr] = value;
}
```
`undefined` ⇒ el atributo **no se escribe**. Es exactamente la semántica del
estampado crudo: nadie afirmó ⇒ no hay atributo ⇒ el elemento hereda.
`resolveProps()` (`:496-503`) hace lo mismo. **No hay que construir nada para
esto.**
### 2.2 · ¿Y sin declararlo en el morfo?
**También, y hay precedente literal.** `partPropsForRegistration()`
(`runtime.svelte.ts:538-551`) ya inyecta `id`, el marcador del componente,
`data-archetype` y `reg.attachment` **sin ninguna declaración en el morfo**, y
el comentario de `data-archetype` justifica por qué:
> `data-archetype` is part of static identity (cross-component classification,
> never mutates), so it ships through partProps — not through dom.apply.
Así que **el movimiento 1 NO exige tocar 55 ficheros de morfo.** Hay dos
vehículos posibles y la elección es de quien lo haga (§4.1).
### 2.3 · ¿Tiene la capa flotante acceso al dueño?
**Sí.** Los sub-proveedores llevan `this.provider` (verificado en `select` y
`popover`).
> ⚠️ **El reparto que decía este apartado era FALSO, y la fuga no estaba donde
> decía.** Censo real (§9.1): de los 11 `FloatingContent.create`, **10** pasan
> `dir: opts.dir` y **1** (`sidebar`) pasa `this.provider.opts.dir` — no 8 y 3.
> Y ese `opts.dir` **no era una fuga**: los 7 envoltorios `Content` que lo
> alimentan ya componían el enlace al padre a mano, así que la afirmación de la
> raíz SÍ cruzaba. Lo que no cruzaba era otra cosa (§9.1).
---
## 3. Qué está mal hoy, en una frase
**El estampado y la propagación son dos convenciones manuales**, y el eje entero
—cuatro auditorías, tres barridos y unos veinte defectos— es lo que cuesta
mantenerlas a mano.
---
## 4. Los dos movimientos
### 4.1 · El estampado pertenece al runtime
**Objetivo**: que las 54 líneas escritas a mano se vuelvan cero, y que un
componente que acepte `dir` no pueda dejar de estamparlo.
**Vehículo A — inyección en `partPropsForRegistration`.** El provider registra
su parte pasando la dirección cruda; el runtime la emite junto a `id` y
`data-archetype`. Precedente exacto (§2.2). Mínimo código. Coste: el runtime
pasa a conocer un concepto que hoy no conoce.
**Vehículo B — un `dynamicAttr` universal en el compilador de morfo.** El
compilador añade a la parte `provider` un `AttrPlan` para `dir` con fuente
`fromProp: 'dir'`. Encaja con «morfo declara, soma ejecuta», y `renderProps()`
ya lo omitiría solo cuando resuelve `undefined` (§2.1). Coste: toca el
compilador y su caché; hay que mirar `contracts.cssSelectors`, porque el
conjunto cerrado de selectores que eidos puede usar se calcula ahí.
⚠️ **Decidir A o B es la primera tarea, y no está decidida.** Mi inclinación es
**A** por tamaño y por precedente, pero B es más fiel a la doctrina. Léete los
dos sitios antes de elegir.
⚠️ **Lo que NO debe pasar**: que el atributo aparezca en componentes que hoy
NO deben llevarlo. Son cuatro y están verificados: `popover`, `tooltip`,
`link-preview` y `context-menu` — su raíz no renderiza elemento o no pinta nada
direccional, y su pintura vive en el panel portalizado. Si el runtime lo emite
universalmente, esos cuatro ganan un atributo inútil. **Es la parte delicada del
movimiento 1**: la emisión tiene que colgar de que el componente ACEPTE `dir`,
no de que exista una parte `provider`.
⚠️ Y **la excepción del §2 del contrato**: `avatar` e `image` NO aceptan `dir` a
propósito (su `:dir()` sólo traduce a físico una prop lógica que ya eligió el
consumidor). Si acaban con la prop por efecto colateral, el refactor se ha
llevado por delante una decisión firmada por el usuario.
### 4.2 · La propagación pertenece a la capa flotante
**Objetivo**: que la afirmación de la raíz cruce el portal sin que nadie escriba
el enlace.
Hoy cada `Content` portalizado vuelve a arrancar `activeDir` desde cero, y por
eso la afirmación no cruza. La forma: **`FloatingContent` toma la dirección del
proveedor DUEÑO, y el `dir` propio del Content la pisa si existe.** Una
composición dentro de la capa en vez de once fuera.
Los 7 enlaces compuestos a mano (`dropdown-menu` ×2, `menubar`, `select`,
`combobox`, `context-menu` ×2) y el de `color-picker` se borran.
Y **cierra sin trabajo extra el pendiente que quedó reportado**: ningún picker
de eidos reenvía `dir` a su `PopoverContent`, así que el cromo del `picker-shell`
dentro del portal (`picker-shell.css:78`, el split del pie) está sin cubrir en
los siete. Con 4.2 deja de ser una tarea; sin 4.2 son seis parches idénticos.
---
> ✅ **4.2 EJECUTADA — ver §9.** Lo de abajo es el enunciado original, conservado
> porque el diagnóstico cambió al verificarlo.
## 5. Orden, y por qué
**4.2 primero.** Es más pequeña, está más acotada (una capa, once consumidores)
y su verificación es directa: abrir un `Select` y comprobar que trigger y panel
coinciden. Además borra código, así que 4.1 llega a un árbol más limpio.
**4.1 después**, y sólo cuando 4.2 esté verde y commiteada.
---
## 6. Verificación — la línea base exacta
Antes de empezar, tomar estas medidas y **compararlas al final**. Cualquier
desviación hay que explicarla, no absorberla:
| | valor esperado |
| ------------------------------------ | -------------------------------------------------------------------------- |
| `npx svelte-check --threshold error` | **77** (la base; sube si otra sesión toca `media-player`) |
| `npm run rtl:check` | **1 error** — `palabras-chrome.css:446`, preexistente y excluido |
| `npm run docs:check` | 0 errores sobre 564 docs |
| Tests de los componentes tocados | verdes; `eidos/lint.test.ts` falla por `audio-player` sin morfo, **ajeno** |
⚠️ **En navegador, y no es opcional**: el censo del estampado NO se puede hacer
por grep. Dos componentes (`listbox`, `navigation-menu`) estampan a través de un
alias `assertedDir` y un grep del patrón literal los da como huecos. La
comprobación buena es leer el DOM:
```js
// con la demo abierta, por cada componente
document.querySelector('[data-select]').getAttribute('dir');
```
⚠️ **El panel del navegador tiene que estar VISIBLE.** Oculto no compone frames:
no hay capturas y **las transiciones CSS se congelan**, así que
`getComputedStyle(...).transform` devuelve la matriz identidad y parece un
defecto que no lo es. Me pasó dos veces.
---
## 7. Trampas ya pagadas — no volver a pisarlas
- **El estampado es el CRUDO** (`opts.dir.current`), nunca `resolvedDir`. Un
valor resuelto escribe `dir="ltr"` cuando nadie afirmó nada, forzando el
subárbol a LTR dentro de una página RTL. Pasó en `sidebar` y lo escribí yo.
- **`dir` en un `<svg>` no hace nada** — la hoja UA de HTML no alcanza a los
elementos SVG. Va en el envoltorio HTML. Medido en Chrome.
- **`OptsFromProps` quita el `undefined`** del prop público y destruye la
distinción entre «nadie afirmó» y `'ltr'`. Quien lo use declara `dir` aparte.
- **Clasificar esto por grep se equivoca.** Yo di `avatar` por defectuoso
leyendo el patrón en vez del componente, y el usuario lo cazó. Abre la receta,
abre el provider, mira dónde renderiza cada parte.
- **Una migración mecánica hereda los defectos que traduce.** El barrido de
§6.3 cambió doce selectores sin mirar los cuerpos y arrastró dos dobles
volteos hasta que los cazó el guard RTL-2.
---
## 8. Estado del árbol al escribir esto
Rama `alpha-0.1-dir-prefs`, con remoto `gita` (no hay `origin`). Último commit
del eje: `21055bd3c`. Sin tocar y **no commitear**: `.claude/settings.local.json`,
`src/arts/adom/__scratch-verify.ts`, `web/routes/alpha/`. `media-player` lo
edita otra sesión — excluirlo de todo staging.
---
## 9. Sesión 2026-08-04 — §4.2 ejecutada
Sin commitear. Rama `alpha-0.1-dir-prefs`, **HEAD se movió a `4755d97d1`
mientras trabajaba** (otra sesión, `media-player` + `audio-player`): esos
ficheros están fuera de este cambio y fuera de cualquier staging.
### 9.1 · El censo del §2.3 estaba mal, y la fuga era otra
Rehecho leyendo los ficheros, no el patrón:
| | |
| --- | --- |
| `FloatingContent.create` | **11** sitios en 9 ficheros |
| …pasando `dir: opts.dir` | **10** |
| …pasando `this.provider.opts.dir` | **1** (`sidebar`) |
| Envoltorios `Content` que componían el enlace al padre A MANO | **7** |
| Envoltorios `Content` SIN enlace | **3** — `popover`, `tooltip`, `link-preview` |
**Y esos 3 no eran un defecto**: sus raíces no aceptaban `dir` en absoluto, así
que no había nada de la raíz que heredar. O sea **entre los 11 consumidores del
motor flotante no había ninguna fuga**.
**La fuga estaba un piso más arriba: en quien COMPONE un popover.** Nueve
componentes crean un `PopoverProvider` internamente
(`color-picker`, `date-picker`, `date-range-picker`, `emoji-picker`,
`gradient-picker`, `natural-time-picker`, `picker`, `time-picker`,
`time-range-picker`) y **siete de ellos aceptan `dir`** — pero `PopoverProvider`
no tenía dónde recibirlo, así que la afirmación moría en el borde del portal.
Medido en Chrome ANTES, con `<DatePicker dir="rtl">` y la página en LTR:
```
[data-floating-wrapper] dir="ltr" ← el panel entero forzado a LTR
[data-picker-shell] direction ltr
[data-picker-footer] direction ltr Clear x=15 · Cancel x=102 · Close x=209
[data-calendar] direction RTL ← lo ÚNICO que espejaba
```
El calendario mirroreaba porque es un componente soma propio que recibe el `dir`
por el árbol de proveedores; el cromo que lo rodea —el `picker-shell`, cuyo pie
reparte con `margin-inline-end: auto` (`picker-shell.css:78`)— no. **Panel
partido por la mitad**, que es exactamente el fallo del §2 del contrato.
### 9.2 · La forma implementada
```
el dir propio de la superficie → el dir afirmado por el proveedor DUEÑO → omitir
```
- `FloatingProviderOpts.dir` — el dueño baja su afirmación (ya pasada por
`activeDir`, o sea `prop → prefs`, así que la cola de prefs queda intacta).
- `FloatingContent.assertedDir` — `opts.dir.current ?? provider.opts.dir.current`.
Los dos estampados del wrapper (rama nativa y rama JS) leen de ahí.
- `createFloatingShellRoot` lo reenvía; es la puerta de 8 de los 10
`FloatingProvider`. Los otros 2 (submenús de `dropdown-menu` y `context-menu`)
pasan el del menú padre.
⚠️ **El punto que decide si esto funciona**: `FloatingContentOpts.dir` pasa a ser
**la prop cruda del Content**, no el resultado de `activeDir`. Si el envoltorio
resolviera ahí, el valor sería casi siempre concreto (prefs) y el dueño **nunca**
ganaría el fallback. Los 10 envoltorios pasan ahora `readableActive(() => dir)`.
Consecuencias:
- **Los 7 enlaces compuestos a mano se borran**, y con ellos el `soma`, el
`activeDir` y el `parentRoot`/`parentMenu` de cada envoltorio.
- `popover`, `tooltip` y `link-preview` **aceptan `dir` en su raíz** (prop
pública nueva + opt). No estampan nada —no renderizan elemento—; el valor
existe para cruzar el portal, y es lo que permite que un picker empuje su
dirección al panel. Sin esa prop habría que escribir
`activeDir(() => undefined, soma)` en la raíz, que es peor y no da nada a cambio.
- Los 9 composers pasan su dir al `PopoverProvider`. `gradient-picker` y `picker`
no aceptan `dir` (§9.17 del eje), así que corren la cadena con el primer
eslabón ausente: `activeDir(() => undefined, soma)`.
**Queda UN `activeDir` con enlace al padre**, y a propósito:
`dropdown-menu-sub-content.svelte` lo necesita para `effectiveSide` (el lado por
defecto del submenú es un `left`/`right` concreto, o sea MATEMÁTICA, que sí
quiere valor resuelto). El ATRIBUTO ya no sale de ahí.
### 9.3 · Verificación
Medido en Chrome real, con la página en LTR y el `dir` movido por el control de
la demo (el camino real, nunca `setAttribute`):
| | antes | después |
| --- | --- | --- |
| `date-picker` · wrapper | `ltr` | **`rtl`** |
| `date-picker` · shell / footer | `ltr` | **`rtl`** |
| `date-picker` · Clear/Cancel/Close x | 15 / 102 / 209 | **200 / 92 / 15** |
| `date-range-picker` · Clear/Cancel/Close x | — | **448 / 217 / 17** |
Y a ojo: el panel entero espeja —cabecera, rejilla de días y pie—, no sólo el
calendario. **Sin regresión** en los que ya funcionaban, comprobados uno a uno:
`dropdown-menu` (raíz y submenú, con `data-side=left` en RTL), `context-menu`,
`menubar`, `select` (por prefs, las dos direcciones), y `tooltip` en `auto`, que
sigue estampando `ltr` — la cola de prefs no se perdió.
| guard | resultado |
| --- | --- |
| `npx svelte-check --threshold error` | **78 = línea base** (A/B con stash: 78 sin mis cambios) |
| Tests de los 20 ámbitos tocados | **158/158** |
| `npm run rtl:check` | **1** — `palabras-chrome.css:446`, preexistente |
| `npm run docs:check` | 0 sobre 565 |
| Pasada completa de vitest | 9 fallos, **ninguno mío** (A/B con el árbol limpio: los 6 deterministas de `contracts`/`eidos-lint` salen igual; `soma-attr-audit`, `orca` y `cookie-adapter` pasan aislados — flaky bajo carga) |
⚠️ La línea base de `check` es **78**, no 77 como decía §6 — con el
`__scratch-verify.ts` sin commitear en el árbol.
### 9.4 · Trampas de esta sesión
- **Lancé `prettier --write` sobre los 43 ficheros tocados y fue un error.**
Reformateó líneas PREEXISTENTES (imports colapsados, prosa de dos READMEs
reflowada: `popover/README.md` pasó de +7/−4 a +49/−46). Deshecho a mano hasta
devolver la diff a su forma quirúrgica. La regla del §5 del eje sigue viva:
**formatea sólo lo que ensuciaste, y mira el tamaño de la diff después.** Lo
único que se conserva del formateo son dos líneas MÍAS que superaban las 100
columnas.
- **Un click JS justo después de navegar no engancha**: hasta que Svelte hidrata,
`.click()` no dispara el handler. Y leer el atributo en la MISMA llamada que el
click devuelve el valor viejo — hay que separar en dos llamadas.
- **El control de dirección de algunas demos vive bajo la pestaña «eidos
System»**, no en la vista Live. Sin cambiar de pestaña no hay chips que pulsar.
### 9.5 · Lo que queda apuntado, sin tocar
1. **9 campos `resolvedDir` MUERTOS en los proveedores `Content`**
(`select` 495, `combobox` 577, `dropdown-menu` 314 y 1230, `context-menu` 252
y 995, `menubar` 563, `tooltip` 409, `link-preview` 268). Verificado que
**nadie los lee** — toda la matemática direccional consume
`this.provider.resolvedDir`, o sea el de la RAÍZ. Ya eran código muerto antes;
ahora además su docblock («Direction for this part's OWN math») describe algo
que `opts.dir` ya no es. **Borrarlos requiere tu palabra.**
2. **Agujero de observabilidad en 5 demos de picker**: `time-picker`,
`time-range-picker`, `color-picker`, `emoji-picker` y `gradient-picker` tienen
el control `dir` del arnés pero **no se lo pasan al componente**, así que la
mitad que ejercita el portal no se puede ver. Sólo `date-picker` y
`date-range-picker` lo pasan — y por eso son los dos que pude medir. Mismo
agujero que §8.2 del eje, misma clase de arreglo.
3. **`context-menu-sub-content` clava `side = 'right'`** (físico) mientras
`dropdown-menu-sub-content` lo deriva de la dirección. Incoherencia entre dos
componentes hermanos, preexistente y ajena a este cambio.
4. **§4.1 sigue entera y sin empezar** — el estampado del `dir` en el runtime, y
la elección Vehículo A vs B sigue sin tomar.
---
## 10. Auditoría de la sesión — lo que salió al re-verificarlo todo
### 10.1 · Código muerto: BORRADO
Los 9 `resolvedDir` de §9.5.1 están **eliminados**. Antes de borrar, verificado
que ninguno se lee: las 5 lecturas vivas de `resolvedDir` en esos ficheros son
todas `this.provider.resolvedDir` —o sea el de la RAÍZ— desde
`SelectTriggerProvider`, `MenuContentProvider`, `ContextMenuContentProvider`,
`MenubarMenuProvider` y `MenubarTriggerProvider`. Cero lecturas desde eidos,
blocks, packs, `web/routes` o tests. Quedan **5** declaraciones, las cinco de
raíces vivas.
### 10.2 · Las demos: las 5 cableadas
`time-picker`, `time-range-picker` y `color-picker` no tenían control de
dirección **en absoluto**; `emoji-picker` y `natural-time-picker` lo tenían en el
arnés pero **no se lo pasaban al componente**. Ahora las siete demos de picker
que aceptan `dir` lo pasan a la instancia, así que el camino de la PROP —el
único que ejercita el portal— es observable en todas.
⚠️ **Corrección a §9.5.2**: dije «5 demos con el control pero sin pasarlo». Falso
en dos sentidos: tres no tenían control, y `gradient-picker`/`picker` no aceptan
`dir` (§9.17 del eje), así que no entran.
### 10.3 · Censo de ISLAS — un defecto nuevo, PREEXISTENTE y de otra familia
Con las demos cableadas se puede al fin escanear cada panel en RTL buscando
elementos que estampen una dirección DISTINTA a la del envoltorio flotante:
| panel | islas |
| --- | --- |
| `date-picker` · `date-range-picker` · `time-picker` · `time-range-picker` | **0** |
| `natural-time-picker` | **1** — `[data-slider]` |
| `emoji-picker` | **3** — `[data-command]` + `[data-toggle-group]` ×2 |
| `color-picker` | **1** — `[data-color-field-format-select]` (un `Select`) |
**La forma es la misma que arregló §4.2, pero para composición EN SITIO en vez
de por portal**: un componente eidos monta otro componente del canon
(`natural-time-picker-panel.svelte` → `Slider.Provider`,
`emoji-picker-content.svelte` → `Command` + `ToggleGroup`,
`color-field-format-select.svelte` → `Select`) **sin reenviarle el `dir`**. El
hijo corre su propia cadena, cae en prefs, estampa `ltr` — y ese atributo CORTA
la herencia del panel, que sí es `rtl`.
Es **preexistente**: mientras el panel entero era LTR la isla no se veía. Lo que
hizo §4.2 fue volverla visible.
⚠️ **No lo he arreglado, y es deliberado**: no hay UN solo precedente en eidos de
reenviar `dir` a un componente compuesto (`grep "dir={" src/uix/eidos` da un
único resultado, y es `data-dir` de otra cosa). Arreglarlo es inventar el patrón
—doctrina nueva, no un parche— y toca 3 ficheros que este movimiento no toca.
**Es un movimiento propio.**
La misma clase de defecto vivía en la DEMO del `color-picker`, que monta `<Tabs>`
y un `<ColorField>` a mano dentro del panel: ahí sí lo arreglé, porque es demo y
porque si no el control de dirección que acabo de añadir enseñaría un panel medio
volteado y parecería defecto del framework.
### 10.4 · Verificación tras la auditoría
| | |
| --- | --- |
| `npx svelte-check --threshold error` | **78 = línea base** (A/B con stash: 78 sin mis cambios) |
| Tests de los 20 ámbitos tocados | **158/158** |
| `npm run rtl:check` | **1**, el de `palabras` |
| `npm run docs:check` | 0 sobre 565 |
| Islas en los 7 paneles | 4 paneles a 0; las 5 restantes documentadas arriba |
⚠️ **Trampa medida dos veces**: `svelte-check` dio **79** en una pasada y 78 en
la siguiente sin tocar nada. Era una CARRERA con la otra sesión guardando
ficheros de `media-player`. En una rama compartida, un número que no reproduce
no es un hallazgo — repite la medida antes de perseguirlo.
⚠️ Las 5 demos tocadas **ya fallaban `prettier --check` en HEAD**, así que no se
formatearon (§9.4).
---
## 11. ENDGAME ejecutado — 2026-08-05 (P1–P5, decisiones D1–D4 firmadas)
El plan completo vive en la sesión que lo ejecutó; lo durable está en el canon
(`direction-contract.md` §1/§2/§6) y en `docs/decisions.md`. Resumen operativo:
| fase | commit | qué |
| --- | --- | --- |
| P1 | `9158d0796` | **DirectionContext dentro de `activeDir`** — la cadena gana el eslabón del ancestro; los 3 reenvíos de 4.3, el enlace manual del submenú y los 3 canarios de media-player se BORRAN. El repro de la otra sesión pasa sin ellos. |
| P2 | `973d2c886` | **Proyección automática + semilla** — `createActiveUix` proyecta por defecto (`projectPrefs:false` opt-out; attach opt-in); `readPrefsEnvironmentFromDom` siembra `<html dir>` puesto a mano (`intent > env > derive > default`). Los 3 cableados manuales borrados en el MISMO commit. |
| P3 | `01cc13d41` | **El flip** — `activeDir(dir)` devuelve la AFIRMACIÓN (prop ?? ctx), sin prefs y sin parámetro soma; la matemática vive en `resolveDir(dir, soma)`. En `auto` la página entera lleva UN `dir` (el `<html>`). SSR: cero `dir` en el payload. |
| P4 | `fc84305c2` | **El morfo declara** — `direction: { parts }` en 51 morfos; `soma.runtime<M>` computa el requisito del tipo (declarado ⇒ requerido; no ⇒ prohibido); `compileMorfo` valida parts fail-closed; censo prop↔morfo como guard (`direction-census.test.ts`) con las excepciones firmadas (field-langs, waveform, popover/tooltip/link-preview). La ceremonia de 4.1 (67+47 `dir: null`, 49 getters) MUERE. |
| P5 | (este commit) | `activeEidosDir` gana el contexto (compartido con soma) y se parte igual (`resolveEidosDir`); charts al día; corpus + decisiones + memoria. |
**El estado final del eje**: nadie escribe `dir=` a mano; ausente hereda de
verdad; olvidar el cable no compila; el ambiente llega al DOM una vez. Los tres
`dir=` de media-player eran los canarios y CANTARON (panel rtl sin ellos).
### Verificación final medida
- `auto`: **1** `[dir]` en toda la página (el `<html>` proyectado) — antes ~cada raíz.
- prefs rtl: select y su panel portalizado SIN atributo, computan rtl por herencia.
- prop rtl: trigger del dropdown estampa, el portal cruza, submenú `data-side=left`, 0 islas.
- matemática: slider sin atributo con página rtl — click 25% físico → 75, `ArrowRight` baja.
- SSR (curl): cero `dir=` en select y accordion.
- `check` 77 = línea base en todas las fases · `rtl:check` 1 (palabras) · `docs:check` 0/566 · suites 1401/1402 (el 1: `soma-attr-audit`, flaky bajo carga, pasa aislado).
### La cola que QUEDA (pospuesta por decisión D4 o ajena)
- ~~**Censo de API**~~ **CERRADO 2026-08-05 (tarde)** — los tres que quedaban:
`picker` y `gradient-picker` aceptan `dir?: Direction` (prop → `activeDir` →
morfo `direction: {}` → runtime; el genérico arrastra la opt a
natural-time-picker y gradient-picker por tipo), y `chat-log` acepta y
reenvía a los `Feed`/`VirtualList` que compone — **sus 2 errores de línea
base MUEREN: la base pasa de 77 a 75**. El censo-guard los cuenta sin
excepción nueva. Verificado en Chrome: gradient-picker root+panel `rtl` por
prop con 0 islas; chat-log AUSENTE en auto y `rtl` por prop.
- **Opts canónicas transversales** (bindProps v2) — eje propio; el censo
completo está en el reporte del agente de diseño de esta sesión.
- Deuda ajena intacta: 91 tests que falsean `Soma.require()` · cero tests de
charts · `smoke`/`perm:check` sin correr.
### Trampas nuevas de esta pasada, para no repetir
- `git commit` SIN pathspec en rama compartida se llevó por delante el índice
de la otra sesión una vez (b41669c43). Desde entonces: SIEMPRE `git commit --
<rutas>`.
- El detector de huérfanos por identificador se tropezó con la RUTA del import
(`core/soma.svelte` contiene "soma") — excluir líneas de import antes de
buscar usos.
- `readableActive`/vista de prefs construida DENTRO de una función pura llamada
por `$derived` = una alocación por pasada — cachear por instancia (WeakMap en
`resolveEidosDir`).
- El gate de P4 cazó a `command` (estampado inline en el assert que el barrido
4.1 no vio) y a 5 runtimes secundarios del mismo morfo — un tipo condicional
bien puesto encuentra lo que los barridos no.

Powered by TurnKey Linux.