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/canon/direction-contract.md

476 lines
24 KiB

docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
title: Direction Contract — how a component obtains, asserts and paints its reading direction
type: canon
audience: human + agent
authority: canonical — the resolution chain, which attribute carries it, and which selector form may read it
status: current
feat(direction): RTL-2 — el guard del doble volteo, la forma que se me colo dos veces Cierra §10.1, la ultima deuda del eje. `dee2c9e3a` arreglo los dos defectos pero dejo la FORMA sin guard, y es la que sobrevivio a la migracion §6.3 y a que yo diera el eje por cerrado en §9.19. LA FIRMA — dentro de un bloque cuyo prelude tiene `:dir()`, la misma familia logica (`border`/`padding`/`margin`/`inset`-`inline`) declarada en LAS DOS caras, EXACTAMENTE UNA con valor neutro (`0`, `auto`, `none`, `initial`, `unset`, `revert`). Ese desequilibrio ES el espejo cancelado: la propiedad ya se habia volteado cuando la regla matchea, asi que reubicarla la devuelve al punto de partida y la deja en el borde opuesto al de sus hermanas. Es mas estrecha que «bloque `:dir()` con puras logicas» a proposito. Una regla que CAMBIA un valor bajo RTL sin reubicarlo es legitima —asimetrico por diseño existe—, y las dos caras con valor son un autor describiendo dos bordes reales. Lo que delata el defecto es el PAR, una cara apagada. Hay un test negativo por cada uno de esos casos, que son los que evitan que la regla se vuelva ruido. LAS TRES DECISIONES que el handoff dejaba abiertas: - `:dir(ltr)` tambien entra. Igual de sospechosa, y no anade ruido. - Marcador PROPIO, `rtl-mirror: <reason>`. Reutilizar `rtl-physical:` mentiria: aqui no hay nada fisico, y un marcador que miente es peor que ninguno. Mismo mecanismo (`exemptLines` toma ahora el patron por parametro), dos vocabularios. Un test comprueba que el marcador de RTL-1 NO exime a RTL-2. - Regla aparte, `lintRtlMirror()` con su propio `RtlMirrorFinding`. Los 14 tests de RTL-1 quedan intactos y el tipo lleva los campos que importan (`family`/`neutral`/`payload`) en vez de forzar los de RTL-1. VERIFICACION, en este orden: - `rtl-lint.test.ts` 27/27 (14 de RTL-1 intactos + 13 nuevos). - RECALL con el runner COMPLETO contra el arbol pre-arreglo: restaure los dos ficheros de `dee2c9e3a^` sobre el arbol, corri `rtl:check` y los devolvi con `git checkout` en el mismo bloque. **2/2 cazados** (`feed.css:147`, `tree-view.css:211`). Probar la funcion no basta: el runner es lo que corre. - `rtl:check` sobre HEAD: 1 error, el de `palabras`, preexistente y excluido. - `docs:check` 0/0 sobre 564 docs. `check` 77 = linea base. Un defecto de presentacion salio al hacerlo: el `calc()` multilinea de tree-view partia el mensaje por el primer salto. Los campos que RTL-2 emite colapsan el whitespace; con test. Actualizados los textos que el handoff avisaba que quedarian obsoletos: el `enforcement:` y §4 del contrato, la fila RTL del build contract, la fila X-1.6 del checklist, y los docblocks de `rtl-lint.ts` y `rtl-check.ts` — que son doctrina, no adorno. ⚠️ Sigue siendo cierto lo que NINGUNA de las dos reglas ve: leen texto CSS. Un `transform` inline escrito por JS, un preset de motion compartido y la geometria SVG siguen necesitando el ojo en RTL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
enforcement: §4 only — scripts/rtl-check.ts (rules RTL-1 and RTL-2, `error`) · `npm run rtl:check`. §1–§3 and §5 have no mechanical guard; they are held by review and by the acceptance rules.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
related:
resolver: src/uix/soma/direction.ts (activeDir, and why it can return undefined)
feat(direction): RTL-2 — el guard del doble volteo, la forma que se me colo dos veces Cierra §10.1, la ultima deuda del eje. `dee2c9e3a` arreglo los dos defectos pero dejo la FORMA sin guard, y es la que sobrevivio a la migracion §6.3 y a que yo diera el eje por cerrado en §9.19. LA FIRMA — dentro de un bloque cuyo prelude tiene `:dir()`, la misma familia logica (`border`/`padding`/`margin`/`inset`-`inline`) declarada en LAS DOS caras, EXACTAMENTE UNA con valor neutro (`0`, `auto`, `none`, `initial`, `unset`, `revert`). Ese desequilibrio ES el espejo cancelado: la propiedad ya se habia volteado cuando la regla matchea, asi que reubicarla la devuelve al punto de partida y la deja en el borde opuesto al de sus hermanas. Es mas estrecha que «bloque `:dir()` con puras logicas» a proposito. Una regla que CAMBIA un valor bajo RTL sin reubicarlo es legitima —asimetrico por diseño existe—, y las dos caras con valor son un autor describiendo dos bordes reales. Lo que delata el defecto es el PAR, una cara apagada. Hay un test negativo por cada uno de esos casos, que son los que evitan que la regla se vuelva ruido. LAS TRES DECISIONES que el handoff dejaba abiertas: - `:dir(ltr)` tambien entra. Igual de sospechosa, y no anade ruido. - Marcador PROPIO, `rtl-mirror: <reason>`. Reutilizar `rtl-physical:` mentiria: aqui no hay nada fisico, y un marcador que miente es peor que ninguno. Mismo mecanismo (`exemptLines` toma ahora el patron por parametro), dos vocabularios. Un test comprueba que el marcador de RTL-1 NO exime a RTL-2. - Regla aparte, `lintRtlMirror()` con su propio `RtlMirrorFinding`. Los 14 tests de RTL-1 quedan intactos y el tipo lleva los campos que importan (`family`/`neutral`/`payload`) en vez de forzar los de RTL-1. VERIFICACION, en este orden: - `rtl-lint.test.ts` 27/27 (14 de RTL-1 intactos + 13 nuevos). - RECALL con el runner COMPLETO contra el arbol pre-arreglo: restaure los dos ficheros de `dee2c9e3a^` sobre el arbol, corri `rtl:check` y los devolvi con `git checkout` en el mismo bloque. **2/2 cazados** (`feed.css:147`, `tree-view.css:211`). Probar la funcion no basta: el runner es lo que corre. - `rtl:check` sobre HEAD: 1 error, el de `palabras`, preexistente y excluido. - `docs:check` 0/0 sobre 564 docs. `check` 77 = linea base. Un defecto de presentacion salio al hacerlo: el `calc()` multilinea de tree-view partia el mensaje por el primer salto. Los campos que RTL-2 emite colapsan el whitespace; con test. Actualizados los textos que el handoff avisaba que quedarian obsoletos: el `enforcement:` y §4 del contrato, la fila RTL del build contract, la fila X-1.6 del checklist, y los docblocks de `rtl-lint.ts` y `rtl-check.ts` — que son doctrina, no adorno. ⚠️ Sigue siendo cierto lo que NINGUNA de las dos reglas ve: leen texto CSS. Un `transform` inline escrito por JS, un preset de motion compartido y la geometria SVG siguen necesitando el ojo en RTL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
guard: src/uix/eidos/rtl-lint.ts · scripts/rtl-check.ts (RTL-1 · RTL-2)
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
prefs: src/arts/prefs/README.md (the app-global preference and its DOM projection)
architecture: docs/architecture/active-architecture.md (where prefs sits in the runtime)
guide: docs/guides/component-guide.md (the RTL rows of the build contract)
checklist: docs/guides/completion-checklist.md (the acceptance rules)
svg: src/uix/eidos/components/chart/README.md (§Direction — mirroring a graphic)
---
# Direction Contract
Reading direction is the one cross-cutting fact that every layer touches: the
consumer may assert it, the app derives it, soma computes with it, the DOM
carries it, and eidos paints from it. This chapter fixes **where each of those
happens** and, more importantly, **where they must not**.
There are two separate questions, and conflating them is the source of every
direction bug this framework has shipped:
1. **What direction does this component compute with?** — a concrete value, used
for arrow keys, pointer sign, placement flips, scroll maths.
2. **What direction does this component assert to the DOM?** — an attribute that
must be _absent_ when nobody asserted anything.
The answers are deliberately different types.
---
## 1. The chain
```text
feat(direction): el eslabon de contexto — la composicion hereda por fisica La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica (2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs. Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del ancestro antes de caer a prefs. Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma, fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL), y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el opts.dir del dueno — que ahora incorpora el contexto. Con la fisica, las convenciones se borran: - los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content x3, color-field-format-select) — el contexto los hace - el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem - los 3 canarios dir= de media-player que la otra sesion dejo puestos a proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no llego al portal") El canario canta: su repro exacto (media-player, manual parts, direction rtl, float de subtitulos) da panel rtl SIN el dir= — y el volume float igual. Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a 0.417 del borde fisico); submenu de dropdown con data-side=left en RTL derivado del contexto. Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper — nunca en constructores de provider. Los tests de provider construyen directo y no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined) cae a traves de prefs. Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un default), sin ancestro cae a prefs, y la publicacion es REACTIVA. check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro eslabones y §1 "Composition carries the assertion" documenta la fisica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
prop dir → ancestor assertion (DirectionContext) → soma.prefs.getDir() → 'ltr'
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```
feat(direction): el eslabon de contexto — la composicion hereda por fisica La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica (2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs. Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del ancestro antes de caer a prefs. Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma, fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL), y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el opts.dir del dueno — que ahora incorpora el contexto. Con la fisica, las convenciones se borran: - los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content x3, color-field-format-select) — el contexto los hace - el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem - los 3 canarios dir= de media-player que la otra sesion dejo puestos a proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no llego al portal") El canario canta: su repro exacto (media-player, manual parts, direction rtl, float de subtitulos) da panel rtl SIN el dir= — y el volume float igual. Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a 0.417 del borde fisico); submenu de dropdown con data-side=left en RTL derivado del contexto. Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper — nunca en constructores de provider. Los tests de provider construyen directo y no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined) cae a traves de prefs. Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un default), sin ancestro cae a prefs, y la publicacion es REACTIVA. check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro eslabones y §1 "Composition carries the assertion" documenta la fisica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Four links, ratified 2026-08-05.** The context link is the "implicit
inheritance" phase left open when the chain was first fixed: every `activeDir`
call **publishes** its assertion (`prop ?? inherited`) into `DirectionContext`
and consults the nearest ancestor's before falling back to prefs. Composition
carries the assertion by physics — a canon component mounted inside an asserted
subtree, in place or through a portal (the Portal forwards Svelte context),
resolves the ancestor's assertion without anyone writing `dir=` at the call
site.
What remains true: a component still does **not read the DOM**. The DOM `dir`
attribute is a _projection_ of the assertion/preference, never a source.
feat(direction): el flip — el atributo es la afirmacion, y undefined ACTUA La cadena se parte en la afirmacion. `activeDir` devuelve ahora SOLO lo que alguien eligio (prop -> contexto del ancestro) y ahi se detiene: eso es lo que el runtime estampa y lo que cruza portales. La preferencia de la app ya no viaja en el atributo de cada componente — llega a la pagina UNA vez, por la proyeccion automatica de P2, y todo lo demas HEREDA. Por que: con prefs dentro de la cadena por-componente, "nadie afirmo" era inalcanzable — el boot por defecto siempre tiene la dimension direction, asi que getDir() casi nunca era undefined y cada raiz del canon estampaba dir="ltr" en una pagina sin afirmaciones. El atributo nunca llegaba a estar AUSENTE, la herencia del DOM nunca llegaba a trabajar, y de ahi la clase entera de islas y reenvios que este eje llevo meses parcheando. La matematica conserva la preferencia como VALOR (no puede leer el DOM) en un solo helper: afirmacion -> soma.prefs.getDir() -> 'ltr' resolveDir() - activeDir(dir) pierde el parametro soma: 57 wrappers, y 49 quedan sin `const soma = Soma.get()` huerfano (limpiados con deteccion de identificador, no de ruta — la primera pasada se tropezaba con 'core/soma.svelte'). - 45 colas `opts.dir.current ?? 'ltr'` -> resolveDir(this.opts.dir, this.soma). - Los 4 getDirectionalKeys de submenus que leian opts.dir.current directo pasan a resolvedDir — con undefined degradaban a ltr EN SILENCIO bajo rtl ambiental. - command: 4.1 le habia dejado `dir: null` en el runtime y el estampado manual inline en el assert (la linea no estaba sola y el barrido no la vio). Ahora el runtime estampa y el inline se va. - listbox / navigation-menu: el alias assertedDir muere — tras el flip, opts.dir ES la afirmacion. - date-range-field-input / time-range-field-input: el reenvio dir= al Field compuesto en sitio sobra — DirectionContext lo lleva. - 7 wrappers renombran `resolvedDir` -> `assertedDir`: el nombre mentia. - picker / gradient-picker: activeDir(() => undefined) = la afirmacion del ancestro o nada — mejor que el prefs-siempre-concreto de antes. MEDIDO en Chrome, el modelo entero: auto UN solo [dir] en toda la pagina: el <html> de la proyeccion (antes lo llevaba casi cada raiz del canon) prefs rtl select sin atributo, computa rtl por herencia; el panel portalizado SIN atributo y computa rtl (heredo del html a traves del body) prop rtl trigger del dropdown estampa rtl, el panel portalizado estampa rtl (la afirmacion cruza), submenu data-side=left, cero islas dentro matematica slider SIN atributo con pagina rtl: click al 25% fisico da 75 y ArrowRight BAJA — resolveDir lee prefs sin que el atributo lo haga SSR curl de select y accordion: cero dir= en el payload El contrato: §1 documenta la cadena partida (activeDir -> afirmacion; resolveDir -> matematica), §6 la proyeccion automatica + la semilla y el parrafo de SSR — el <html dir> del primer render pertenece a app.html o al servidor (modelo react-aria). check 77 = linea base · suites soma+active-uix+prefs+contracts 1362 tests: los 6 de contracts y soma-attr-audit son el conjunto preexistente (A/B dos veces en fases previas; attr-audit pasa aislado) · media-player 17/17 tras hacer resolveDir defensivo con mocks sin prefs · rtl:check 1 (palabras) · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**The chain is split at the assertion.** `activeDir` returns the ASSERTION
(`prop → ctx`) and stops there — that is what the attribute stamps and what
crosses portals. The preference is deliberately NOT part of it: prefs reach
the page once, through the boot's automatic DOM projection on `<html>`, and
everything below inherits. Only the MATHS still needs the preference as a
value (it cannot read the DOM), and that tail lives in one helper:
| Link | Who runs it | Where |
| ----------------- | ----------------------- | -------------------------------------------------- |
| `prop → ctx` | `activeDir(getter)` | the **wrapper** `.svelte`, at `Provider.create(…)` |
| `→ prefs → 'ltr'` | `resolveDir(dir, soma)` | the **provider**, once, in `resolvedDir` |
feat(direction): el eslabon de contexto — la composicion hereda por fisica La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica (2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs. Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del ancestro antes de caer a prefs. Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma, fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL), y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el opts.dir del dueno — que ahora incorpora el contexto. Con la fisica, las convenciones se borran: - los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content x3, color-field-format-select) — el contexto los hace - el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem - los 3 canarios dir= de media-player que la otra sesion dejo puestos a proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no llego al portal") El canario canta: su repro exacto (media-player, manual parts, direction rtl, float de subtitulos) da panel rtl SIN el dir= — y el volume float igual. Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a 0.417 del borde fisico); submenu de dropdown con data-side=left en RTL derivado del contexto. Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper — nunca en constructores de provider. Los tests de provider construyen directo y no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined) cae a traves de prefs. Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un default), sin ancestro cae a prefs, y la publicacion es REACTIVA. check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro eslabones y §1 "Composition carries the assertion" documenta la fisica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
A component with no soma provider runs the same first links through
feat(direction): las charts aceptan `dir` y corren la cadena canonica Decision del usuario. Yo habia recomendado lo contrario —dejarlas y ablandar el contrato— y me equivocaba en el diagnostico: no era que la cadena no valiese para eidos, era que **le faltaba el punto de entrada**. `activeDir(getter, soma)` pide un `Soma`, y una chart solo tiene un `ActiveEidos`. Y `ActiveEidos.prefs` es un `ActivePrefs` crudo, que no tiene `getDir()` —ese metodo vive en la vista que mira a soma—. Por eso la cadena "no era ejecutable" y por eso alguien puso ahi un `getComputedStyle`. La adaptacion entre las dos superficies es TODA la diferencia: src/uix/eidos/direction.ts → activeEidosDir(getter, prefs) Mismos dos eslabones, mismo `Direction | undefined`, misma cola `'ltr'` en el consumidor. Abre la puerta tambien a `avatar` e `image`, que son el mismo caso. Fuera la lectura del DOM y fuera la copia byte-a-byte que `chart.svelte` tenia del mismo mecanismo —dos fuentes de verdad para un hecho, y ademas el observer vigilaba `<html dir>` mientras la lectura era del elemento, asi que un ancestro que volteara en caliente no se veia nunca—. ⚠️ EL HALLAZGO QUE SOLO SALE MIDIENDO: **estampar `dir` en un `<svg>` no hace nada.** El atributo lo mapea a `direction` la hoja UA de HTML, que no alcanza a los elementos SVG: medido en Chrome, un `<svg dir="ltr">` bajo un ancestro `dir="rtl"` sigue computando `rtl`. Yo lo habia puesto ahi y el comentario afirmaba que servia. Va en el envoltorio HTML (`<figure data-chart-figure>`, `<div data-chart-frame>`). Importa porque `text-anchor` es LOGICO: con el estampado en el nodo equivocado la matematica espeja y las etiquetas no —el fallo de §2 con otro disfraz—. MEDIDO en Chrome, heatmap: LTR `ene/feb/mar` en x=26/90/154, celdas 26‥858; RTL en 851/787/723, celdas 6‥838. Funnel: poligonos de 0‥226 a 193‥420, etiquetas de 249 a 171. El toggle del topbar sigue volteandolas, ahora por prefs y no por el DOM. COSTE ACEPTADO, verificado: envolver una chart en `<div dir="rtl">` sin pasar la prop ya no la voltea. Falla de forma CONSISTENTE —el estampado del envoltorio gana sobre el ancestro, asi que matematica y pintura siguen de acuerdo— y no con las etiquetas cruzando el grafico. §7 del contrato deja de ser una divergencia declarada y pasa a ser la tabla de los dos puntos de entrada. Su justificacion era ademas falsa en dos puntos: no era "el unico sitio del catalogo" (quedan 4 mas, uno vivo en `field-langs-switcher`), y decia que una chart no se podia voltear por subarbol cuando `direction` se HEREDA y si se podia. `check` 77 = linea base. `docs:check` 0/0. Tests de eidos + libs/plots: 424/425, el fallo es `audio-player` sin morfo, preexistente y ajeno. ⚠️ No pude ver los pixeles: el panel del navegador no compone frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`activeEidosDir` — see §7.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`activeDir` (`src/uix/soma/direction.ts`) returns
feat(direction): el flip — el atributo es la afirmacion, y undefined ACTUA La cadena se parte en la afirmacion. `activeDir` devuelve ahora SOLO lo que alguien eligio (prop -> contexto del ancestro) y ahi se detiene: eso es lo que el runtime estampa y lo que cruza portales. La preferencia de la app ya no viaja en el atributo de cada componente — llega a la pagina UNA vez, por la proyeccion automatica de P2, y todo lo demas HEREDA. Por que: con prefs dentro de la cadena por-componente, "nadie afirmo" era inalcanzable — el boot por defecto siempre tiene la dimension direction, asi que getDir() casi nunca era undefined y cada raiz del canon estampaba dir="ltr" en una pagina sin afirmaciones. El atributo nunca llegaba a estar AUSENTE, la herencia del DOM nunca llegaba a trabajar, y de ahi la clase entera de islas y reenvios que este eje llevo meses parcheando. La matematica conserva la preferencia como VALOR (no puede leer el DOM) en un solo helper: afirmacion -> soma.prefs.getDir() -> 'ltr' resolveDir() - activeDir(dir) pierde el parametro soma: 57 wrappers, y 49 quedan sin `const soma = Soma.get()` huerfano (limpiados con deteccion de identificador, no de ruta — la primera pasada se tropezaba con 'core/soma.svelte'). - 45 colas `opts.dir.current ?? 'ltr'` -> resolveDir(this.opts.dir, this.soma). - Los 4 getDirectionalKeys de submenus que leian opts.dir.current directo pasan a resolvedDir — con undefined degradaban a ltr EN SILENCIO bajo rtl ambiental. - command: 4.1 le habia dejado `dir: null` en el runtime y el estampado manual inline en el assert (la linea no estaba sola y el barrido no la vio). Ahora el runtime estampa y el inline se va. - listbox / navigation-menu: el alias assertedDir muere — tras el flip, opts.dir ES la afirmacion. - date-range-field-input / time-range-field-input: el reenvio dir= al Field compuesto en sitio sobra — DirectionContext lo lleva. - 7 wrappers renombran `resolvedDir` -> `assertedDir`: el nombre mentia. - picker / gradient-picker: activeDir(() => undefined) = la afirmacion del ancestro o nada — mejor que el prefs-siempre-concreto de antes. MEDIDO en Chrome, el modelo entero: auto UN solo [dir] en toda la pagina: el <html> de la proyeccion (antes lo llevaba casi cada raiz del canon) prefs rtl select sin atributo, computa rtl por herencia; el panel portalizado SIN atributo y computa rtl (heredo del html a traves del body) prop rtl trigger del dropdown estampa rtl, el panel portalizado estampa rtl (la afirmacion cruza), submenu data-side=left, cero islas dentro matematica slider SIN atributo con pagina rtl: click al 25% fisico da 75 y ArrowRight BAJA — resolveDir lee prefs sin que el atributo lo haga SSR curl de select y accordion: cero dir= en el payload El contrato: §1 documenta la cadena partida (activeDir -> afirmacion; resolveDir -> matematica), §6 la proyeccion automatica + la semilla y el parrafo de SSR — el <html dir> del primer render pertenece a app.html o al servidor (modelo react-aria). check 77 = linea base · suites soma+active-uix+prefs+contracts 1362 tests: los 6 de contracts y soma-attr-audit son el conjunto preexistente (A/B dos veces en fases previas; attr-audit pasa aislado) · media-player 17/17 tras hacer resolveDir defensivo con mocks sin prefs · rtl:check 1 (palabras) · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`Active<Direction | undefined>` — the raw assertion. The maths tail lives
per-provider:
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```ts
feat(direction): el flip — el atributo es la afirmacion, y undefined ACTUA La cadena se parte en la afirmacion. `activeDir` devuelve ahora SOLO lo que alguien eligio (prop -> contexto del ancestro) y ahi se detiene: eso es lo que el runtime estampa y lo que cruza portales. La preferencia de la app ya no viaja en el atributo de cada componente — llega a la pagina UNA vez, por la proyeccion automatica de P2, y todo lo demas HEREDA. Por que: con prefs dentro de la cadena por-componente, "nadie afirmo" era inalcanzable — el boot por defecto siempre tiene la dimension direction, asi que getDir() casi nunca era undefined y cada raiz del canon estampaba dir="ltr" en una pagina sin afirmaciones. El atributo nunca llegaba a estar AUSENTE, la herencia del DOM nunca llegaba a trabajar, y de ahi la clase entera de islas y reenvios que este eje llevo meses parcheando. La matematica conserva la preferencia como VALOR (no puede leer el DOM) en un solo helper: afirmacion -> soma.prefs.getDir() -> 'ltr' resolveDir() - activeDir(dir) pierde el parametro soma: 57 wrappers, y 49 quedan sin `const soma = Soma.get()` huerfano (limpiados con deteccion de identificador, no de ruta — la primera pasada se tropezaba con 'core/soma.svelte'). - 45 colas `opts.dir.current ?? 'ltr'` -> resolveDir(this.opts.dir, this.soma). - Los 4 getDirectionalKeys de submenus que leian opts.dir.current directo pasan a resolvedDir — con undefined degradaban a ltr EN SILENCIO bajo rtl ambiental. - command: 4.1 le habia dejado `dir: null` en el runtime y el estampado manual inline en el assert (la linea no estaba sola y el barrido no la vio). Ahora el runtime estampa y el inline se va. - listbox / navigation-menu: el alias assertedDir muere — tras el flip, opts.dir ES la afirmacion. - date-range-field-input / time-range-field-input: el reenvio dir= al Field compuesto en sitio sobra — DirectionContext lo lleva. - 7 wrappers renombran `resolvedDir` -> `assertedDir`: el nombre mentia. - picker / gradient-picker: activeDir(() => undefined) = la afirmacion del ancestro o nada — mejor que el prefs-siempre-concreto de antes. MEDIDO en Chrome, el modelo entero: auto UN solo [dir] en toda la pagina: el <html> de la proyeccion (antes lo llevaba casi cada raiz del canon) prefs rtl select sin atributo, computa rtl por herencia; el panel portalizado SIN atributo y computa rtl (heredo del html a traves del body) prop rtl trigger del dropdown estampa rtl, el panel portalizado estampa rtl (la afirmacion cruza), submenu data-side=left, cero islas dentro matematica slider SIN atributo con pagina rtl: click al 25% fisico da 75 y ArrowRight BAJA — resolveDir lee prefs sin que el atributo lo haga SSR curl de select y accordion: cero dir= en el payload El contrato: §1 documenta la cadena partida (activeDir -> afirmacion; resolveDir -> matematica), §6 la proyeccion automatica + la semilla y el parrafo de SSR — el <html dir> del primer render pertenece a app.html o al servidor (modelo react-aria). check 77 = linea base · suites soma+active-uix+prefs+contracts 1362 tests: los 6 de contracts y soma-attr-audit son el conjunto preexistente (A/B dos veces en fases previas; attr-audit pasa aislado) · media-player 17/17 tras hacer resolveDir defensivo con mocks sin prefs · rtl:check 1 (palabras) · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```
**Why the chain is split.** `undefined` means _nobody asserted a direction_ —
feat(direction): el eslabon de contexto — la composicion hereda por fisica La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica (2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs. Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del ancestro antes de caer a prefs. Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma, fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL), y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el opts.dir del dueno — que ahora incorpora el contexto. Con la fisica, las convenciones se borran: - los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content x3, color-field-format-select) — el contexto los hace - el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem - los 3 canarios dir= de media-player que la otra sesion dejo puestos a proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no llego al portal") El canario canta: su repro exacto (media-player, manual parts, direction rtl, float de subtitulos) da panel rtl SIN el dir= — y el volume float igual. Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a 0.417 del borde fisico); submenu de dropdown con data-side=left en RTL derivado del contexto. Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper — nunca en constructores de provider. Los tests de provider construyen directo y no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined) cae a traves de prefs. Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un default), sin ancestro cae a prefs, y la publicacion es REACTIVA. check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro eslabones y §1 "Composition carries the assertion" documenta la fisica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
neither the consumer via the prop, nor an ancestor via its own assertion, nor
the app via a registered `direction` preference. That is not the same fact as
`'ltr'`, and every `=== 'rtl'` test in the codebase erases the difference. So
the provider keeps both: `resolvedDir` for its own maths, and the raw
`opts.dir.current` for the DOM.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): el eslabon de contexto — la composicion hereda por fisica La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica (2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs. Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del ancestro antes de caer a prefs. Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma, fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL), y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el opts.dir del dueno — que ahora incorpora el contexto. Con la fisica, las convenciones se borran: - los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content x3, color-field-format-select) — el contexto los hace - el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem - los 3 canarios dir= de media-player que la otra sesion dejo puestos a proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no llego al portal") El canario canta: su repro exacto (media-player, manual parts, direction rtl, float de subtitulos) da panel rtl SIN el dir= — y el volume float igual. Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a 0.417 del borde fisico); submenu de dropdown con data-side=left en RTL derivado del contexto. Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper — nunca en constructores de provider. Los tests de provider construyen directo y no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined) cae a traves de prefs. Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un default), sin ancestro cae a prefs, y la publicacion es REACTIVA. check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro eslabones y §1 "Composition carries the assertion" documenta la fisica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### Composition carries the assertion — by physics
feat(direction): la propagacion pertenece a la capa flotante Un panel portalizado se renderiza FUERA del subarbol de su dueño, asi que la herencia no lo alcanza: la afirmacion del dueño solo llega si alguien la lleva. Eso estaba delegado en cada consumidor — siete envoltorios Content componian el enlace al padre a mano — y quien COMPONE un popover no tenia por donde hacerlo. Medido con <DatePicker dir="rtl"> sobre una pagina LTR: el wrapper flotante estampaba dir="ltr", el picker-shell y su pie se disponian en LTR, y solo el calendario espejaba. Panel partido por la mitad, el fallo del §2 del contrato. La capa lo compone ahora una sola vez: el dir propio de la superficie -> el dir afirmado por el proveedor DUEÑO -> omitir - FloatingProviderOpts.dir el dueño baja su afirmacion (ya pasada por activeDir, o sea prop -> prefs) - FloatingContent.assertedDir donde se encuentran; lo leen los DOS estampados del wrapper (rama nativa y rama JS) - createFloatingShellRoot lo reenvia; es la puerta de 8 de los 10 FloatingProvider. Los 2 submenus toman el del menu padre. FloatingContentOpts.dir pasa a ser la prop CRUDA del Content. Resolverla ahi la haria casi siempre concreta (prefs) y el dueño no ganaria nunca el fallback. Se borran los 7 enlaces compuestos a mano con su soma / activeDir / parentRoot. Sobrevive uno a proposito: dropdown-menu-sub-content lo necesita para effectiveSide, que es matematica y quiere valor resuelto. popover, tooltip y link-preview aceptan dir en su raiz. No estampan nada — no renderizan elemento —; el valor existe para cruzar el portal, y es lo que permite que los 9 componentes que componen un popover empujen su direccion al panel. Borrados 9 resolvedDir muertos en los proveedores Content: nadie los leia (toda la matematica direccional consume el de la RAIZ) y el cambio de semantica de opts.dir dejaba su docblock mintiendo. Demos: las 7 que aceptan dir lo pasan ahora a la instancia — time-picker, time-range-picker y color-picker no tenian control ninguno; emoji-picker y natural-time-picker lo tenian solo en el arnes. Sin eso el camino de la PROP, el unico que ejercita el portal, no era observable. Verificado en Chrome real, direccion movida por el control de la demo: date-picker Clear/Cancel/Close 15/102/209 -> 200/92/15 espejo exacto date-range-picker 448/217/17 sin regresion dropdown-menu (raiz y submenu, data-side=left en RTL), context-menu, menubar, select, sidebar (flyout), y tooltip en auto sigue estampando ltr — la cola de prefs sobrevive check 78 = linea base (A/B con stash) · 158/158 en los 20 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/565. Queda documentado en el handoff un defecto PREEXISTENTE de otra familia que esto vuelve visible: un componente eidos que monta otro componente del canon en SITIO no le reenvia el dir, y el hijo estampa ltr cortando la herencia del panel (natural-time-picker/Slider, emoji-picker/Command+ToggleGroup, color-field/Select). No hay precedente de ese reenvio en eidos: es doctrina nueva y movimiento propio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): la composicion lleva la afirmacion — tambien en sitio 4.2 cerro el caso del portal. Faltaba el gemelo: un componente que MONTA otro componente del canon arranca dentro de si una segunda cadena. Sin la afirmacion del dueño esa cadena salta la prop, aterriza en prefs y estampa un `dir` propio — y un `dir` estampado CORTA la herencia de todo lo que cuelga. El padre afirma rtl, el hijo estampa ltr, y el subarbol se parte. Medido con las demos ya cableadas, escaneando cada panel en RTL en busca de elementos que estampen una direccion distinta a la del envoltorio flotante: natural-time-picker 1 isla [data-slider] emoji-picker 3 islas [data-command] + [data-toggle-group] x2 color-field 1 isla [data-color-field-format-select] (un Select) Preexistente: mientras el panel entero era LTR la isla no se veia. Lo que hizo 4.2 fue volverla visible. El canon pasa a enunciar UNA regla con DOS vehiculos, elegidos por geometria y no por gusto (direction-contract.md §1, "Composition carries the assertion"): a traves de un portal -> la capa flotante, una vez por overlay composicion en sitio -> la prop `dir`, en el punto de llamada Lo que se entrega es siempre la afirmacion CRUDA (`opts.dir.current`), nunca `resolvedDir`: asi "nadie afirmo" sigue siendo distinguible y la cola de prefs del hijo sigue corriendo. Verificado en Chrome real, pagina en LTR y direccion movida por el control de la demo: los tres paneles pasan a 0 islas y el area del color-picker computa por fin `rtl`. Los dos sliders de canal siguen en espejo exacto — hue 210 cae a 0.583 del borde izquierdo en LTR y a 0.417 en RTL, alpha al maximo pasa de 1.0 a 0.0 — o sea §9.12 no se ha movido. check 78 = linea base · 59/59 en los 8 ambitos · rtl:check 1 (palabras) · docs:check 0/565. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The chain is per-component, so a component that MOUNTS another canon component
feat(direction): el eslabon de contexto — la composicion hereda por fisica La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica (2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs. Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del ancestro antes de caer a prefs. Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma, fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL), y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el opts.dir del dueno — que ahora incorpora el contexto. Con la fisica, las convenciones se borran: - los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content x3, color-field-format-select) — el contexto los hace - el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem - los 3 canarios dir= de media-player que la otra sesion dejo puestos a proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no llego al portal") El canario canta: su repro exacto (media-player, manual parts, direction rtl, float de subtitulos) da panel rtl SIN el dir= — y el volume float igual. Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a 0.417 del borde fisico); submenu de dropdown con data-side=left en RTL derivado del contexto. Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper — nunca en constructores de provider. Los tests de provider construyen directo y no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined) cae a traves de prefs. Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un default), sin ancestro cae a prefs, y la publicacion es REACTIVA. check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro eslabones y §1 "Composition carries the assertion" documenta la fisica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
starts a second chain inside the first. Before the context link that second
chain skipped the prop, landed on prefs, and stamped a value of its own — and a
stamped `dir` **cuts inheritance for everything below it**: the parent asserted
`rtl`, the child stamped `ltr`, the subtree split. The framework carried the
assertion by convention (a "forward `dir=` at the call site" rule with
hand-written forwards) until 2026-08-05; the convention kept being missed in
the field, which is why it is now the resolver's job.
**In place** nothing needs writing: the child's `activeDir` consults
`DirectionContext` before prefs, so it resolves — and stamps — the ancestor's
assertion. A submenu inheriting from its parent menu is the same mechanism,
not a special case.
**Through a portal** the panel is rendered outside its owner's subtree, so DOM
inheritance cannot reach it; the Svelte context still does (the Portal forwards
contexts), and `soma/layers/floating` additionally composes the owner link once
for every overlay:
feat(direction): la propagacion pertenece a la capa flotante Un panel portalizado se renderiza FUERA del subarbol de su dueño, asi que la herencia no lo alcanza: la afirmacion del dueño solo llega si alguien la lleva. Eso estaba delegado en cada consumidor — siete envoltorios Content componian el enlace al padre a mano — y quien COMPONE un popover no tenia por donde hacerlo. Medido con <DatePicker dir="rtl"> sobre una pagina LTR: el wrapper flotante estampaba dir="ltr", el picker-shell y su pie se disponian en LTR, y solo el calendario espejaba. Panel partido por la mitad, el fallo del §2 del contrato. La capa lo compone ahora una sola vez: el dir propio de la superficie -> el dir afirmado por el proveedor DUEÑO -> omitir - FloatingProviderOpts.dir el dueño baja su afirmacion (ya pasada por activeDir, o sea prop -> prefs) - FloatingContent.assertedDir donde se encuentran; lo leen los DOS estampados del wrapper (rama nativa y rama JS) - createFloatingShellRoot lo reenvia; es la puerta de 8 de los 10 FloatingProvider. Los 2 submenus toman el del menu padre. FloatingContentOpts.dir pasa a ser la prop CRUDA del Content. Resolverla ahi la haria casi siempre concreta (prefs) y el dueño no ganaria nunca el fallback. Se borran los 7 enlaces compuestos a mano con su soma / activeDir / parentRoot. Sobrevive uno a proposito: dropdown-menu-sub-content lo necesita para effectiveSide, que es matematica y quiere valor resuelto. popover, tooltip y link-preview aceptan dir en su raiz. No estampan nada — no renderizan elemento —; el valor existe para cruzar el portal, y es lo que permite que los 9 componentes que componen un popover empujen su direccion al panel. Borrados 9 resolvedDir muertos en los proveedores Content: nadie los leia (toda la matematica direccional consume el de la RAIZ) y el cambio de semantica de opts.dir dejaba su docblock mintiendo. Demos: las 7 que aceptan dir lo pasan ahora a la instancia — time-picker, time-range-picker y color-picker no tenian control ninguno; emoji-picker y natural-time-picker lo tenian solo en el arnes. Sin eso el camino de la PROP, el unico que ejercita el portal, no era observable. Verificado en Chrome real, direccion movida por el control de la demo: date-picker Clear/Cancel/Close 15/102/209 -> 200/92/15 espejo exacto date-range-picker 448/217/17 sin regresion dropdown-menu (raiz y submenu, data-side=left en RTL), context-menu, menubar, select, sidebar (flyout), y tooltip en auto sigue estampando ltr — la cola de prefs sobrevive check 78 = linea base (A/B con stash) · 158/158 en los 20 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/565. Queda documentado en el handoff un defecto PREEXISTENTE de otra familia que esto vuelve visible: un componente eidos que monta otro componente del canon en SITIO no le reenvia el dir, y el hijo estampa ltr cortando la herencia del panel (natural-time-picker/Slider, emoji-picker/Command+ToggleGroup, color-field/Select). No hay precedente de ese reenvio en eidos: es doctrina nueva y movimiento propio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```text
the surface's own dir prop → the owning provider's asserted dir → omit
```
feat(direction): el eslabon de contexto — la composicion hereda por fisica La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica (2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs. Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del ancestro antes de caer a prefs. Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma, fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL), y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el opts.dir del dueno — que ahora incorpora el contexto. Con la fisica, las convenciones se borran: - los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content x3, color-field-format-select) — el contexto los hace - el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem - los 3 canarios dir= de media-player que la otra sesion dejo puestos a proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no llego al portal") El canario canta: su repro exacto (media-player, manual parts, direction rtl, float de subtitulos) da panel rtl SIN el dir= — y el volume float igual. Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a 0.417 del borde fisico); submenu de dropdown con data-side=left en RTL derivado del contexto. Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper — nunca en constructores de provider. Los tests de provider construyen directo y no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined) cae a traves de prefs. Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un default), sin ancestro cae a prefs, y la publicacion es REACTIVA. check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro eslabones y §1 "Composition carries the assertion" documenta la fisica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`FloatingProviderOpts.dir` is where the owner hands its assertion down;
`FloatingContent.assertedDir` is where the two meet. The consequence for anyone
writing an overlay:
feat(direction): la propagacion pertenece a la capa flotante Un panel portalizado se renderiza FUERA del subarbol de su dueño, asi que la herencia no lo alcanza: la afirmacion del dueño solo llega si alguien la lleva. Eso estaba delegado en cada consumidor — siete envoltorios Content componian el enlace al padre a mano — y quien COMPONE un popover no tenia por donde hacerlo. Medido con <DatePicker dir="rtl"> sobre una pagina LTR: el wrapper flotante estampaba dir="ltr", el picker-shell y su pie se disponian en LTR, y solo el calendario espejaba. Panel partido por la mitad, el fallo del §2 del contrato. La capa lo compone ahora una sola vez: el dir propio de la superficie -> el dir afirmado por el proveedor DUEÑO -> omitir - FloatingProviderOpts.dir el dueño baja su afirmacion (ya pasada por activeDir, o sea prop -> prefs) - FloatingContent.assertedDir donde se encuentran; lo leen los DOS estampados del wrapper (rama nativa y rama JS) - createFloatingShellRoot lo reenvia; es la puerta de 8 de los 10 FloatingProvider. Los 2 submenus toman el del menu padre. FloatingContentOpts.dir pasa a ser la prop CRUDA del Content. Resolverla ahi la haria casi siempre concreta (prefs) y el dueño no ganaria nunca el fallback. Se borran los 7 enlaces compuestos a mano con su soma / activeDir / parentRoot. Sobrevive uno a proposito: dropdown-menu-sub-content lo necesita para effectiveSide, que es matematica y quiere valor resuelto. popover, tooltip y link-preview aceptan dir en su raiz. No estampan nada — no renderizan elemento —; el valor existe para cruzar el portal, y es lo que permite que los 9 componentes que componen un popover empujen su direccion al panel. Borrados 9 resolvedDir muertos en los proveedores Content: nadie los leia (toda la matematica direccional consume el de la RAIZ) y el cambio de semantica de opts.dir dejaba su docblock mintiendo. Demos: las 7 que aceptan dir lo pasan ahora a la instancia — time-picker, time-range-picker y color-picker no tenian control ninguno; emoji-picker y natural-time-picker lo tenian solo en el arnes. Sin eso el camino de la PROP, el unico que ejercita el portal, no era observable. Verificado en Chrome real, direccion movida por el control de la demo: date-picker Clear/Cancel/Close 15/102/209 -> 200/92/15 espejo exacto date-range-picker 448/217/17 sin regresion dropdown-menu (raiz y submenu, data-side=left en RTL), context-menu, menubar, select, sidebar (flyout), y tooltip en auto sigue estampando ltr — la cola de prefs sobrevive check 78 = linea base (A/B con stash) · 158/158 en los 20 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/565. Queda documentado en el handoff un defecto PREEXISTENTE de otra familia que esto vuelve visible: un componente eidos que monta otro componente del canon en SITIO no le reenvia el dir, y el hijo estampa ltr cortando la herencia del panel (natural-time-picker/Slider, emoji-picker/Command+ToggleGroup, color-field/Select). No hay precedente de ese reenvio en eidos: es doctrina nueva y movimiento propio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> A `Content` wrapper passes its `dir` prop **raw** — `readableActive(() => dir)`,
> never `activeDir`. Running the chain there makes the value always concrete, and
> the owner's assertion could then never win the fallback.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### The public prop
Every component that has any direction-dependent behaviour or paint declares:
```ts
dir?: Direction;
```
`Direction` is the alias, never the union written by hand. Writing
`dir?: 'ltr' | 'rtl'` compiles identically and leaves the contract outside the
alias — widening or renaming `Direction` would not reach it, and the drift would
not break at type level, which is the whole point of having the alias.
> ⚠️ `OptsFromProps` **strips the `undefined`** from an optional public prop by
> default (`Exclude<P[K], undefined>`), which would destroy exactly the
> distinction §1 is built on. A component using that helper lists `dir` in the
> `Preserve` parameter — `OptsFromProps<P, Managed, StateKey, 'dir'>` — so
> `Active<Direction | undefined>` survives, and the wrapper passes the box
> straight through the bag: `dir: activeDir(() => dir)` is a valid
> `bindProps` entry (Active pass-through), keeping the call — and the context
> it publishes — visible in the wrapper init.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## 2. The two attributes
| Attribute | Value | Present when | Read by |
| ---------- | -------------------------- | ------------------------------- | --------------------------------------- |
| `dir` | **raw** `opts.dir.current` | someone asserted a direction | the browser (`:dir()`, bidi, UA sheet) |
| `data-dir` | **resolved** `resolvedDir` | the component chose to stamp it | recipes that need an unconditional hook |
`dir` is a native HTML attribute with real platform semantics: it drives the
bidi algorithm, isolates its subtree, and is what `:dir()` matches against. It
is stamped **raw**. Stamping a defaulted `'ltr'` is what makes a component force
its own subtree back to LTR inside an RTL page; an omitted attribute inherits,
for free, with no DOM read.
`data-dir` is the component's own attribute, carrying the resolved value — so
where a component stamps it, it is always present, unlike `dir`. It exists for
the case where a recipe needs a selector that always matches: a glyph that must
be flipped to agree with maths the component already did. Using it is not a
violation of §3; it is a different attribute answering a different question.
**But it is opt-in, not routine.** The acceptance rule that visual props map to
`data-{prop}` does not apply here — `dir` is native, `:dir()` cannot see
`data-dir`, and a component that stamps `data-dir` without a recipe rule reading
it has only enlarged the DOM. Stamp it when a recipe needs it, and not before.
### When stamping is mandatory
fix(direction): la regla de estampado era demasiado estrecha — 16 componentes la incumplian Decision del usuario (opcion B). La regla de §2 decia «estampa si el recipe usa `:dir()`», y esa premisa es FALSA: `:dir()` no es el unico lector. Tambien leen la direccion computada del elemento las propiedades logicas (`margin-inline-*`, `text-align: start`) y —lo que lo hace casi universal— `flex-direction: row`, que se INVIERTE. En la practica, cualquier componente con maquetacion horizontal depende de la direccion. La regla honesta, ya escrita: quien acepta `dir` lo estampa. La pregunta no es SI, es EN QUE ELEMENTO — el que lleva la pintura. Y para eso hay cuatro casos, ahora documentados: raiz normal · superficie PORTALIZADA (el wrapper flotante, que la capa ya estampa porque fuera del subarbol no puede heredar) · componente con LAS DOS mitades, que necesita las dos · y geometria aplicada desde JS, que no necesita atributo. Censo: de 55 que aceptan `dir`, 20 no estampaban. Clasificados uno a uno leyendo recipe + wrapper + donde renderiza cada parte: - 16 ESTAMPAN ahora, cada uno en la parte que lleva la pintura. Los sub-partes (drawer→Content, float-panel→Content, dropdown-menu→Trigger porque su raiz no renderiza elemento) llevan comentario nombrando la pintura que cubren. - 4 NO estampan y es CORRECTO: popover, tooltip, link-preview y context-menu. Su unica pintura direccional es el panel portalizado, que ya estampa la capa flotante; y el root de context-menu no renderiza elemento ninguno. DOS DEFECTOS QUE EL BARRIDO DESTAPO, uno de ellos MIO: 1. `sidebar` pasaba el valor RESUELTO al motor flotante. Esa linea la escribi yo en §9.19: arregle que se saltara la prop y deje el otro defecto, que siempre es concreto. La capa estampa lo que recibe, asi que un flyout SIN afirmacion recibia `dir="ltr"` — forzando su subarbol a LTR dentro de una pagina RTL, que es exactamente lo que el crudo existe para impedir. Era el UNICO de once consumidores flotantes que lo hacia. De paso desaparece su `resolvedDir`: sidebar no hace matematica direccional, toda su pintura es CSS. 2. La afirmacion de la raiz NO LLEGABA al panel portalizado. `select`, `combobox` y `context-menu` resolvian su Content con `activeDir(() => dir, soma)`, sin paso por el padre — asi que `<Select dir="rtl">` movia el trigger y dejaba la lista atras. Antes del barrido no se notaba porque NINGUNA de las dos mitades estaba afirmada y coincidian por heredar las dos del `<html>`; estampar el trigger lo habria hecho VISIBLE. Compuesto el enlace en el punto de llamada, que es la forma que §1 imprime y que `dropdown-menu` y `menubar` ya usaban. El resolutor sigue sin paso por el padre. Y `color-picker`, que es el mismo caso con una regla `:dir()` REAL varada: el area mirroring vive en el panel portalizado (`color-picker.css:298,335`) mientras la matematica del thumb y el arrastre salen de la raiz. Sin el enlace, el thumb se voltea y el gradiente de debajo no — justo lo que avisa el comentario de la receta. Su wrapper ya reestampaba `data-color` por esta misma razon; le faltaba `dir`. MEDIDO en Chrome (select): la raiz pasa de NO tener atributo a estamparlo, y trigger y panel coinciden en las tres posiciones del toggle. Estructuralmente ya no pueden divergir: ambos leen `opts.dir` de la misma raiz. `check` sin errores en ninguno de los 20 (el total 80 son 77 de base mas 3 de `media-player`, que edita otra sesion). 90 tests de los componentes tocados en verde. `rtl:check` 1, el de `palabras`. `docs:check` 0/0. QUEDA, reportado y sin tocar: ningun picker de eidos reenvia `dir` a su `PopoverContent`, asi que el cromo del `picker-shell` dentro del portal (`picker-shell.css:78`, el split del pie) queda sin cubrir en los siete. Los controles interiores (Calendar, los Slider) se salvan porque reciben el crudo por su cuenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **A component that accepts `dir` MUST stamp it on the element that carries the
> direction-dependent paint.**
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
fix(direction): la regla de estampado era demasiado estrecha — 16 componentes la incumplian Decision del usuario (opcion B). La regla de §2 decia «estampa si el recipe usa `:dir()`», y esa premisa es FALSA: `:dir()` no es el unico lector. Tambien leen la direccion computada del elemento las propiedades logicas (`margin-inline-*`, `text-align: start`) y —lo que lo hace casi universal— `flex-direction: row`, que se INVIERTE. En la practica, cualquier componente con maquetacion horizontal depende de la direccion. La regla honesta, ya escrita: quien acepta `dir` lo estampa. La pregunta no es SI, es EN QUE ELEMENTO — el que lleva la pintura. Y para eso hay cuatro casos, ahora documentados: raiz normal · superficie PORTALIZADA (el wrapper flotante, que la capa ya estampa porque fuera del subarbol no puede heredar) · componente con LAS DOS mitades, que necesita las dos · y geometria aplicada desde JS, que no necesita atributo. Censo: de 55 que aceptan `dir`, 20 no estampaban. Clasificados uno a uno leyendo recipe + wrapper + donde renderiza cada parte: - 16 ESTAMPAN ahora, cada uno en la parte que lleva la pintura. Los sub-partes (drawer→Content, float-panel→Content, dropdown-menu→Trigger porque su raiz no renderiza elemento) llevan comentario nombrando la pintura que cubren. - 4 NO estampan y es CORRECTO: popover, tooltip, link-preview y context-menu. Su unica pintura direccional es el panel portalizado, que ya estampa la capa flotante; y el root de context-menu no renderiza elemento ninguno. DOS DEFECTOS QUE EL BARRIDO DESTAPO, uno de ellos MIO: 1. `sidebar` pasaba el valor RESUELTO al motor flotante. Esa linea la escribi yo en §9.19: arregle que se saltara la prop y deje el otro defecto, que siempre es concreto. La capa estampa lo que recibe, asi que un flyout SIN afirmacion recibia `dir="ltr"` — forzando su subarbol a LTR dentro de una pagina RTL, que es exactamente lo que el crudo existe para impedir. Era el UNICO de once consumidores flotantes que lo hacia. De paso desaparece su `resolvedDir`: sidebar no hace matematica direccional, toda su pintura es CSS. 2. La afirmacion de la raiz NO LLEGABA al panel portalizado. `select`, `combobox` y `context-menu` resolvian su Content con `activeDir(() => dir, soma)`, sin paso por el padre — asi que `<Select dir="rtl">` movia el trigger y dejaba la lista atras. Antes del barrido no se notaba porque NINGUNA de las dos mitades estaba afirmada y coincidian por heredar las dos del `<html>`; estampar el trigger lo habria hecho VISIBLE. Compuesto el enlace en el punto de llamada, que es la forma que §1 imprime y que `dropdown-menu` y `menubar` ya usaban. El resolutor sigue sin paso por el padre. Y `color-picker`, que es el mismo caso con una regla `:dir()` REAL varada: el area mirroring vive en el panel portalizado (`color-picker.css:298,335`) mientras la matematica del thumb y el arrastre salen de la raiz. Sin el enlace, el thumb se voltea y el gradiente de debajo no — justo lo que avisa el comentario de la receta. Su wrapper ya reestampaba `data-color` por esta misma razon; le faltaba `dir`. MEDIDO en Chrome (select): la raiz pasa de NO tener atributo a estamparlo, y trigger y panel coinciden en las tres posiciones del toggle. Estructuralmente ya no pueden divergir: ambos leen `opts.dir` de la misma raiz. `check` sin errores en ninguno de los 20 (el total 80 son 77 de base mas 3 de `media-player`, que edita otra sesion). 90 tests de los componentes tocados en verde. `rtl:check` 1, el de `palabras`. `docs:check` 0/0. QUEDA, reportado y sin tocar: ningun picker de eidos reenvia `dir` a su `PopoverContent`, asi que el cromo del `picker-shell` dentro del portal (`picker-shell.css:78`, el split del pie) queda sin cubrir en los siete. Los controles interiores (Calendar, los Slider) se salvan porque reciben el crudo por su cuenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The attribute is the ONLY way the paint learns what the prop asserted. Miss it
and the component is split in half: the prop moves the **maths** and leaves the
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**paint** behind. Consumer asserts `dir="rtl"` on an LTR page, the arrow keys
flip, and nothing mirrors.
fix(direction): la regla de estampado era demasiado estrecha — 16 componentes la incumplian Decision del usuario (opcion B). La regla de §2 decia «estampa si el recipe usa `:dir()`», y esa premisa es FALSA: `:dir()` no es el unico lector. Tambien leen la direccion computada del elemento las propiedades logicas (`margin-inline-*`, `text-align: start`) y —lo que lo hace casi universal— `flex-direction: row`, que se INVIERTE. En la practica, cualquier componente con maquetacion horizontal depende de la direccion. La regla honesta, ya escrita: quien acepta `dir` lo estampa. La pregunta no es SI, es EN QUE ELEMENTO — el que lleva la pintura. Y para eso hay cuatro casos, ahora documentados: raiz normal · superficie PORTALIZADA (el wrapper flotante, que la capa ya estampa porque fuera del subarbol no puede heredar) · componente con LAS DOS mitades, que necesita las dos · y geometria aplicada desde JS, que no necesita atributo. Censo: de 55 que aceptan `dir`, 20 no estampaban. Clasificados uno a uno leyendo recipe + wrapper + donde renderiza cada parte: - 16 ESTAMPAN ahora, cada uno en la parte que lleva la pintura. Los sub-partes (drawer→Content, float-panel→Content, dropdown-menu→Trigger porque su raiz no renderiza elemento) llevan comentario nombrando la pintura que cubren. - 4 NO estampan y es CORRECTO: popover, tooltip, link-preview y context-menu. Su unica pintura direccional es el panel portalizado, que ya estampa la capa flotante; y el root de context-menu no renderiza elemento ninguno. DOS DEFECTOS QUE EL BARRIDO DESTAPO, uno de ellos MIO: 1. `sidebar` pasaba el valor RESUELTO al motor flotante. Esa linea la escribi yo en §9.19: arregle que se saltara la prop y deje el otro defecto, que siempre es concreto. La capa estampa lo que recibe, asi que un flyout SIN afirmacion recibia `dir="ltr"` — forzando su subarbol a LTR dentro de una pagina RTL, que es exactamente lo que el crudo existe para impedir. Era el UNICO de once consumidores flotantes que lo hacia. De paso desaparece su `resolvedDir`: sidebar no hace matematica direccional, toda su pintura es CSS. 2. La afirmacion de la raiz NO LLEGABA al panel portalizado. `select`, `combobox` y `context-menu` resolvian su Content con `activeDir(() => dir, soma)`, sin paso por el padre — asi que `<Select dir="rtl">` movia el trigger y dejaba la lista atras. Antes del barrido no se notaba porque NINGUNA de las dos mitades estaba afirmada y coincidian por heredar las dos del `<html>`; estampar el trigger lo habria hecho VISIBLE. Compuesto el enlace en el punto de llamada, que es la forma que §1 imprime y que `dropdown-menu` y `menubar` ya usaban. El resolutor sigue sin paso por el padre. Y `color-picker`, que es el mismo caso con una regla `:dir()` REAL varada: el area mirroring vive en el panel portalizado (`color-picker.css:298,335`) mientras la matematica del thumb y el arrastre salen de la raiz. Sin el enlace, el thumb se voltea y el gradiente de debajo no — justo lo que avisa el comentario de la receta. Su wrapper ya reestampaba `data-color` por esta misma razon; le faltaba `dir`. MEDIDO en Chrome (select): la raiz pasa de NO tener atributo a estamparlo, y trigger y panel coinciden en las tres posiciones del toggle. Estructuralmente ya no pueden divergir: ambos leen `opts.dir` de la misma raiz. `check` sin errores en ninguno de los 20 (el total 80 son 77 de base mas 3 de `media-player`, que edita otra sesion). 90 tests de los componentes tocados en verde. `rtl:check` 1, el de `palabras`. `docs:check` 0/0. QUEDA, reportado y sin tocar: ningun picker de eidos reenvia `dir` a su `PopoverContent`, asi que el cromo del `picker-shell` dentro del portal (`picker-shell.css:78`, el split del pie) queda sin cubrir en los siete. Los controles interiores (Calendar, los Slider) se salvan porque reciben el crudo por su cuenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`:dir()` is not the only reader, and reading it as the trigger for this rule is
too narrow. Everything below resolves against the element's own computed
direction:
- **`:dir()`** in the recipe;
- **logical properties** — `margin-inline-start`, `inset-inline-end`,
`text-align: start`, and `flex-direction: row`, which reverses;
- **SVG `text-anchor`**, which is logical (§7).
Which is to say: in practice a component with any horizontal layout at all is
direction-dependent, and the honest reading of the rule is that accepting `dir`
implies stamping it.
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### The morfo declares it, the type demands the wire, the runtime stamps it
fix(direction): la regla de estampado era demasiado estrecha — 16 componentes la incumplian Decision del usuario (opcion B). La regla de §2 decia «estampa si el recipe usa `:dir()`», y esa premisa es FALSA: `:dir()` no es el unico lector. Tambien leen la direccion computada del elemento las propiedades logicas (`margin-inline-*`, `text-align: start`) y —lo que lo hace casi universal— `flex-direction: row`, que se INVIERTE. En la practica, cualquier componente con maquetacion horizontal depende de la direccion. La regla honesta, ya escrita: quien acepta `dir` lo estampa. La pregunta no es SI, es EN QUE ELEMENTO — el que lleva la pintura. Y para eso hay cuatro casos, ahora documentados: raiz normal · superficie PORTALIZADA (el wrapper flotante, que la capa ya estampa porque fuera del subarbol no puede heredar) · componente con LAS DOS mitades, que necesita las dos · y geometria aplicada desde JS, que no necesita atributo. Censo: de 55 que aceptan `dir`, 20 no estampaban. Clasificados uno a uno leyendo recipe + wrapper + donde renderiza cada parte: - 16 ESTAMPAN ahora, cada uno en la parte que lleva la pintura. Los sub-partes (drawer→Content, float-panel→Content, dropdown-menu→Trigger porque su raiz no renderiza elemento) llevan comentario nombrando la pintura que cubren. - 4 NO estampan y es CORRECTO: popover, tooltip, link-preview y context-menu. Su unica pintura direccional es el panel portalizado, que ya estampa la capa flotante; y el root de context-menu no renderiza elemento ninguno. DOS DEFECTOS QUE EL BARRIDO DESTAPO, uno de ellos MIO: 1. `sidebar` pasaba el valor RESUELTO al motor flotante. Esa linea la escribi yo en §9.19: arregle que se saltara la prop y deje el otro defecto, que siempre es concreto. La capa estampa lo que recibe, asi que un flyout SIN afirmacion recibia `dir="ltr"` — forzando su subarbol a LTR dentro de una pagina RTL, que es exactamente lo que el crudo existe para impedir. Era el UNICO de once consumidores flotantes que lo hacia. De paso desaparece su `resolvedDir`: sidebar no hace matematica direccional, toda su pintura es CSS. 2. La afirmacion de la raiz NO LLEGABA al panel portalizado. `select`, `combobox` y `context-menu` resolvian su Content con `activeDir(() => dir, soma)`, sin paso por el padre — asi que `<Select dir="rtl">` movia el trigger y dejaba la lista atras. Antes del barrido no se notaba porque NINGUNA de las dos mitades estaba afirmada y coincidian por heredar las dos del `<html>`; estampar el trigger lo habria hecho VISIBLE. Compuesto el enlace en el punto de llamada, que es la forma que §1 imprime y que `dropdown-menu` y `menubar` ya usaban. El resolutor sigue sin paso por el padre. Y `color-picker`, que es el mismo caso con una regla `:dir()` REAL varada: el area mirroring vive en el panel portalizado (`color-picker.css:298,335`) mientras la matematica del thumb y el arrastre salen de la raiz. Sin el enlace, el thumb se voltea y el gradiente de debajo no — justo lo que avisa el comentario de la receta. Su wrapper ya reestampaba `data-color` por esta misma razon; le faltaba `dir`. MEDIDO en Chrome (select): la raiz pasa de NO tener atributo a estamparlo, y trigger y panel coinciden en las tres posiciones del toggle. Estructuralmente ya no pueden divergir: ambos leen `opts.dir` de la misma raiz. `check` sin errores en ninguno de los 20 (el total 80 son 77 de base mas 3 de `media-player`, que edita otra sesion). 90 tests de los componentes tocados en verde. `rtl:check` 1, el de `palabras`. `docs:check` 0/0. QUEDA, reportado y sin tocar: ningun picker de eidos reenvia `dir` a su `PopoverContent`, asi que el cromo del `picker-shell` dentro del portal (`picker-shell.css:78`, el split del pie) queda sin cubrir en los siete. Los controles interiores (Calendar, los Slider) se salvan porque reciben el crudo por su cuenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion que hay que repetir 49 veces y que un tercio del catalogo incumple no es una convencion — es una abstraccion que falta. VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B (un AttrPlan del compilador de morfo) con el argumento de que A deja `dir` invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con `:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que le achacaba a A no existe. Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que expanden: renderProps() 21 un AttrPlan les llegaria runtimePart.props 27 NO les llegaria ninguna 1 O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a `renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se inyectan ahi sin declaracion en morfo. EL GATE ES DE TIPOS, que es el premio de verdad: dir: SomaRuntimeDir | null // OBLIGATORIO en SomaRuntimeSources `null` es la unica salida y dice algo cierto: este runtime no estampa direccion. Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que alguien abre el componente en RTL. `dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza elemento), drawer y float-panel ['content'] (pintan en el portal), number-field y css-field ['provider','input']. VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la demo: tabs auto -> ltr prop rtl -> rtl vuelta a auto -> ltr dropdown-menu el TRIGGER pasa a rtl y el panel portalizado tambien number-field raiz e input, los dos a rtl drawer el content portalizado a rtl por la cadena de prefs accordion sigue la cadena y vuelve, sin una sola linea en su proveedor check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado: uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el otro es que `media-player` ya no puede compilar sin el campo (su unica linea tocada, +1/-1, es mecanica). Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga). ⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco tests de "exposes root props". Un barrido con dos formas sintacticas necesita las dos en el mapa, no una. Estampados a mano restantes en el catalogo: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The rule above used to be a convention: one hand-written line per provider, 49
copies of it, and 20 of 55 components that had simply forgotten. A convention
repeated 49 times that a third of the catalogue breaks is not a convention — it
is a missing abstraction.
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
So the mechanism lives where contracts live. The MORFO declares that the
component has a direction (and which parts carry it); the requirement is
COMPUTED from that declaration through `soma.runtime<M>`; the runtime writes
the attribute:
feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion que hay que repetir 49 veces y que un tercio del catalogo incumple no es una convencion — es una abstraccion que falta. VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B (un AttrPlan del compilador de morfo) con el argumento de que A deja `dir` invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con `:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que le achacaba a A no existe. Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que expanden: renderProps() 21 un AttrPlan les llegaria runtimePart.props 27 NO les llegaria ninguna 1 O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a `renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se inyectan ahi sin declaracion en morfo. EL GATE ES DE TIPOS, que es el premio de verdad: dir: SomaRuntimeDir | null // OBLIGATORIO en SomaRuntimeSources `null` es la unica salida y dice algo cierto: este runtime no estampa direccion. Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que alguien abre el componente en RTL. `dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza elemento), drawer y float-panel ['content'] (pintan en el portal), number-field y css-field ['provider','input']. VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la demo: tabs auto -> ltr prop rtl -> rtl vuelta a auto -> ltr dropdown-menu el TRIGGER pasa a rtl y el panel portalizado tambien number-field raiz e input, los dos a rtl drawer el content portalizado a rtl por la cadena de prefs accordion sigue la cadena y vuelve, sin una sola linea en su proveedor check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado: uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el otro es que `media-player` ya no puede compilar sin el campo (su unica linea tocada, +1/-1, es mecanica). Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga). ⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco tests de "exposes root props". Un barrido con dos formas sintacticas necesita las dos en el mapa, no una. Estampados a mano restantes en el catalogo: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```ts
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// morfo — the declaration
direction: {}, // stamps on 'provider'
direction: { parts: ['trigger'] }, // a root that renders no element
// provider — the wire the type now demands
feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion que hay que repetir 49 veces y que un tercio del catalogo incumple no es una convencion — es una abstraccion que falta. VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B (un AttrPlan del compilador de morfo) con el argumento de que A deja `dir` invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con `:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que le achacaba a A no existe. Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que expanden: renderProps() 21 un AttrPlan les llegaria runtimePart.props 27 NO les llegaria ninguna 1 O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a `renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se inyectan ahi sin declaracion en morfo. EL GATE ES DE TIPOS, que es el premio de verdad: dir: SomaRuntimeDir | null // OBLIGATORIO en SomaRuntimeSources `null` es la unica salida y dice algo cierto: este runtime no estampa direccion. Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que alguien abre el componente en RTL. `dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza elemento), drawer y float-panel ['content'] (pintan en el portal), number-field y css-field ['provider','input']. VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la demo: tabs auto -> ltr prop rtl -> rtl vuelta a auto -> ltr dropdown-menu el TRIGGER pasa a rtl y el panel portalizado tambien number-field raiz e input, los dos a rtl drawer el content portalizado a rtl por la cadena de prefs accordion sigue la cadena y vuelve, sin una sola linea en su proveedor check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado: uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el otro es que `media-player` ya no puede compilar sin el campo (su unica linea tocada, +1/-1, es mecanica). Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga). ⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco tests de "exposes root props". Un barrido con dos formas sintacticas necesita las dos en el mapa, no una. Estampados a mano restantes en el catalogo: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
this.runtime = this.soma.runtime(xMorfo, {
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
dir: this.opts.dir, // the raw ASSERTION, never resolved
feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion que hay que repetir 49 veces y que un tercio del catalogo incumple no es una convencion — es una abstraccion que falta. VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B (un AttrPlan del compilador de morfo) con el argumento de que A deja `dir` invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con `:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que le achacaba a A no existe. Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que expanden: renderProps() 21 un AttrPlan les llegaria runtimePart.props 27 NO les llegaria ninguna 1 O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a `renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se inyectan ahi sin declaracion en morfo. EL GATE ES DE TIPOS, que es el premio de verdad: dir: SomaRuntimeDir | null // OBLIGATORIO en SomaRuntimeSources `null` es la unica salida y dice algo cierto: este runtime no estampa direccion. Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que alguien abre el componente en RTL. `dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza elemento), drawer y float-panel ['content'] (pintan en el portal), number-field y css-field ['provider','input']. VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la demo: tabs auto -> ltr prop rtl -> rtl vuelta a auto -> ltr dropdown-menu el TRIGGER pasa a rtl y el panel portalizado tambien number-field raiz e input, los dos a rtl drawer el content portalizado a rtl por la cadena de prefs accordion sigue la cadena y vuelve, sin una sola linea en su proveedor check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado: uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el otro es que `media-player` ya no puede compilar sin el campo (su unica linea tocada, +1/-1, es mecanica). Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga). ⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco tests de "exposes root props". Un barrido con dos formas sintacticas necesita las dos en el mapa, no una. Estampados a mano restantes en el catalogo: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// …
});
```
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
A morfo that declares `direction` makes `dir` **required** — forgetting the
wire does not compile. A morfo that doesn't makes it **forbidden** — a
component with no reading direction can never gain a stray attribute, and the
~75 direction-less runtimes never mention the axis at all. `direction.parts`
is validated against the declared part tree when the morfo compiles: an
unknown name **throws** instead of silently stamping nothing. A provider must
not write `dir` into its render props — the runtime already put it there.
A secondary runtime of the SAME morfo (an item, a cell, a sentinel) meets the
same requirement and passes the same owner assertion; the stamp only lands on
`direction.parts`, which those runtimes never register.
The census guard (`src/uix/morfo/direction-census.test.ts`) closes the one
edge the type cannot see — the public prop and the morfo live in different
files — by crossing `dir?: Direction` in each component's props with the
morfo declaration, in both directions, with the signed exceptions listed
inline (`field-langs`, `waveform`, and the three pure overlays whose stamp
belongs to the floating layer).
feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion que hay que repetir 49 veces y que un tercio del catalogo incumple no es una convencion — es una abstraccion que falta. VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B (un AttrPlan del compilador de morfo) con el argumento de que A deja `dir` invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con `:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que le achacaba a A no existe. Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que expanden: renderProps() 21 un AttrPlan les llegaria runtimePart.props 27 NO les llegaria ninguna 1 O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a `renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se inyectan ahi sin declaracion en morfo. EL GATE ES DE TIPOS, que es el premio de verdad: dir: SomaRuntimeDir | null // OBLIGATORIO en SomaRuntimeSources `null` es la unica salida y dice algo cierto: este runtime no estampa direccion. Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que alguien abre el componente en RTL. `dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza elemento), drawer y float-panel ['content'] (pintan en el portal), number-field y css-field ['provider','input']. VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la demo: tabs auto -> ltr prop rtl -> rtl vuelta a auto -> ltr dropdown-menu el TRIGGER pasa a rtl y el panel portalizado tambien number-field raiz e input, los dos a rtl drawer el content portalizado a rtl por la cadena de prefs accordion sigue la cadena y vuelve, sin una sola linea en su proveedor check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado: uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el otro es que `media-player` ya no puede compilar sin el campo (su unica linea tocada, +1/-1, es mecanica). Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga). ⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco tests de "exposes root props". Un barrido con dos formas sintacticas necesita las dos en el mapa, no una. Estampados a mano restantes en el catalogo: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**The open question is still WHICH element, not whether**, and now it is
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
answered in the declaration: `direction.parts` names the parts that carry the
stamp, defaulting to `['provider']`. It is the element the paint sits on:
feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion que hay que repetir 49 veces y que un tercio del catalogo incumple no es una convencion — es una abstraccion que falta. VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B (un AttrPlan del compilador de morfo) con el argumento de que A deja `dir` invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con `:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que le achacaba a A no existe. Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que expanden: renderProps() 21 un AttrPlan les llegaria runtimePart.props 27 NO les llegaria ninguna 1 O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a `renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se inyectan ahi sin declaracion en morfo. EL GATE ES DE TIPOS, que es el premio de verdad: dir: SomaRuntimeDir | null // OBLIGATORIO en SomaRuntimeSources `null` es la unica salida y dice algo cierto: este runtime no estampa direccion. Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que alguien abre el componente en RTL. `dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza elemento), drawer y float-panel ['content'] (pintan en el portal), number-field y css-field ['provider','input']. VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la demo: tabs auto -> ltr prop rtl -> rtl vuelta a auto -> ltr dropdown-menu el TRIGGER pasa a rtl y el panel portalizado tambien number-field raiz e input, los dos a rtl drawer el content portalizado a rtl por la cadena de prefs accordion sigue la cadena y vuelve, sin una sola linea en su proveedor check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado: uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el otro es que `media-player` ya no puede compilar sin el campo (su unica linea tocada, +1/-1, es mecanica). Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga). ⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco tests de "exposes root props". Un barrido con dos formas sintacticas necesita las dos en el mapa, no una. Estampados a mano restantes en el catalogo: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- Ordinary components — the provider root, which is the default.
- A root that **renders no element** — the stamp goes to the part that IS the
in-place half (`dropdown-menu` → `parts: ['trigger']`).
- A field that paints on **both** its shell and its input —
`parts: ['provider', 'input']`.
fix(direction): la regla de estampado era demasiado estrecha — 16 componentes la incumplian Decision del usuario (opcion B). La regla de §2 decia «estampa si el recipe usa `:dir()`», y esa premisa es FALSA: `:dir()` no es el unico lector. Tambien leen la direccion computada del elemento las propiedades logicas (`margin-inline-*`, `text-align: start`) y —lo que lo hace casi universal— `flex-direction: row`, que se INVIERTE. En la practica, cualquier componente con maquetacion horizontal depende de la direccion. La regla honesta, ya escrita: quien acepta `dir` lo estampa. La pregunta no es SI, es EN QUE ELEMENTO — el que lleva la pintura. Y para eso hay cuatro casos, ahora documentados: raiz normal · superficie PORTALIZADA (el wrapper flotante, que la capa ya estampa porque fuera del subarbol no puede heredar) · componente con LAS DOS mitades, que necesita las dos · y geometria aplicada desde JS, que no necesita atributo. Censo: de 55 que aceptan `dir`, 20 no estampaban. Clasificados uno a uno leyendo recipe + wrapper + donde renderiza cada parte: - 16 ESTAMPAN ahora, cada uno en la parte que lleva la pintura. Los sub-partes (drawer→Content, float-panel→Content, dropdown-menu→Trigger porque su raiz no renderiza elemento) llevan comentario nombrando la pintura que cubren. - 4 NO estampan y es CORRECTO: popover, tooltip, link-preview y context-menu. Su unica pintura direccional es el panel portalizado, que ya estampa la capa flotante; y el root de context-menu no renderiza elemento ninguno. DOS DEFECTOS QUE EL BARRIDO DESTAPO, uno de ellos MIO: 1. `sidebar` pasaba el valor RESUELTO al motor flotante. Esa linea la escribi yo en §9.19: arregle que se saltara la prop y deje el otro defecto, que siempre es concreto. La capa estampa lo que recibe, asi que un flyout SIN afirmacion recibia `dir="ltr"` — forzando su subarbol a LTR dentro de una pagina RTL, que es exactamente lo que el crudo existe para impedir. Era el UNICO de once consumidores flotantes que lo hacia. De paso desaparece su `resolvedDir`: sidebar no hace matematica direccional, toda su pintura es CSS. 2. La afirmacion de la raiz NO LLEGABA al panel portalizado. `select`, `combobox` y `context-menu` resolvian su Content con `activeDir(() => dir, soma)`, sin paso por el padre — asi que `<Select dir="rtl">` movia el trigger y dejaba la lista atras. Antes del barrido no se notaba porque NINGUNA de las dos mitades estaba afirmada y coincidian por heredar las dos del `<html>`; estampar el trigger lo habria hecho VISIBLE. Compuesto el enlace en el punto de llamada, que es la forma que §1 imprime y que `dropdown-menu` y `menubar` ya usaban. El resolutor sigue sin paso por el padre. Y `color-picker`, que es el mismo caso con una regla `:dir()` REAL varada: el area mirroring vive en el panel portalizado (`color-picker.css:298,335`) mientras la matematica del thumb y el arrastre salen de la raiz. Sin el enlace, el thumb se voltea y el gradiente de debajo no — justo lo que avisa el comentario de la receta. Su wrapper ya reestampaba `data-color` por esta misma razon; le faltaba `dir`. MEDIDO en Chrome (select): la raiz pasa de NO tener atributo a estamparlo, y trigger y panel coinciden en las tres posiciones del toggle. Estructuralmente ya no pueden divergir: ambos leen `opts.dir` de la misma raiz. `check` sin errores en ninguno de los 20 (el total 80 son 77 de base mas 3 de `media-player`, que edita otra sesion). 90 tests de los componentes tocados en verde. `rtl:check` 1, el de `palabras`. `docs:check` 0/0. QUEDA, reportado y sin tocar: ningun picker de eidos reenvia `dir` a su `PopoverContent`, asi que el cromo del `picker-shell` dentro del portal (`picker-shell.css:78`, el split del pie) queda sin cubrir en los siete. Los controles interiores (Calendar, los Slider) se salvan porque reciben el crudo por su cuenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Portalled surfaces** — the floating wrapper, which the floating layer
feat(direction): la propagacion pertenece a la capa flotante Un panel portalizado se renderiza FUERA del subarbol de su dueño, asi que la herencia no lo alcanza: la afirmacion del dueño solo llega si alguien la lleva. Eso estaba delegado en cada consumidor — siete envoltorios Content componian el enlace al padre a mano — y quien COMPONE un popover no tenia por donde hacerlo. Medido con <DatePicker dir="rtl"> sobre una pagina LTR: el wrapper flotante estampaba dir="ltr", el picker-shell y su pie se disponian en LTR, y solo el calendario espejaba. Panel partido por la mitad, el fallo del §2 del contrato. La capa lo compone ahora una sola vez: el dir propio de la superficie -> el dir afirmado por el proveedor DUEÑO -> omitir - FloatingProviderOpts.dir el dueño baja su afirmacion (ya pasada por activeDir, o sea prop -> prefs) - FloatingContent.assertedDir donde se encuentran; lo leen los DOS estampados del wrapper (rama nativa y rama JS) - createFloatingShellRoot lo reenvia; es la puerta de 8 de los 10 FloatingProvider. Los 2 submenus toman el del menu padre. FloatingContentOpts.dir pasa a ser la prop CRUDA del Content. Resolverla ahi la haria casi siempre concreta (prefs) y el dueño no ganaria nunca el fallback. Se borran los 7 enlaces compuestos a mano con su soma / activeDir / parentRoot. Sobrevive uno a proposito: dropdown-menu-sub-content lo necesita para effectiveSide, que es matematica y quiere valor resuelto. popover, tooltip y link-preview aceptan dir en su raiz. No estampan nada — no renderizan elemento —; el valor existe para cruzar el portal, y es lo que permite que los 9 componentes que componen un popover empujen su direccion al panel. Borrados 9 resolvedDir muertos en los proveedores Content: nadie los leia (toda la matematica direccional consume el de la RAIZ) y el cambio de semantica de opts.dir dejaba su docblock mintiendo. Demos: las 7 que aceptan dir lo pasan ahora a la instancia — time-picker, time-range-picker y color-picker no tenian control ninguno; emoji-picker y natural-time-picker lo tenian solo en el arnes. Sin eso el camino de la PROP, el unico que ejercita el portal, no era observable. Verificado en Chrome real, direccion movida por el control de la demo: date-picker Clear/Cancel/Close 15/102/209 -> 200/92/15 espejo exacto date-range-picker 448/217/17 sin regresion dropdown-menu (raiz y submenu, data-side=left en RTL), context-menu, menubar, select, sidebar (flyout), y tooltip en auto sigue estampando ltr — la cola de prefs sobrevive check 78 = linea base (A/B con stash) · 158/158 en los 20 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/565. Queda documentado en el handoff un defecto PREEXISTENTE de otra familia que esto vuelve visible: un componente eidos que monta otro componente del canon en SITIO no le reenvia el dir, y el hijo estampa ltr cortando la herencia del panel (natural-time-picker/Slider, emoji-picker/Command+ToggleGroup, color-field/Select). No hay precedente de ese reenvio en eidos: es doctrina nueva y movimiento propio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
stamps from `assertedDir` (§1). It has to: portalled to `<body>`, it is
outside the component's subtree and cannot inherit from the trigger. A
feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion que hay que repetir 49 veces y que un tercio del catalogo incumple no es una convencion — es una abstraccion que falta. VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B (un AttrPlan del compilador de morfo) con el argumento de que A deja `dir` invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con `:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que le achacaba a A no existe. Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que expanden: renderProps() 21 un AttrPlan les llegaria runtimePart.props 27 NO les llegaria ninguna 1 O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a `renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se inyectan ahi sin declaracion en morfo. EL GATE ES DE TIPOS, que es el premio de verdad: dir: SomaRuntimeDir | null // OBLIGATORIO en SomaRuntimeSources `null` es la unica salida y dice algo cierto: este runtime no estampa direccion. Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que alguien abre el componente en RTL. `dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza elemento), drawer y float-panel ['content'] (pintan en el portal), number-field y css-field ['provider','input']. VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la demo: tabs auto -> ltr prop rtl -> rtl vuelta a auto -> ltr dropdown-menu el TRIGGER pasa a rtl y el panel portalizado tambien number-field raiz e input, los dos a rtl drawer el content portalizado a rtl por la cadena de prefs accordion sigue la cadena y vuelve, sin una sola linea en su proveedor check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado: uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el otro es que `media-player` ya no puede compilar sin el campo (su unica linea tocada, +1/-1, es mecanica). Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga). ⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco tests de "exposes root props". Un barrido con dos formas sintacticas necesita las dos en el mapa, no una. Estampados a mano restantes en el catalogo: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
component whose only direction-dependent paint is portalled names that part
(`drawer` / `float-panel` → `parts: ['content']`); one that merely composes
an overlay is covered provided it hands that overlay its `dir`.
fix(direction): la regla de estampado era demasiado estrecha — 16 componentes la incumplian Decision del usuario (opcion B). La regla de §2 decia «estampa si el recipe usa `:dir()`», y esa premisa es FALSA: `:dir()` no es el unico lector. Tambien leen la direccion computada del elemento las propiedades logicas (`margin-inline-*`, `text-align: start`) y —lo que lo hace casi universal— `flex-direction: row`, que se INVIERTE. En la practica, cualquier componente con maquetacion horizontal depende de la direccion. La regla honesta, ya escrita: quien acepta `dir` lo estampa. La pregunta no es SI, es EN QUE ELEMENTO — el que lleva la pintura. Y para eso hay cuatro casos, ahora documentados: raiz normal · superficie PORTALIZADA (el wrapper flotante, que la capa ya estampa porque fuera del subarbol no puede heredar) · componente con LAS DOS mitades, que necesita las dos · y geometria aplicada desde JS, que no necesita atributo. Censo: de 55 que aceptan `dir`, 20 no estampaban. Clasificados uno a uno leyendo recipe + wrapper + donde renderiza cada parte: - 16 ESTAMPAN ahora, cada uno en la parte que lleva la pintura. Los sub-partes (drawer→Content, float-panel→Content, dropdown-menu→Trigger porque su raiz no renderiza elemento) llevan comentario nombrando la pintura que cubren. - 4 NO estampan y es CORRECTO: popover, tooltip, link-preview y context-menu. Su unica pintura direccional es el panel portalizado, que ya estampa la capa flotante; y el root de context-menu no renderiza elemento ninguno. DOS DEFECTOS QUE EL BARRIDO DESTAPO, uno de ellos MIO: 1. `sidebar` pasaba el valor RESUELTO al motor flotante. Esa linea la escribi yo en §9.19: arregle que se saltara la prop y deje el otro defecto, que siempre es concreto. La capa estampa lo que recibe, asi que un flyout SIN afirmacion recibia `dir="ltr"` — forzando su subarbol a LTR dentro de una pagina RTL, que es exactamente lo que el crudo existe para impedir. Era el UNICO de once consumidores flotantes que lo hacia. De paso desaparece su `resolvedDir`: sidebar no hace matematica direccional, toda su pintura es CSS. 2. La afirmacion de la raiz NO LLEGABA al panel portalizado. `select`, `combobox` y `context-menu` resolvian su Content con `activeDir(() => dir, soma)`, sin paso por el padre — asi que `<Select dir="rtl">` movia el trigger y dejaba la lista atras. Antes del barrido no se notaba porque NINGUNA de las dos mitades estaba afirmada y coincidian por heredar las dos del `<html>`; estampar el trigger lo habria hecho VISIBLE. Compuesto el enlace en el punto de llamada, que es la forma que §1 imprime y que `dropdown-menu` y `menubar` ya usaban. El resolutor sigue sin paso por el padre. Y `color-picker`, que es el mismo caso con una regla `:dir()` REAL varada: el area mirroring vive en el panel portalizado (`color-picker.css:298,335`) mientras la matematica del thumb y el arrastre salen de la raiz. Sin el enlace, el thumb se voltea y el gradiente de debajo no — justo lo que avisa el comentario de la receta. Su wrapper ya reestampaba `data-color` por esta misma razon; le faltaba `dir`. MEDIDO en Chrome (select): la raiz pasa de NO tener atributo a estamparlo, y trigger y panel coinciden en las tres posiciones del toggle. Estructuralmente ya no pueden divergir: ambos leen `opts.dir` de la misma raiz. `check` sin errores en ninguno de los 20 (el total 80 son 77 de base mas 3 de `media-player`, que edita otra sesion). 90 tests de los componentes tocados en verde. `rtl:check` 1, el de `palabras`. `docs:check` 0/0. QUEDA, reportado y sin tocar: ningun picker de eidos reenvia `dir` a su `PopoverContent`, asi que el cromo del `picker-shell` dentro del portal (`picker-shell.css:78`, el split del pie) queda sin cubrir en los siete. Los controles interiores (Calendar, los Slider) se salvan porque reciben el crudo por su cuenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- A component with **both** halves — a trigger in place and a menu portalled —
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
needs both.
fix(direction): la regla de estampado era demasiado estrecha — 16 componentes la incumplian Decision del usuario (opcion B). La regla de §2 decia «estampa si el recipe usa `:dir()`», y esa premisa es FALSA: `:dir()` no es el unico lector. Tambien leen la direccion computada del elemento las propiedades logicas (`margin-inline-*`, `text-align: start`) y —lo que lo hace casi universal— `flex-direction: row`, que se INVIERTE. En la practica, cualquier componente con maquetacion horizontal depende de la direccion. La regla honesta, ya escrita: quien acepta `dir` lo estampa. La pregunta no es SI, es EN QUE ELEMENTO — el que lleva la pintura. Y para eso hay cuatro casos, ahora documentados: raiz normal · superficie PORTALIZADA (el wrapper flotante, que la capa ya estampa porque fuera del subarbol no puede heredar) · componente con LAS DOS mitades, que necesita las dos · y geometria aplicada desde JS, que no necesita atributo. Censo: de 55 que aceptan `dir`, 20 no estampaban. Clasificados uno a uno leyendo recipe + wrapper + donde renderiza cada parte: - 16 ESTAMPAN ahora, cada uno en la parte que lleva la pintura. Los sub-partes (drawer→Content, float-panel→Content, dropdown-menu→Trigger porque su raiz no renderiza elemento) llevan comentario nombrando la pintura que cubren. - 4 NO estampan y es CORRECTO: popover, tooltip, link-preview y context-menu. Su unica pintura direccional es el panel portalizado, que ya estampa la capa flotante; y el root de context-menu no renderiza elemento ninguno. DOS DEFECTOS QUE EL BARRIDO DESTAPO, uno de ellos MIO: 1. `sidebar` pasaba el valor RESUELTO al motor flotante. Esa linea la escribi yo en §9.19: arregle que se saltara la prop y deje el otro defecto, que siempre es concreto. La capa estampa lo que recibe, asi que un flyout SIN afirmacion recibia `dir="ltr"` — forzando su subarbol a LTR dentro de una pagina RTL, que es exactamente lo que el crudo existe para impedir. Era el UNICO de once consumidores flotantes que lo hacia. De paso desaparece su `resolvedDir`: sidebar no hace matematica direccional, toda su pintura es CSS. 2. La afirmacion de la raiz NO LLEGABA al panel portalizado. `select`, `combobox` y `context-menu` resolvian su Content con `activeDir(() => dir, soma)`, sin paso por el padre — asi que `<Select dir="rtl">` movia el trigger y dejaba la lista atras. Antes del barrido no se notaba porque NINGUNA de las dos mitades estaba afirmada y coincidian por heredar las dos del `<html>`; estampar el trigger lo habria hecho VISIBLE. Compuesto el enlace en el punto de llamada, que es la forma que §1 imprime y que `dropdown-menu` y `menubar` ya usaban. El resolutor sigue sin paso por el padre. Y `color-picker`, que es el mismo caso con una regla `:dir()` REAL varada: el area mirroring vive en el panel portalizado (`color-picker.css:298,335`) mientras la matematica del thumb y el arrastre salen de la raiz. Sin el enlace, el thumb se voltea y el gradiente de debajo no — justo lo que avisa el comentario de la receta. Su wrapper ya reestampaba `data-color` por esta misma razon; le faltaba `dir`. MEDIDO en Chrome (select): la raiz pasa de NO tener atributo a estamparlo, y trigger y panel coinciden en las tres posiciones del toggle. Estructuralmente ya no pueden divergir: ambos leen `opts.dir` de la misma raiz. `check` sin errores en ninguno de los 20 (el total 80 son 77 de base mas 3 de `media-player`, que edita otra sesion). 90 tests de los componentes tocados en verde. `rtl:check` 1, el de `palabras`. `docs:check` 0/0. QUEDA, reportado y sin tocar: ningun picker de eidos reenvia `dir` a su `PopoverContent`, asi que el cromo del `picker-shell` dentro del portal (`picker-shell.css:78`, el split del pie) queda sin cubrir en los siete. Los controles interiores (Calendar, los Slider) se salvan porque reciben el crudo por su cuenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Nothing painted from CSS at all** — geometry computed and applied by JS —
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
needs no attribute, and adding one only enlarges the DOM: the morfo simply
declares no `direction`.
feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion que hay que repetir 49 veces y que un tercio del catalogo incumple no es una convencion — es una abstraccion que falta. VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B (un AttrPlan del compilador de morfo) con el argumento de que A deja `dir` invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con `:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que le achacaba a A no existe. Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que expanden: renderProps() 21 un AttrPlan les llegaria runtimePart.props 27 NO les llegaria ninguna 1 O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a `renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se inyectan ahi sin declaracion en morfo. EL GATE ES DE TIPOS, que es el premio de verdad: dir: SomaRuntimeDir | null // OBLIGATORIO en SomaRuntimeSources `null` es la unica salida y dice algo cierto: este runtime no estampa direccion. Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que alguien abre el componente en RTL. `dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza elemento), drawer y float-panel ['content'] (pintan en el portal), number-field y css-field ['provider','input']. VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la demo: tabs auto -> ltr prop rtl -> rtl vuelta a auto -> ltr dropdown-menu el TRIGGER pasa a rtl y el panel portalizado tambien number-field raiz e input, los dos a rtl drawer el content portalizado a rtl por la cadena de prefs accordion sigue la cadena y vuelve, sin una sola linea en su proveedor check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado: uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el otro es que `media-player` ya no puede compilar sin el campo (su unica linea tocada, +1/-1, es mecanica). Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga). ⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco tests de "exposes root props". Un barrido con dos formas sintacticas necesita las dos en el mapa, no una. Estampados a mano restantes en el catalogo: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Ask what the paint reads and where it lives**, then name that part.
fix(direction): la regla de estampado era demasiado estrecha — 16 componentes la incumplian Decision del usuario (opcion B). La regla de §2 decia «estampa si el recipe usa `:dir()`», y esa premisa es FALSA: `:dir()` no es el unico lector. Tambien leen la direccion computada del elemento las propiedades logicas (`margin-inline-*`, `text-align: start`) y —lo que lo hace casi universal— `flex-direction: row`, que se INVIERTE. En la practica, cualquier componente con maquetacion horizontal depende de la direccion. La regla honesta, ya escrita: quien acepta `dir` lo estampa. La pregunta no es SI, es EN QUE ELEMENTO — el que lleva la pintura. Y para eso hay cuatro casos, ahora documentados: raiz normal · superficie PORTALIZADA (el wrapper flotante, que la capa ya estampa porque fuera del subarbol no puede heredar) · componente con LAS DOS mitades, que necesita las dos · y geometria aplicada desde JS, que no necesita atributo. Censo: de 55 que aceptan `dir`, 20 no estampaban. Clasificados uno a uno leyendo recipe + wrapper + donde renderiza cada parte: - 16 ESTAMPAN ahora, cada uno en la parte que lleva la pintura. Los sub-partes (drawer→Content, float-panel→Content, dropdown-menu→Trigger porque su raiz no renderiza elemento) llevan comentario nombrando la pintura que cubren. - 4 NO estampan y es CORRECTO: popover, tooltip, link-preview y context-menu. Su unica pintura direccional es el panel portalizado, que ya estampa la capa flotante; y el root de context-menu no renderiza elemento ninguno. DOS DEFECTOS QUE EL BARRIDO DESTAPO, uno de ellos MIO: 1. `sidebar` pasaba el valor RESUELTO al motor flotante. Esa linea la escribi yo en §9.19: arregle que se saltara la prop y deje el otro defecto, que siempre es concreto. La capa estampa lo que recibe, asi que un flyout SIN afirmacion recibia `dir="ltr"` — forzando su subarbol a LTR dentro de una pagina RTL, que es exactamente lo que el crudo existe para impedir. Era el UNICO de once consumidores flotantes que lo hacia. De paso desaparece su `resolvedDir`: sidebar no hace matematica direccional, toda su pintura es CSS. 2. La afirmacion de la raiz NO LLEGABA al panel portalizado. `select`, `combobox` y `context-menu` resolvian su Content con `activeDir(() => dir, soma)`, sin paso por el padre — asi que `<Select dir="rtl">` movia el trigger y dejaba la lista atras. Antes del barrido no se notaba porque NINGUNA de las dos mitades estaba afirmada y coincidian por heredar las dos del `<html>`; estampar el trigger lo habria hecho VISIBLE. Compuesto el enlace en el punto de llamada, que es la forma que §1 imprime y que `dropdown-menu` y `menubar` ya usaban. El resolutor sigue sin paso por el padre. Y `color-picker`, que es el mismo caso con una regla `:dir()` REAL varada: el area mirroring vive en el panel portalizado (`color-picker.css:298,335`) mientras la matematica del thumb y el arrastre salen de la raiz. Sin el enlace, el thumb se voltea y el gradiente de debajo no — justo lo que avisa el comentario de la receta. Su wrapper ya reestampaba `data-color` por esta misma razon; le faltaba `dir`. MEDIDO en Chrome (select): la raiz pasa de NO tener atributo a estamparlo, y trigger y panel coinciden en las tres posiciones del toggle. Estructuralmente ya no pueden divergir: ambos leen `opts.dir` de la misma raiz. `check` sin errores en ninguno de los 20 (el total 80 son 77 de base mas 3 de `media-player`, que edita otra sesion). 90 tests de los componentes tocados en verde. `rtl:check` 1, el de `palabras`. `docs:check` 0/0. QUEDA, reportado y sin tocar: ningun picker de eidos reenvia `dir` a su `PopoverContent`, asi que el cromo del `picker-shell` dentro del portal (`picker-shell.css:78`, el split del pie) queda sin cubrir en los siete. Los controles interiores (Calendar, los Slider) se salvan porque reciben el crudo por su cuenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
docs(direction): la excepcion que el usuario veto — :dir() que traduce una prop logica «NO añadas la direccion al avatar porque tambien depende de donde se situa el badge». Tiene razon, y mi regla de §2 —«si el recipe usa :dir(), acepta y estampa `dir`»— es demasiado ancha: mandaba al siguiente a hacer justo eso. NO toda regla `:dir()` afirma una direccion. `Avatar.Badge` expone la esquina con `position` en terminos LOGICOS (`top-start` / `top-end`), asi que el ancla (`inset-inline-*`) ya se voltea sola; `:dir(rtl)` solo invierte el SIGNO del `translate` fisico que hace sobresalir la insignia —por eso las cuatro reglas llevan su marcador `rtl-physical:`, es el caso sancionado de RTL-1, no un selector de lado—. `Image` es lo mismo con otro mecanismo: `object-position` no tiene palabra clave logica, asi que `:dir(rtl)` traduce a `left`/`right` el `start`/`end` que el consumidor ya eligio. En los dos, una prop `dir` seria un SEGUNDO mando para lo que el primero ya decide, y pelearia en vez de componer: el ancla logica resuelve por la direccion ambiente mientras la prop dice fijarla. Esa regla `:dir()` necesita la direccion que REALMENTE maqueto el elemento, que es la heredada, por diseño y no por descuido. El test que queda escrito: si el componente tiene vocabulario logico propio para el eje, no lleva `dir`. Un `Switch` no lo tiene —su pulgar viaja en un sentido y la direccion es el unico input—, asi que ahi si es significativo. Un `Avatar.Badge` tiene `position`, asi que no. Barrido: de los 13 recipes con `:dir()`, SOLO `avatar` e `image` tienen ademas prop logica start/end. La excepcion son exactamente esos dos. Corrige tambien lo que reporte como hallazgo: «avatar e image usan `:dir()` sin aceptar `dir`» NO es un defecto ni una decision pendiente, es el diseño correcto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### The exception: `:dir()` translating an already-logical prop
Not every `:dir()` rule is about asserting a direction. Some components expose
the axis themselves, as a **logical prop** — a badge at `top-start` or
`top-end`, an image anchored at `start` or `end`. There the consumer has already
said which side they mean, in logical terms; `:dir()` is only supplying the
physical form CSS lacks: the sign of a `translate`, or `object-position: left`
where no `object-position: start` exists.
**Those components must NOT take a `dir` prop.** It would be a second knob for
an outcome the first knob already decides, and the two would fight rather than
compose — the logical anchor resolves from the ambient direction while the prop
claims to set it. The `:dir()` rule needs the direction that actually laid the
element out, which is the inherited one, by design and not by omission.
The test is whether the component has its own logical vocabulary for the axis.
A `Switch` has none — its thumb travels one way and direction is the only input,
so asserting `dir` on it is meaningful. An `Avatar.Badge` has `position`, so it
is not.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## 3. Selector doctrine
| Form | Verdict |
| ------------------ | --------------------------------------- |
| `:dir(rtl)` | **the canon** |
| `[dir='rtl'] …` | **forbidden** |
| `[data-dir='rtl']` | legitimate — a different attribute (§2) |
`[dir='rtl']` fails twice. The attribute is not defaulted, so the common case is
an element with no `dir` at all and the selector never matches; and as a
descendant selector it ignores any nearer `dir` that re-declares the axis.
`:dir()` reads the resolved direction and has neither failure.
---
## 4. The trap the guard exists for
**A logical anchor paired with a physical displacement.** `inset-inline-start`
flips with the direction; `transform: translateX(-50%)` does not. Put them in
the same block and the element lands a full offset off-axis in RTL — the anchor
moved, the transform did not.
`RTL-1` (`src/uix/eidos/rtl-lint.ts`, run by `npm run rtl:check`) is the guard.
It reads CSS text and flags an inline-axis translate inside a block that anchors
on `inset-inline*` / `margin-inline*`. The sanctioned escape is a
`rtl-physical: <reason>` comment when the geometry genuinely is physical.
**Know its blind spot before trusting a green run**: it sees a pairing only when
both halves are CSS declarations. An anchor in the recipe plus a `transform`
written inline by JS is the same defect and is invisible to it — as is the same
pairing living in a shared motion preset rather than in a recipe.
### The double flip
The opposite mistake, and the harder one to see: **a `:dir(rtl)` rule whose body
only re-assigns logical properties.** A logical property has _already_ mirrored
by the time that rule matches, so moving it from `inline-start` to `inline-end`
sends it back to where it started — and, since the sibling declarations were
left alone, it lands on the opposite edge from the thing it belongs to. An
indent on one side with its rail on the other is the signature.
feat(direction): RTL-2 — el guard del doble volteo, la forma que se me colo dos veces Cierra §10.1, la ultima deuda del eje. `dee2c9e3a` arreglo los dos defectos pero dejo la FORMA sin guard, y es la que sobrevivio a la migracion §6.3 y a que yo diera el eje por cerrado en §9.19. LA FIRMA — dentro de un bloque cuyo prelude tiene `:dir()`, la misma familia logica (`border`/`padding`/`margin`/`inset`-`inline`) declarada en LAS DOS caras, EXACTAMENTE UNA con valor neutro (`0`, `auto`, `none`, `initial`, `unset`, `revert`). Ese desequilibrio ES el espejo cancelado: la propiedad ya se habia volteado cuando la regla matchea, asi que reubicarla la devuelve al punto de partida y la deja en el borde opuesto al de sus hermanas. Es mas estrecha que «bloque `:dir()` con puras logicas» a proposito. Una regla que CAMBIA un valor bajo RTL sin reubicarlo es legitima —asimetrico por diseño existe—, y las dos caras con valor son un autor describiendo dos bordes reales. Lo que delata el defecto es el PAR, una cara apagada. Hay un test negativo por cada uno de esos casos, que son los que evitan que la regla se vuelva ruido. LAS TRES DECISIONES que el handoff dejaba abiertas: - `:dir(ltr)` tambien entra. Igual de sospechosa, y no anade ruido. - Marcador PROPIO, `rtl-mirror: <reason>`. Reutilizar `rtl-physical:` mentiria: aqui no hay nada fisico, y un marcador que miente es peor que ninguno. Mismo mecanismo (`exemptLines` toma ahora el patron por parametro), dos vocabularios. Un test comprueba que el marcador de RTL-1 NO exime a RTL-2. - Regla aparte, `lintRtlMirror()` con su propio `RtlMirrorFinding`. Los 14 tests de RTL-1 quedan intactos y el tipo lleva los campos que importan (`family`/`neutral`/`payload`) en vez de forzar los de RTL-1. VERIFICACION, en este orden: - `rtl-lint.test.ts` 27/27 (14 de RTL-1 intactos + 13 nuevos). - RECALL con el runner COMPLETO contra el arbol pre-arreglo: restaure los dos ficheros de `dee2c9e3a^` sobre el arbol, corri `rtl:check` y los devolvi con `git checkout` en el mismo bloque. **2/2 cazados** (`feed.css:147`, `tree-view.css:211`). Probar la funcion no basta: el runner es lo que corre. - `rtl:check` sobre HEAD: 1 error, el de `palabras`, preexistente y excluido. - `docs:check` 0/0 sobre 564 docs. `check` 77 = linea base. Un defecto de presentacion salio al hacerlo: el `calc()` multilinea de tree-view partia el mensaje por el primer salto. Los campos que RTL-2 emite colapsan el whitespace; con test. Actualizados los textos que el handoff avisaba que quedarian obsoletos: el `enforcement:` y §4 del contrato, la fila RTL del build contract, la fila X-1.6 del checklist, y los docblocks de `rtl-lint.ts` y `rtl-check.ts` — que son doctrina, no adorno. ⚠️ Sigue siendo cierto lo que NINGUNA de las dos reglas ve: leen texto CSS. Un `transform` inline escrito por JS, un preset de motion compartido y la geometria SVG siguen necesitando el ojo en RTL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The tell is the pair: inside a `:dir()` block, the same logical family declared
on **both** faces, one turned off (`0`, `auto`, `none`) and the other repainted.
Delete the rule and check the base declaration instead — it had already
mirrored.
`RTL-2` guards this shape, in the same file and the same run as RTL-1. Its
escape hatch is `rtl-mirror: <reason>`, a different marker from RTL-1's
`rtl-physical:` because it declares a different thing. Reach for it only when a
design breaks the mirror on purpose; the fix is almost always the deletion.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Two further physical forms the guard cannot reach, both of which must be checked
by eye in RTL:
- **SVG geometry and `text-anchor`** — a graphic with a _reading_ axis mirrors;
a _radial_ one does not. `text-anchor` is logical: leave it alone when the
composition mirrors, force the physical value when it does not.
- **Inline `transform` written by a component** — beat it with the independent
`rotate` / `translate` / `scale` properties, which compose with `transform`
instead of replacing it, rather than escalating to `!important`.
---
## 5. Behaviour that mirrors, and behaviour that does not
Mirroring is decided by whether the interaction runs along a **reading axis**,
not by whether it is horizontal.
- **Mirrors** — anything the user reads or traverses in sequence: arrow-key
navigation (via `getDirectionalKeys`), carousel travel, a slider's value
ramp, a colour channel spread across the inline dimension, a heat-map's
columns, a calendar's months.
- **Does not mirror** — geometry that is intrinsically physical: radial
controls (a colour wheel, a dial), compass-named resize handles, a freely
positioned panel's coordinates, an image being panned inside a viewport.
The distinction is the same one the references draw, and it is why a colour
_area_ mirrors while a colour _wheel_ does not.
When a component's own geometry is physical but its handles are placed with
logical insets, translate the logical edge into a physical one **once**, at the
boundary, rather than sprinkling `=== 'rtl'` through the handlers.
### Scrolling
In RTL the CSSOM scroll model runs **negative**: `scrollLeft` rests at `0` at
the start of the content and decreases to `-(scrollWidth - clientWidth)`.
Distance along the reading axis and distance from the physical left edge are
therefore two different quantities, and a component that needs both must name
both.
---
## 6. The app-global half
`prefs.direction` is the single source of the _app's_ effective direction: it
feat(direction): el flip — el atributo es la afirmacion, y undefined ACTUA La cadena se parte en la afirmacion. `activeDir` devuelve ahora SOLO lo que alguien eligio (prop -> contexto del ancestro) y ahi se detiene: eso es lo que el runtime estampa y lo que cruza portales. La preferencia de la app ya no viaja en el atributo de cada componente — llega a la pagina UNA vez, por la proyeccion automatica de P2, y todo lo demas HEREDA. Por que: con prefs dentro de la cadena por-componente, "nadie afirmo" era inalcanzable — el boot por defecto siempre tiene la dimension direction, asi que getDir() casi nunca era undefined y cada raiz del canon estampaba dir="ltr" en una pagina sin afirmaciones. El atributo nunca llegaba a estar AUSENTE, la herencia del DOM nunca llegaba a trabajar, y de ahi la clase entera de islas y reenvios que este eje llevo meses parcheando. La matematica conserva la preferencia como VALOR (no puede leer el DOM) en un solo helper: afirmacion -> soma.prefs.getDir() -> 'ltr' resolveDir() - activeDir(dir) pierde el parametro soma: 57 wrappers, y 49 quedan sin `const soma = Soma.get()` huerfano (limpiados con deteccion de identificador, no de ruta — la primera pasada se tropezaba con 'core/soma.svelte'). - 45 colas `opts.dir.current ?? 'ltr'` -> resolveDir(this.opts.dir, this.soma). - Los 4 getDirectionalKeys de submenus que leian opts.dir.current directo pasan a resolvedDir — con undefined degradaban a ltr EN SILENCIO bajo rtl ambiental. - command: 4.1 le habia dejado `dir: null` en el runtime y el estampado manual inline en el assert (la linea no estaba sola y el barrido no la vio). Ahora el runtime estampa y el inline se va. - listbox / navigation-menu: el alias assertedDir muere — tras el flip, opts.dir ES la afirmacion. - date-range-field-input / time-range-field-input: el reenvio dir= al Field compuesto en sitio sobra — DirectionContext lo lleva. - 7 wrappers renombran `resolvedDir` -> `assertedDir`: el nombre mentia. - picker / gradient-picker: activeDir(() => undefined) = la afirmacion del ancestro o nada — mejor que el prefs-siempre-concreto de antes. MEDIDO en Chrome, el modelo entero: auto UN solo [dir] en toda la pagina: el <html> de la proyeccion (antes lo llevaba casi cada raiz del canon) prefs rtl select sin atributo, computa rtl por herencia; el panel portalizado SIN atributo y computa rtl (heredo del html a traves del body) prop rtl trigger del dropdown estampa rtl, el panel portalizado estampa rtl (la afirmacion cruza), submenu data-side=left, cero islas dentro matematica slider SIN atributo con pagina rtl: click al 25% fisico da 75 y ArrowRight BAJA — resolveDir lee prefs sin que el atributo lo haga SSR curl de select y accordion: cero dir= en el payload El contrato: §1 documenta la cadena partida (activeDir -> afirmacion; resolveDir -> matematica), §6 la proyeccion automatica + la semilla y el parrafo de SSR — el <html dir> del primer render pertenece a app.html o al servidor (modelo react-aria). check 77 = linea base · suites soma+active-uix+prefs+contracts 1362 tests: los 6 de contracts y soma-attr-audit son el conjunto preexistente (A/B dos veces en fases previas; attr-audit pasa aislado) · media-player 17/17 tras hacer resolveDir defensivo con mocks sin prefs · rtl:check 1 (palabras) · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
derives from `prefs.language` unless an explicit intent — or the environment
seed — overrides it. `ActivePrefsDomProjection` writes that value to
`<html dir>` — together with `<html lang>`, which travels with it because the
browser reads both from the DOM for font selection, hyphenation and
screen-reader announcement.
**The projection is automatic in standalone boot** (ratified 2026-08-05):
components stamp only ASSERTED directions, so the page must reflect the
ambient preference or nothing does. `projectPrefs: false` opts out when the
app owns `<html>` (i18n by routing, its own projection); in `attachActiveUix`
feat(active-uix)!: el boot lo compila el build del SITIO con su propio esquema, y lo que UIX escribe en <html dir> lleva marca de propiedad (fila 2 del boot, F1 del cierre) F1 del plan de cierre del framework. Cierra el último trabajo del eje boot. EL DEFECTO. El boot por defecto compilaba un catálogo de preferencias de UN solo idioma (createDefaultUixPrefsSchema), y la raíz sembraba su entorno leyendo el <html dir> que el boot acababa de escribir: un usuario árabe de una app multiidioma no veía un parpadeo, se quedaba en LTR TODA la sesión, porque el runtime tomaba la salida del boot como si la hubiera declarado la página. EL COMPILADOR POR SITIO. Una sola fusión, composeUixPrefsSchema, la consumen createActiveUix y el boot: no puede derivar. El generador acepta --schema y --out (el especificador boot/boot-schema.ts apunta al esquema del sitio o a boot/default-schema.ts); los guards (sin runas, techo de tamaño, ASCII, sin </script, valores string por atributo) son errores del COMPILADOR con mensajes para el consumidor, porque ahora el artefacto lo produce el sitio. Un esquema que importa el barrel $prefs se rechaza nombrándolo. scripts/uix-boot-check.ts es el guard de rancidez: plugin de Vite que tumba el build con un artefacto viejo (probado con vite build real) y función para CI. Delta cero por esquema: por defecto, multiidioma con árabe, ejes redefinidos y esquema parcial. LA MARCA DE PROPIEDAD, diseño firmado por el autor. El canon de dirección dice que <html dir> es una proyección y nunca una fuente, con una sola excepción: el dir del AUTOR en la plantilla o el servidor. El boot rompió la premisa de esa excepción. Todo lo que UIX escribe en <html dir> lleva ahora PREFS_DIR_PROJECTED_ATTR (el boot con valor «boot», cada proyección del runtime con un token de instancia); la semilla solo adopta un dir SIN marca. Un dir escrito por script no es una fuente: en ejecución la dirección se afirma con prefs.setIntent('direction') o options.prefs.environment, que ganan a la semilla. Se descartaron, midiendo, la marca con valor (cierra el script y congela la dirección al navegar entre layouts) y la inferencia por valor. dispose solo retira lo que todavía es SUYO: SvelteKit crea la raíz del layout nuevo ANTES de destruir la vieja (medido), y un retiro a ciegas dejaba el <html> de la raíz nueva sin dir, lang ni data-motion/sound/haptic (medido hoy: los nueve atributos a null). Mismo principio que el unstamp de sema, que comprueba data-event-id. esbuild se declara como devDependency EXACTA 0.27.4: los bytes del boot y su hash dependen del minificador, y una subida dentro de un rango rompería la sincronía. Boot 15 198 B, sha256-hsqdGYcrRu3oEc0Q3G/A67ApQT3q9c/vT9zMDgxROg8= (el hash se mueve: firmado por el autor). VERIFICACIÓN. Constructor Opus en cuatro rondas y adversarial Opus en dos pasadas independientes, con sus reproducciones repetidas tras cada cierre: en Chromium, entrar en árabe y pasar a inglés y a español sigue al idioma, y al revés también; el dir de plantilla gana antes y después de hidratar y en una raíz recreada; una raíz recreada sobre una viva sigue al idioma con y sin boot; tras create b → destroy a, <html> conserva lo que proyectó b. Diez defectos declarados por el adversarial, cerrados (D1–D10): marca de propiedad, esquema parcial que estampaba "undefined", vigilante de dev mudo, plugin sin test, receta de CI que no cargaba en jsdom, cifras y prosa, y un vite build real que se cae cuando el boot no compila. Mutaciones en rojo, restauradas byte a byte: la proyección no marca · dispose sin guard de dueño (también repetida por el coordinador: 3 rojos) · la semilla compara valor en vez de presencia · marcar el dir del autor. Suite entera 463 ficheros / 5 437 tests, exit 0 · check 0 errores en src/ y en scripts/ (89 en web/, ledger intacto) · docs:check 0/0 · generate:boot dos veces byte-idéntico. LO QUE NO CIERRA (al ledger de cierre): (1) PREEXISTENTE — en la misma navegación entre layouts, el ActiveEidos.dispose de la raíz vieja sigue retirando data-theme/mode/density/scaling de la raíz nueva; exige cambiar el dispose de eidos, otra capa. (2) La propiedad de los atributos proyectados cuelga de la marca de dir: si la plantilla declara la dirección, dispose deja lang y data-motion puestos cuando la raíz se desmonta sin sustituta (benigno). (3) Sin boot, un script que escribe dir antes de la primera raíz es indistinguible de la plantilla y se adopta. (4) El vigilante de dev no reacciona a ficheros nuevos ni a inputs fuera del root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
it is opt-IN for the same reason. And the root SEEDS the environment from the
AUTHOR's `<html dir>` — the document template's or the server's
(`readPrefsEnvironmentFromDom`) — so the projection adopts that assertion
instead of rewriting it with the language-derived value — precedence
`intent > environment seed > derive(language) > default`.
**That is the one `dir` read back from the DOM** (§1: the attribute is a
projection, never a source). Everything UIX writes to `<html dir>` — the
pre-hydration boot and every runtime projection — carries the ownership mark
`data-dir-projected` (`PREFS_DIR_PROJECTED_ATTR`), and the seed skips a marked
`dir` by presence; the author's `dir`, adopted, is never marked. A script that
writes over a marked `dir` is not a source either: at run time a direction is
asserted with `prefs.setIntent('direction', …)` or `options.prefs.environment`,
both of which beat the seed. The mark's value names its owner, and a
projection's `dispose` retires its attributes only while the mark still names
it — the next root is created before the old one is destroyed.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
That projection is the reason the common case needs no per-component
assertion at all: the page declares its direction once at the root, every
component inherits it, and `:dir()` resolves correctly with no attribute in
between. The per-component prop exists for the other case — asserting a
direction for **one subtree** against the page's.
feat(direction): el flip — el atributo es la afirmacion, y undefined ACTUA La cadena se parte en la afirmacion. `activeDir` devuelve ahora SOLO lo que alguien eligio (prop -> contexto del ancestro) y ahi se detiene: eso es lo que el runtime estampa y lo que cruza portales. La preferencia de la app ya no viaja en el atributo de cada componente — llega a la pagina UNA vez, por la proyeccion automatica de P2, y todo lo demas HEREDA. Por que: con prefs dentro de la cadena por-componente, "nadie afirmo" era inalcanzable — el boot por defecto siempre tiene la dimension direction, asi que getDir() casi nunca era undefined y cada raiz del canon estampaba dir="ltr" en una pagina sin afirmaciones. El atributo nunca llegaba a estar AUSENTE, la herencia del DOM nunca llegaba a trabajar, y de ahi la clase entera de islas y reenvios que este eje llevo meses parcheando. La matematica conserva la preferencia como VALOR (no puede leer el DOM) en un solo helper: afirmacion -> soma.prefs.getDir() -> 'ltr' resolveDir() - activeDir(dir) pierde el parametro soma: 57 wrappers, y 49 quedan sin `const soma = Soma.get()` huerfano (limpiados con deteccion de identificador, no de ruta — la primera pasada se tropezaba con 'core/soma.svelte'). - 45 colas `opts.dir.current ?? 'ltr'` -> resolveDir(this.opts.dir, this.soma). - Los 4 getDirectionalKeys de submenus que leian opts.dir.current directo pasan a resolvedDir — con undefined degradaban a ltr EN SILENCIO bajo rtl ambiental. - command: 4.1 le habia dejado `dir: null` en el runtime y el estampado manual inline en el assert (la linea no estaba sola y el barrido no la vio). Ahora el runtime estampa y el inline se va. - listbox / navigation-menu: el alias assertedDir muere — tras el flip, opts.dir ES la afirmacion. - date-range-field-input / time-range-field-input: el reenvio dir= al Field compuesto en sitio sobra — DirectionContext lo lleva. - 7 wrappers renombran `resolvedDir` -> `assertedDir`: el nombre mentia. - picker / gradient-picker: activeDir(() => undefined) = la afirmacion del ancestro o nada — mejor que el prefs-siempre-concreto de antes. MEDIDO en Chrome, el modelo entero: auto UN solo [dir] en toda la pagina: el <html> de la proyeccion (antes lo llevaba casi cada raiz del canon) prefs rtl select sin atributo, computa rtl por herencia; el panel portalizado SIN atributo y computa rtl (heredo del html a traves del body) prop rtl trigger del dropdown estampa rtl, el panel portalizado estampa rtl (la afirmacion cruza), submenu data-side=left, cero islas dentro matematica slider SIN atributo con pagina rtl: click al 25% fisico da 75 y ArrowRight BAJA — resolveDir lee prefs sin que el atributo lo haga SSR curl de select y accordion: cero dir= en el payload El contrato: §1 documenta la cadena partida (activeDir -> afirmacion; resolveDir -> matematica), §6 la proyeccion automatica + la semilla y el parrafo de SSR — el <html dir> del primer render pertenece a app.html o al servidor (modelo react-aria). check 77 = linea base · suites soma+active-uix+prefs+contracts 1362 tests: los 6 de contracts y soma-attr-audit son el conjunto preexistente (A/B dos veces en fases previas; attr-audit pasa aislado) · media-player 17/17 tras hacer resolveDir defensivo con mocks sin prefs · rtl:check 1 (palabras) · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### Server rendering owns the first paint
The projection runs in the client. The `<html dir>` of the FIRST render
therefore belongs to `app.html` or the server (the react-aria model): an app
whose default direction is RTL sets `<html dir="rtl" lang="ar">` at the
document template — the seed adopts it, the projection keeps it, and there is
no LTR flash. Components ship no `dir` in SSR output unless the consumer
asserted one, which is the same rule they follow in the client.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Detail of the preference and its projection: [`src/arts/prefs/README.md`](../../src/arts/prefs/README.md).
---
feat(direction): las charts aceptan `dir` y corren la cadena canonica Decision del usuario. Yo habia recomendado lo contrario —dejarlas y ablandar el contrato— y me equivocaba en el diagnostico: no era que la cadena no valiese para eidos, era que **le faltaba el punto de entrada**. `activeDir(getter, soma)` pide un `Soma`, y una chart solo tiene un `ActiveEidos`. Y `ActiveEidos.prefs` es un `ActivePrefs` crudo, que no tiene `getDir()` —ese metodo vive en la vista que mira a soma—. Por eso la cadena "no era ejecutable" y por eso alguien puso ahi un `getComputedStyle`. La adaptacion entre las dos superficies es TODA la diferencia: src/uix/eidos/direction.ts → activeEidosDir(getter, prefs) Mismos dos eslabones, mismo `Direction | undefined`, misma cola `'ltr'` en el consumidor. Abre la puerta tambien a `avatar` e `image`, que son el mismo caso. Fuera la lectura del DOM y fuera la copia byte-a-byte que `chart.svelte` tenia del mismo mecanismo —dos fuentes de verdad para un hecho, y ademas el observer vigilaba `<html dir>` mientras la lectura era del elemento, asi que un ancestro que volteara en caliente no se veia nunca—. ⚠️ EL HALLAZGO QUE SOLO SALE MIDIENDO: **estampar `dir` en un `<svg>` no hace nada.** El atributo lo mapea a `direction` la hoja UA de HTML, que no alcanza a los elementos SVG: medido en Chrome, un `<svg dir="ltr">` bajo un ancestro `dir="rtl"` sigue computando `rtl`. Yo lo habia puesto ahi y el comentario afirmaba que servia. Va en el envoltorio HTML (`<figure data-chart-figure>`, `<div data-chart-frame>`). Importa porque `text-anchor` es LOGICO: con el estampado en el nodo equivocado la matematica espeja y las etiquetas no —el fallo de §2 con otro disfraz—. MEDIDO en Chrome, heatmap: LTR `ene/feb/mar` en x=26/90/154, celdas 26‥858; RTL en 851/787/723, celdas 6‥838. Funnel: poligonos de 0‥226 a 193‥420, etiquetas de 249 a 171. El toggle del topbar sigue volteandolas, ahora por prefs y no por el DOM. COSTE ACEPTADO, verificado: envolver una chart en `<div dir="rtl">` sin pasar la prop ya no la voltea. Falla de forma CONSISTENTE —el estampado del envoltorio gana sobre el ancestro, asi que matematica y pintura siguen de acuerdo— y no con las etiquetas cruzando el grafico. §7 del contrato deja de ser una divergencia declarada y pasa a ser la tabla de los dos puntos de entrada. Su justificacion era ademas falsa en dos puntos: no era "el unico sitio del catalogo" (quedan 4 mas, uno vivo en `field-langs-switcher`), y decia que una chart no se podia voltear por subarbol cuando `direction` se HEREDA y si se podia. `check` 77 = linea base. `docs:check` 0/0. Tests de eidos + libs/plots: 424/425, el fallo es `audio-player` sin morfo, preexistente y ajeno. ⚠️ No pude ver los pixeles: el panel del navegador no compone frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 7. Components with no soma provider
A few components are eidos-only: no provider, no morfo, no `Soma`. The chain
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
still applies to them — only the entry points differ, because the maths tail
is handed a `Soma` in one layer and an `ActivePrefs` in the other:
feat(direction): las charts aceptan `dir` y corren la cadena canonica Decision del usuario. Yo habia recomendado lo contrario —dejarlas y ablandar el contrato— y me equivocaba en el diagnostico: no era que la cadena no valiese para eidos, era que **le faltaba el punto de entrada**. `activeDir(getter, soma)` pide un `Soma`, y una chart solo tiene un `ActiveEidos`. Y `ActiveEidos.prefs` es un `ActivePrefs` crudo, que no tiene `getDir()` —ese metodo vive en la vista que mira a soma—. Por eso la cadena "no era ejecutable" y por eso alguien puso ahi un `getComputedStyle`. La adaptacion entre las dos superficies es TODA la diferencia: src/uix/eidos/direction.ts → activeEidosDir(getter, prefs) Mismos dos eslabones, mismo `Direction | undefined`, misma cola `'ltr'` en el consumidor. Abre la puerta tambien a `avatar` e `image`, que son el mismo caso. Fuera la lectura del DOM y fuera la copia byte-a-byte que `chart.svelte` tenia del mismo mecanismo —dos fuentes de verdad para un hecho, y ademas el observer vigilaba `<html dir>` mientras la lectura era del elemento, asi que un ancestro que volteara en caliente no se veia nunca—. ⚠️ EL HALLAZGO QUE SOLO SALE MIDIENDO: **estampar `dir` en un `<svg>` no hace nada.** El atributo lo mapea a `direction` la hoja UA de HTML, que no alcanza a los elementos SVG: medido en Chrome, un `<svg dir="ltr">` bajo un ancestro `dir="rtl"` sigue computando `rtl`. Yo lo habia puesto ahi y el comentario afirmaba que servia. Va en el envoltorio HTML (`<figure data-chart-figure>`, `<div data-chart-frame>`). Importa porque `text-anchor` es LOGICO: con el estampado en el nodo equivocado la matematica espeja y las etiquetas no —el fallo de §2 con otro disfraz—. MEDIDO en Chrome, heatmap: LTR `ene/feb/mar` en x=26/90/154, celdas 26‥858; RTL en 851/787/723, celdas 6‥838. Funnel: poligonos de 0‥226 a 193‥420, etiquetas de 249 a 171. El toggle del topbar sigue volteandolas, ahora por prefs y no por el DOM. COSTE ACEPTADO, verificado: envolver una chart en `<div dir="rtl">` sin pasar la prop ya no la voltea. Falla de forma CONSISTENTE —el estampado del envoltorio gana sobre el ancestro, asi que matematica y pintura siguen de acuerdo— y no con las etiquetas cruzando el grafico. §7 del contrato deja de ser una divergencia declarada y pasa a ser la tabla de los dos puntos de entrada. Su justificacion era ademas falsa en dos puntos: no era "el unico sitio del catalogo" (quedan 4 mas, uno vivo en `field-langs-switcher`), y decia que una chart no se podia voltear por subarbol cuando `direction` se HEREDA y si se podia. `check` 77 = linea base. `docs:check` 0/0. Tests de eidos + libs/plots: 424/425, el fallo es `audio-player` sin morfo, preexistente y ajeno. ⚠️ No pude ver los pixeles: el panel del navegador no compone frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Layer | Assertion | Maths tail |
| ----- | ------------------------ | ----------------------------------- |
| soma | `activeDir(getter)` | `resolveDir(dir, soma)` |
| eidos | `activeEidosDir(getter)` | `resolveEidosDir(dir, eidos.prefs)` |
feat(direction): las charts aceptan `dir` y corren la cadena canonica Decision del usuario. Yo habia recomendado lo contrario —dejarlas y ablandar el contrato— y me equivocaba en el diagnostico: no era que la cadena no valiese para eidos, era que **le faltaba el punto de entrada**. `activeDir(getter, soma)` pide un `Soma`, y una chart solo tiene un `ActiveEidos`. Y `ActiveEidos.prefs` es un `ActivePrefs` crudo, que no tiene `getDir()` —ese metodo vive en la vista que mira a soma—. Por eso la cadena "no era ejecutable" y por eso alguien puso ahi un `getComputedStyle`. La adaptacion entre las dos superficies es TODA la diferencia: src/uix/eidos/direction.ts → activeEidosDir(getter, prefs) Mismos dos eslabones, mismo `Direction | undefined`, misma cola `'ltr'` en el consumidor. Abre la puerta tambien a `avatar` e `image`, que son el mismo caso. Fuera la lectura del DOM y fuera la copia byte-a-byte que `chart.svelte` tenia del mismo mecanismo —dos fuentes de verdad para un hecho, y ademas el observer vigilaba `<html dir>` mientras la lectura era del elemento, asi que un ancestro que volteara en caliente no se veia nunca—. ⚠️ EL HALLAZGO QUE SOLO SALE MIDIENDO: **estampar `dir` en un `<svg>` no hace nada.** El atributo lo mapea a `direction` la hoja UA de HTML, que no alcanza a los elementos SVG: medido en Chrome, un `<svg dir="ltr">` bajo un ancestro `dir="rtl"` sigue computando `rtl`. Yo lo habia puesto ahi y el comentario afirmaba que servia. Va en el envoltorio HTML (`<figure data-chart-figure>`, `<div data-chart-frame>`). Importa porque `text-anchor` es LOGICO: con el estampado en el nodo equivocado la matematica espeja y las etiquetas no —el fallo de §2 con otro disfraz—. MEDIDO en Chrome, heatmap: LTR `ene/feb/mar` en x=26/90/154, celdas 26‥858; RTL en 851/787/723, celdas 6‥838. Funnel: poligonos de 0‥226 a 193‥420, etiquetas de 249 a 171. El toggle del topbar sigue volteandolas, ahora por prefs y no por el DOM. COSTE ACEPTADO, verificado: envolver una chart en `<div dir="rtl">` sin pasar la prop ya no la voltea. Falla de forma CONSISTENTE —el estampado del envoltorio gana sobre el ancestro, asi que matematica y pintura siguen de acuerdo— y no con las etiquetas cruzando el grafico. §7 del contrato deja de ser una divergencia declarada y pasa a ser la tabla de los dos puntos de entrada. Su justificacion era ademas falsa en dos puntos: no era "el unico sitio del catalogo" (quedan 4 mas, uno vivo en `field-langs-switcher`), y decia que una chart no se podia voltear por subarbol cuando `direction` se HEREDA y si se podia. `check` 77 = linea base. `docs:check` 0/0. Tests de eidos + libs/plots: 424/425, el fallo es `audio-player` sin morfo, preexistente y ajeno. ⚠️ No pude ver los pixeles: el panel del navegador no compone frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Both assertion entry points publish into the SAME `DirectionContext` — shared
on purpose, so an eidos-only chart inside an asserted soma subtree (or the
reverse) resolves the same fact. Both return `Direction | undefined`; the raw
stamp is the consumer's, exactly as in §1 and §2. The adapter in the maths
tail is the whole difference: `ActiveEidos.prefs` is the raw arts service,
which has no `getDir()` — that method lives on the soma-facing view.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): las charts aceptan `dir` y corren la cadena canonica Decision del usuario. Yo habia recomendado lo contrario —dejarlas y ablandar el contrato— y me equivocaba en el diagnostico: no era que la cadena no valiese para eidos, era que **le faltaba el punto de entrada**. `activeDir(getter, soma)` pide un `Soma`, y una chart solo tiene un `ActiveEidos`. Y `ActiveEidos.prefs` es un `ActivePrefs` crudo, que no tiene `getDir()` —ese metodo vive en la vista que mira a soma—. Por eso la cadena "no era ejecutable" y por eso alguien puso ahi un `getComputedStyle`. La adaptacion entre las dos superficies es TODA la diferencia: src/uix/eidos/direction.ts → activeEidosDir(getter, prefs) Mismos dos eslabones, mismo `Direction | undefined`, misma cola `'ltr'` en el consumidor. Abre la puerta tambien a `avatar` e `image`, que son el mismo caso. Fuera la lectura del DOM y fuera la copia byte-a-byte que `chart.svelte` tenia del mismo mecanismo —dos fuentes de verdad para un hecho, y ademas el observer vigilaba `<html dir>` mientras la lectura era del elemento, asi que un ancestro que volteara en caliente no se veia nunca—. ⚠️ EL HALLAZGO QUE SOLO SALE MIDIENDO: **estampar `dir` en un `<svg>` no hace nada.** El atributo lo mapea a `direction` la hoja UA de HTML, que no alcanza a los elementos SVG: medido en Chrome, un `<svg dir="ltr">` bajo un ancestro `dir="rtl"` sigue computando `rtl`. Yo lo habia puesto ahi y el comentario afirmaba que servia. Va en el envoltorio HTML (`<figure data-chart-figure>`, `<div data-chart-frame>`). Importa porque `text-anchor` es LOGICO: con el estampado en el nodo equivocado la matematica espeja y las etiquetas no —el fallo de §2 con otro disfraz—. MEDIDO en Chrome, heatmap: LTR `ene/feb/mar` en x=26/90/154, celdas 26‥858; RTL en 851/787/723, celdas 6‥838. Funnel: poligonos de 0‥226 a 193‥420, etiquetas de 249 a 171. El toggle del topbar sigue volteandolas, ahora por prefs y no por el DOM. COSTE ACEPTADO, verificado: envolver una chart en `<div dir="rtl">` sin pasar la prop ya no la voltea. Falla de forma CONSISTENTE —el estampado del envoltorio gana sobre el ancestro, asi que matematica y pintura siguen de acuerdo— y no con las etiquetas cruzando el grafico. §7 del contrato deja de ser una divergencia declarada y pasa a ser la tabla de los dos puntos de entrada. Su justificacion era ademas falsa en dos puntos: no era "el unico sitio del catalogo" (quedan 4 mas, uno vivo en `field-langs-switcher`), y decia que una chart no se podia voltear por subarbol cuando `direction` se HEREDA y si se podia. `check` 77 = linea base. `docs:check` 0/0. Tests de eidos + libs/plots: 424/425, el fallo es `audio-player` sin morfo, preexistente y ajeno. ⚠️ No pude ver los pixeles: el panel del navegador no compone frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The chart family is the worked example. It resolved by reading
`getComputedStyle(node).direction` off its own element until the entry point
existed, because the chain was genuinely not runnable from eidos.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): las charts aceptan `dir` y corren la cadena canonica Decision del usuario. Yo habia recomendado lo contrario —dejarlas y ablandar el contrato— y me equivocaba en el diagnostico: no era que la cadena no valiese para eidos, era que **le faltaba el punto de entrada**. `activeDir(getter, soma)` pide un `Soma`, y una chart solo tiene un `ActiveEidos`. Y `ActiveEidos.prefs` es un `ActivePrefs` crudo, que no tiene `getDir()` —ese metodo vive en la vista que mira a soma—. Por eso la cadena "no era ejecutable" y por eso alguien puso ahi un `getComputedStyle`. La adaptacion entre las dos superficies es TODA la diferencia: src/uix/eidos/direction.ts → activeEidosDir(getter, prefs) Mismos dos eslabones, mismo `Direction | undefined`, misma cola `'ltr'` en el consumidor. Abre la puerta tambien a `avatar` e `image`, que son el mismo caso. Fuera la lectura del DOM y fuera la copia byte-a-byte que `chart.svelte` tenia del mismo mecanismo —dos fuentes de verdad para un hecho, y ademas el observer vigilaba `<html dir>` mientras la lectura era del elemento, asi que un ancestro que volteara en caliente no se veia nunca—. ⚠️ EL HALLAZGO QUE SOLO SALE MIDIENDO: **estampar `dir` en un `<svg>` no hace nada.** El atributo lo mapea a `direction` la hoja UA de HTML, que no alcanza a los elementos SVG: medido en Chrome, un `<svg dir="ltr">` bajo un ancestro `dir="rtl"` sigue computando `rtl`. Yo lo habia puesto ahi y el comentario afirmaba que servia. Va en el envoltorio HTML (`<figure data-chart-figure>`, `<div data-chart-frame>`). Importa porque `text-anchor` es LOGICO: con el estampado en el nodo equivocado la matematica espeja y las etiquetas no —el fallo de §2 con otro disfraz—. MEDIDO en Chrome, heatmap: LTR `ene/feb/mar` en x=26/90/154, celdas 26‥858; RTL en 851/787/723, celdas 6‥838. Funnel: poligonos de 0‥226 a 193‥420, etiquetas de 249 a 171. El toggle del topbar sigue volteandolas, ahora por prefs y no por el DOM. COSTE ACEPTADO, verificado: envolver una chart en `<div dir="rtl">` sin pasar la prop ya no la voltea. Falla de forma CONSISTENTE —el estampado del envoltorio gana sobre el ancestro, asi que matematica y pintura siguen de acuerdo— y no con las etiquetas cruzando el grafico. §7 del contrato deja de ser una divergencia declarada y pasa a ser la tabla de los dos puntos de entrada. Su justificacion era ademas falsa en dos puntos: no era "el unico sitio del catalogo" (quedan 4 mas, uno vivo en `field-langs-switcher`), y decia que una chart no se podia voltear por subarbol cuando `direction` se HEREDA y si se podia. `check` 77 = linea base. `docs:check` 0/0. Tests de eidos + libs/plots: 424/425, el fallo es `audio-player` sin morfo, preexistente y ajeno. ⚠️ No pude ver los pixeles: el panel del navegador no compone frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> ⚠️ **`dir` on an `<svg>` does nothing.** The attribute is mapped to the
> `direction` property by the HTML UA stylesheet, which does not reach SVG
> elements — measured in Chrome, an `<svg dir="ltr">` under a `dir="rtl"`
> ancestor still computes `rtl`. Stamp the chart's HTML wrapper (`<figure>`,
> `<div data-chart-frame>`) instead. This matters more than it looks: SVG
> `text-anchor` is LOGICAL, so it resolves against the inherited direction. Get
> the stamp wrong and the maths mirrors while the labels do not, which is §2's
> split-in-half failure wearing a different hat.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): las charts aceptan `dir` y corren la cadena canonica Decision del usuario. Yo habia recomendado lo contrario —dejarlas y ablandar el contrato— y me equivocaba en el diagnostico: no era que la cadena no valiese para eidos, era que **le faltaba el punto de entrada**. `activeDir(getter, soma)` pide un `Soma`, y una chart solo tiene un `ActiveEidos`. Y `ActiveEidos.prefs` es un `ActivePrefs` crudo, que no tiene `getDir()` —ese metodo vive en la vista que mira a soma—. Por eso la cadena "no era ejecutable" y por eso alguien puso ahi un `getComputedStyle`. La adaptacion entre las dos superficies es TODA la diferencia: src/uix/eidos/direction.ts → activeEidosDir(getter, prefs) Mismos dos eslabones, mismo `Direction | undefined`, misma cola `'ltr'` en el consumidor. Abre la puerta tambien a `avatar` e `image`, que son el mismo caso. Fuera la lectura del DOM y fuera la copia byte-a-byte que `chart.svelte` tenia del mismo mecanismo —dos fuentes de verdad para un hecho, y ademas el observer vigilaba `<html dir>` mientras la lectura era del elemento, asi que un ancestro que volteara en caliente no se veia nunca—. ⚠️ EL HALLAZGO QUE SOLO SALE MIDIENDO: **estampar `dir` en un `<svg>` no hace nada.** El atributo lo mapea a `direction` la hoja UA de HTML, que no alcanza a los elementos SVG: medido en Chrome, un `<svg dir="ltr">` bajo un ancestro `dir="rtl"` sigue computando `rtl`. Yo lo habia puesto ahi y el comentario afirmaba que servia. Va en el envoltorio HTML (`<figure data-chart-figure>`, `<div data-chart-frame>`). Importa porque `text-anchor` es LOGICO: con el estampado en el nodo equivocado la matematica espeja y las etiquetas no —el fallo de §2 con otro disfraz—. MEDIDO en Chrome, heatmap: LTR `ene/feb/mar` en x=26/90/154, celdas 26‥858; RTL en 851/787/723, celdas 6‥838. Funnel: poligonos de 0‥226 a 193‥420, etiquetas de 249 a 171. El toggle del topbar sigue volteandolas, ahora por prefs y no por el DOM. COSTE ACEPTADO, verificado: envolver una chart en `<div dir="rtl">` sin pasar la prop ya no la voltea. Falla de forma CONSISTENTE —el estampado del envoltorio gana sobre el ancestro, asi que matematica y pintura siguen de acuerdo— y no con las etiquetas cruzando el grafico. §7 del contrato deja de ser una divergencia declarada y pasa a ser la tabla de los dos puntos de entrada. Su justificacion era ademas falsa en dos puntos: no era "el unico sitio del catalogo" (quedan 4 mas, uno vivo en `field-langs-switcher`), y decia que una chart no se podia voltear por subarbol cuando `direction` se HEREDA y si se podia. `check` 77 = linea base. `docs:check` 0/0. Tests de eidos + libs/plots: 424/425, el fallo es `audio-player` sin morfo, preexistente y ajeno. ⚠️ No pude ver los pixeles: el panel del navegador no compone frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The mirroring rules a graphic must obey are worked out per chart in
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
[`src/uix/eidos/components/chart/README.md`](../../src/uix/eidos/components/chart/README.md) §Direction.
---
## 8. Checklist
When you touch a component's direction behaviour:
1. Is `dir?: Direction` declared, using the alias?
feat(direction): la propagacion pertenece a la capa flotante Un panel portalizado se renderiza FUERA del subarbol de su dueño, asi que la herencia no lo alcanza: la afirmacion del dueño solo llega si alguien la lleva. Eso estaba delegado en cada consumidor — siete envoltorios Content componian el enlace al padre a mano — y quien COMPONE un popover no tenia por donde hacerlo. Medido con <DatePicker dir="rtl"> sobre una pagina LTR: el wrapper flotante estampaba dir="ltr", el picker-shell y su pie se disponian en LTR, y solo el calendario espejaba. Panel partido por la mitad, el fallo del §2 del contrato. La capa lo compone ahora una sola vez: el dir propio de la superficie -> el dir afirmado por el proveedor DUEÑO -> omitir - FloatingProviderOpts.dir el dueño baja su afirmacion (ya pasada por activeDir, o sea prop -> prefs) - FloatingContent.assertedDir donde se encuentran; lo leen los DOS estampados del wrapper (rama nativa y rama JS) - createFloatingShellRoot lo reenvia; es la puerta de 8 de los 10 FloatingProvider. Los 2 submenus toman el del menu padre. FloatingContentOpts.dir pasa a ser la prop CRUDA del Content. Resolverla ahi la haria casi siempre concreta (prefs) y el dueño no ganaria nunca el fallback. Se borran los 7 enlaces compuestos a mano con su soma / activeDir / parentRoot. Sobrevive uno a proposito: dropdown-menu-sub-content lo necesita para effectiveSide, que es matematica y quiere valor resuelto. popover, tooltip y link-preview aceptan dir en su raiz. No estampan nada — no renderizan elemento —; el valor existe para cruzar el portal, y es lo que permite que los 9 componentes que componen un popover empujen su direccion al panel. Borrados 9 resolvedDir muertos en los proveedores Content: nadie los leia (toda la matematica direccional consume el de la RAIZ) y el cambio de semantica de opts.dir dejaba su docblock mintiendo. Demos: las 7 que aceptan dir lo pasan ahora a la instancia — time-picker, time-range-picker y color-picker no tenian control ninguno; emoji-picker y natural-time-picker lo tenian solo en el arnes. Sin eso el camino de la PROP, el unico que ejercita el portal, no era observable. Verificado en Chrome real, direccion movida por el control de la demo: date-picker Clear/Cancel/Close 15/102/209 -> 200/92/15 espejo exacto date-range-picker 448/217/17 sin regresion dropdown-menu (raiz y submenu, data-side=left en RTL), context-menu, menubar, select, sidebar (flyout), y tooltip en auto sigue estampando ltr — la cola de prefs sobrevive check 78 = linea base (A/B con stash) · 158/158 en los 20 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/565. Queda documentado en el handoff un defecto PREEXISTENTE de otra familia que esto vuelve visible: un componente eidos que monta otro componente del canon en SITIO no le reenvia el dir, y el hijo estampa ltr cortando la herencia del panel (natural-time-picker/Slider, emoji-picker/Command+ToggleGroup, color-field/Select). No hay precedente de ese reenvio en eidos: es doctrina nueva y movimiento propio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
2. Does the **wrapper** run `activeDir(() => dir, soma)`? (A portalled
`Content` is the exception — it passes the prop raw; §1.)
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
3. Does the MORFO declare `direction` (with `parts` when the paint is not on
the provider root), and does the provider wire `dir: this.opts.dir` — the
raw assertion? (Either half missing does not compile; the census guard
crosses prop and morfo.) Does the provider default once, in `resolvedDir =
resolveDir(this.opts.dir, this.soma)`, and never elsewhere? In-place
feat(direction): eidos comparte el contexto, y el corpus registra el endgame La entrada eidos se parte igual que la de soma: `activeEidosDir(dir)` devuelve la AFIRMACION (prop -> ancestro) y publica en el MISMO DirectionContext — un chart eidos-only dentro de un subarbol soma afirmado (o al reves) resuelve el mismo hecho. La cola matematica va a `resolveEidosDir(dir, eidos.prefs)`, con la vista de prefs cacheada por instancia (WeakMap): construirla dentro de una funcion pura llamada por $derived era una alocacion por pasada. createChartRtl consume las dos mitades por su lado: `attr` estampa la afirmacion cruda (ausente hereda del <html> proyectado), `current`/`anchor` resuelven la cola. Corpus: - direction-contract.md §2 pasa del campo obligatorio de 4.1 al mecanismo del morfo (declaracion -> tipo condicional -> estampado; secundarios del mismo morfo; el censo como guard), §7 documenta el contexto compartido y la tabla de entradas partidas, y el checklist §8.3 pide morfo + cable. - docs/decisions.md registra la ratificacion D1-D4 del 2026-08-05 con las cuatro decisiones y su porque. - CONTINUE-direction-runtime.md §11 cierra el handoff: tabla de fases con commits, verificacion final medida, la cola pospuesta por D4 y las trampas nuevas (pathspec SIEMPRE en rama compartida; el detector de huerfanos y la ruta del import; la vista en $derived). check 77 = linea base · morfo+direction suites 131/131 · docs:check 0/566. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
children need nothing — `DirectionContext` carries the assertion — but a
PROVIDER a component creates directly (every picker's
feat(direction): el eslabon de contexto — la composicion hereda por fisica La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica (2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs. Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del ancestro antes de caer a prefs. Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma, fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL), y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el opts.dir del dueno — que ahora incorpora el contexto. Con la fisica, las convenciones se borran: - los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content x3, color-field-format-select) — el contexto los hace - el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem - los 3 canarios dir= de media-player que la otra sesion dejo puestos a proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no llego al portal") El canario canta: su repro exacto (media-player, manual parts, direction rtl, float de subtitulos) da panel rtl SIN el dir= — y el volume float igual. Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a 0.417 del borde fisico); submenu de dropdown con data-side=left en RTL derivado del contexto. Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper — nunca en constructores de provider. Los tests de provider construyen directo y no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined) cae a traves de prefs. Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un default), sin ancestro cae a prefs, y la publicacion es REACTIVA. check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1 (palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro eslabones y §1 "Composition carries the assertion" documenta la fisica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`PopoverProvider.create`) still receives its `dir` opt: provider creation is
not a component boundary, so no context is published in between.
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
4. Does the recipe branch with `:dir()`? Then is the raw `dir` stamped?
5. Does any block pair a logical anchor with a physical displacement?
(`npm run rtl:check`)
6. Are the SVG, the inline transforms and the animation presets checked **by
eye, in RTL** — the three places the guard cannot see?

Powered by TurnKey Linux.