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/scripts/layer-check.ts

236 lines
8.6 KiB

test(eidos): la capa compartida deja de no tener guard — uno de texto y uno de valor computado La auditoria encontro que `affix` no tenia NINGUNA cobertura automatica: nueve zonas, el remapeo RTL, la regla de stretch y el empate de `position` con `[data-button]` estaban verificados solo a mano. Con dos consumidores y un tercero planificado, eso era el hallazgo estructural. Van dos guards, y la division del trabajo entre ellos es medida, no supuesta. `shared-layer-contract.test.ts` (texto, proyecto node) — nueve zonas cubiertas, `stretch` solo en bordes de bloque, ningun eje leido sin la ranura de override, ninguna regla de geometria enganchada en `data-affix` (la identidad), y el contrato de consumidor: importa la capa, estampa el gancho, no acuna tokens propios, y reafirma `position` si el primitivo que compone ya declara uno. Existe por UNA razon que el navegador no puede cubrir: `getComputedStyle` de `--_affix-safe-inline-start` devuelve `"0px"` en escritorio — medido. La sustitucion ya ocurrio, asi que que `env(safe-area-inset-*)` alimento la ranura se perdio. Un remapeo con una sola mitad volteada se lee igual que uno correcto en toda maquina donde corre CI, y solo aparece en un movil con muesca, apaisado y en RTL. El texto es el unico sitio donde ese emparejamiento se ve. `scripts/layer-check.ts` (navegador, `npm run layer:check`) — para todo elemento con el gancho: `position` es el que la capa promete, la z resuelve, los tokens publicos resuelven, y ninguna ranura declarada inline computa a vacio (la firma exacta de un ciclo de custom property). La lista de consumidores es DERIVADA de quien importa la capa, asi que MenuDial entrara el dia que migre sin tocar nada. Lo que NO comprueba, y esta escrito en su cabecera en vez de tapado: la geometria. La asercion obvia —"el inset anclado no es `auto`"— es INUTIL: `getComputedStyle` da el valor usado, asi que un `auto` se lee como pixeles (medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo). No hay forma a nivel de propiedad de distinguir "324px porque el calc murio" de "324px porque lo pidio el autor". Eso exige comparar contra el bloque contenedor, que exige caminar ancestros. Diferido al tercer consumidor. Ambos guards se verificaron POR MUTACION, porque un test en verde no prueba nada hasta que falla sobre el defecto que dice atrapar: texto M1 media mitad del remapeo RTL -> falla M2 zona borrada -> falla M3 vuelta a un solo nivel de token -> falla M4 puente sin `position` -> falla M5 fab reacuna --fab-z -> falla navegador M6 la capa pierde el position de veras -> falla M7 token de z renombrado -> falla M8 la demo deja de ejercitar la capa -> falla M8 no es teorico: la PRIMERA corrida de `layer:check` dio verde habiendo mirado CERO elementos, porque la demo de fab arrancaba en `placement="static"`, que no estampa gancho. Un guard que aprueba porque no habia nada que mirar es peor que no tenerlo, asi que ese caso es ahora un fallo y el default de la demo pasa a `bottom-end` — "floating" es el nombre del componente y su demo deberia ejercitarlo (la caja del stage tiene `contain: layout`, no se escapa). Y una comprobacion mas que dejo dicha: quitar `position: fixed` del puente de fab NO rompe nada en el orden de carga actual — la capa gana el empate igual. Esa linea es un seguro contra un orden que puede cambiar entre builds, no el arreglo de un estado roto; por eso la cubre el guard de texto y no el de navegador. check 69 = base intacta · smoke fab PASS · layer:check 0 violaciones sobre 2 consumidores · suite 120/120. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/**
* Shared-visual-layer COMPUTED check.
*
* A shared layer is a stylesheet keyed on a hook several components stamp
fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix Defecto de diseno mio, cortado por el autor. Mientras la capa vivio en `components/affix/affix.css`, sus consumidores la importaban con `'../affix/affix.css'`: **un componente dependiendo del directorio de otro**, que es canon prohibido — «un componente no es libreria de otro». `Fab` y `MenuDial` quedaban esclavizados a `Affix` por una ruta, cuando lo unico que comparten es geometria. Va a `eidos/lib/viewport-placement.css`, junto a `lib/list-surface.css`, y los tres consumidores importan de ahi. Ninguno depende de ningun componente. LO QUE HIZO FALTA PARA PODER MOVERLA, que es lo que me faltaba entender. Ya lo intente esta manana y lo revirti porque `recipe-css-contract` fallaba — deje que el guard dictara la arquitectura en vez de preguntarme por que `list-surface` si puede vivir en `lib/`. La respuesta era el diseno entero: **no tiene clave de receta**. Declara sus `--list-*` dentro de su propio CSS, componiendo primitivas que ya son temeables. Aplicado igual: la capa declara `--viewport-placement-offset` / `-z` sobre el propio gancho, compuestos de `var(--space-4)` y `var(--z-index-affix)`. Sin clave en `recipes/base.ts` no hay exigencia de `components/{c}/{c}.css`, y sin esa exigencia no hay acoplamiento. El retoque ademas queda a la altura correcta: un tema mueve la escala de espacio y la escalera de z, no un alias por componente. Renombres que arrastra, todos hacia nombres de CAPA y ninguno hacia un componente: `--affix-offset`/`-z` -> `--viewport-placement-offset`/`-z`, sus ranuras `--_affix-*` -> `--_viewport-placement-*`, y las dos customs del remapeo de safe-area. La clave `affix` sale de `recipes/base.ts` y sus dos tokens del `:root` generado. Sin cambio de comportamiento, medido en las tres rutas: `Fab` sigue en `fixed`, z 100 (la capa ofrece 150 y su puente la baja por la ranura — los dos niveles haciendo su trabajo) y a 16px de sus dos bordes; `Affix` en sus nueve zonas exactas con z 150; y su elemento lleva solo `data-affix`, la identidad. check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 · layer:check 0/3 · suite eidos 123/123 · rtl 0/178 · docs:check 0/627 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* (`lib/viewport-placement.css` → `data-viewport-placement`). Its failure modes are
test(eidos): la capa compartida deja de no tener guard — uno de texto y uno de valor computado La auditoria encontro que `affix` no tenia NINGUNA cobertura automatica: nueve zonas, el remapeo RTL, la regla de stretch y el empate de `position` con `[data-button]` estaban verificados solo a mano. Con dos consumidores y un tercero planificado, eso era el hallazgo estructural. Van dos guards, y la division del trabajo entre ellos es medida, no supuesta. `shared-layer-contract.test.ts` (texto, proyecto node) — nueve zonas cubiertas, `stretch` solo en bordes de bloque, ningun eje leido sin la ranura de override, ninguna regla de geometria enganchada en `data-affix` (la identidad), y el contrato de consumidor: importa la capa, estampa el gancho, no acuna tokens propios, y reafirma `position` si el primitivo que compone ya declara uno. Existe por UNA razon que el navegador no puede cubrir: `getComputedStyle` de `--_affix-safe-inline-start` devuelve `"0px"` en escritorio — medido. La sustitucion ya ocurrio, asi que que `env(safe-area-inset-*)` alimento la ranura se perdio. Un remapeo con una sola mitad volteada se lee igual que uno correcto en toda maquina donde corre CI, y solo aparece en un movil con muesca, apaisado y en RTL. El texto es el unico sitio donde ese emparejamiento se ve. `scripts/layer-check.ts` (navegador, `npm run layer:check`) — para todo elemento con el gancho: `position` es el que la capa promete, la z resuelve, los tokens publicos resuelven, y ninguna ranura declarada inline computa a vacio (la firma exacta de un ciclo de custom property). La lista de consumidores es DERIVADA de quien importa la capa, asi que MenuDial entrara el dia que migre sin tocar nada. Lo que NO comprueba, y esta escrito en su cabecera en vez de tapado: la geometria. La asercion obvia —"el inset anclado no es `auto`"— es INUTIL: `getComputedStyle` da el valor usado, asi que un `auto` se lee como pixeles (medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo). No hay forma a nivel de propiedad de distinguir "324px porque el calc murio" de "324px porque lo pidio el autor". Eso exige comparar contra el bloque contenedor, que exige caminar ancestros. Diferido al tercer consumidor. Ambos guards se verificaron POR MUTACION, porque un test en verde no prueba nada hasta que falla sobre el defecto que dice atrapar: texto M1 media mitad del remapeo RTL -> falla M2 zona borrada -> falla M3 vuelta a un solo nivel de token -> falla M4 puente sin `position` -> falla M5 fab reacuna --fab-z -> falla navegador M6 la capa pierde el position de veras -> falla M7 token de z renombrado -> falla M8 la demo deja de ejercitar la capa -> falla M8 no es teorico: la PRIMERA corrida de `layer:check` dio verde habiendo mirado CERO elementos, porque la demo de fab arrancaba en `placement="static"`, que no estampa gancho. Un guard que aprueba porque no habia nada que mirar es peor que no tenerlo, asi que ese caso es ahora un fallo y el default de la demo pasa a `bottom-end` — "floating" es el nombre del componente y su demo deberia ejercitarlo (la caja del stage tiene `contain: layout`, no se escapa). Y una comprobacion mas que dejo dicha: quitar `position: fixed` del puente de fab NO rompe nada en el orden de carga actual — la capa gana el empate igual. Esa linea es un seguro contra un orden que puede cambiar entre builds, no el arreglo de un estado roto; por eso la cubre el guard de texto y no el de navegador. check 69 = base intacta · smoke fab PASS · layer:check 0 violaciones sobre 2 consumidores · suite 120/120. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* all computed-value ones, and NONE of them is visible to a text lint:
*
* - a specificity tie lost to a composed primitive (`button.css` declares
* `position: relative` on `[data-button]` at the same 0,1,0 as the layer's
* base rule, and both files are code-split, so source order decides);
* - a `revert-layer` from another recipe rolling the property back to the UA
* value (`box.css` does this for `position` and all four insets);
* - a custom-property CYCLE, which CSS resolves to guaranteed-invalid, killing
* the `calc()` that consumes it.
*
* Types were green for every one of those. This script is the eye.
*
* WHAT IT DELIBERATELY DOES NOT CHECK — and the reason is measured, not assumed.
* The obvious assertion ("the anchored inset is not `auto`") is USELESS:
* `getComputedStyle` reports the USED value for a positioned element, so an
* `inset: auto` reads back as a pixel number (measured: `-1976.7px` on a probe,
* and `324px` on the real cycle failure). There is no property-level way to tell
* "324px because the calc died" from "324px because the author asked for it".
* Discriminating that needs GEOMETRY — the element's edge against its containing
* block's — which in turn needs an ancestor walk for `transform` / `filter` /
* `contain` / `will-change`. Deferred until the layer has a third consumer; the
* hole is stated here rather than papered over with an assertion that passes on
* the broken case.
*
* The consumer list is DERIVED, not configured: any eidos component whose
* wrapper imports the layer is checked. A component joins the day it migrates.
*
* Exit codes:
* 0 — every element carrying a layer hook computes its contract.
* 1 — at least one violation.
* 2 — no dev server / no consumers found.
*/
import { chromium } from 'playwright';
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = join(HERE, '..');
const EIDOS_COMPONENTS = join(REPO, 'src/uix/eidos/components');
/** The layers this script knows how to assert, and what their hook promises. */
const LAYERS = [
{
name: 'affix',
/** The component that OWNS the layer — its wrapper imports it relatively. */
owner: 'affix',
/** Matches a consumer wrapper that imports the layer stylesheet. */
fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix Defecto de diseno mio, cortado por el autor. Mientras la capa vivio en `components/affix/affix.css`, sus consumidores la importaban con `'../affix/affix.css'`: **un componente dependiendo del directorio de otro**, que es canon prohibido — «un componente no es libreria de otro». `Fab` y `MenuDial` quedaban esclavizados a `Affix` por una ruta, cuando lo unico que comparten es geometria. Va a `eidos/lib/viewport-placement.css`, junto a `lib/list-surface.css`, y los tres consumidores importan de ahi. Ninguno depende de ningun componente. LO QUE HIZO FALTA PARA PODER MOVERLA, que es lo que me faltaba entender. Ya lo intente esta manana y lo revirti porque `recipe-css-contract` fallaba — deje que el guard dictara la arquitectura en vez de preguntarme por que `list-surface` si puede vivir en `lib/`. La respuesta era el diseno entero: **no tiene clave de receta**. Declara sus `--list-*` dentro de su propio CSS, componiendo primitivas que ya son temeables. Aplicado igual: la capa declara `--viewport-placement-offset` / `-z` sobre el propio gancho, compuestos de `var(--space-4)` y `var(--z-index-affix)`. Sin clave en `recipes/base.ts` no hay exigencia de `components/{c}/{c}.css`, y sin esa exigencia no hay acoplamiento. El retoque ademas queda a la altura correcta: un tema mueve la escala de espacio y la escalera de z, no un alias por componente. Renombres que arrastra, todos hacia nombres de CAPA y ninguno hacia un componente: `--affix-offset`/`-z` -> `--viewport-placement-offset`/`-z`, sus ranuras `--_affix-*` -> `--_viewport-placement-*`, y las dos customs del remapeo de safe-area. La clave `affix` sale de `recipes/base.ts` y sus dos tokens del `:root` generado. Sin cambio de comportamiento, medido en las tres rutas: `Fab` sigue en `fixed`, z 100 (la capa ofrece 150 y su puente la baja por la ranura — los dos niveles haciendo su trabajo) y a 16px de sus dos bordes; `Affix` en sus nueve zonas exactas con z 150; y su elemento lleva solo `data-affix`, la identidad. check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 · layer:check 0/3 · suite eidos 123/123 · rtl 0/178 · docs:check 0/627 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
importRe: /import ['"][^'"]*viewport-placement\.css['"]/,
fix(morfo): affix deja de declarar lo que solo lee una capa, y el gancho pasa a nombre de CAPA Correccion doctrinal, senalada por el autor: **morfo es la capa declarativa ENTRE capas**. Un atributo que consume una sola no es contrato. `affixMorfo` declaraba `data-affix-placement` y `data-affix-stretch`. Los lee UNA: el CSS. No debian estar ahi — y la doctrina nombra la familia exacta (`morfo.md` §`undeclaredState`: «the escape hatch for attrs OUTSIDE the contract — a visual wrapper's `data-size`, a presentation flag like `data-sheet`»). Los declare porque `morfo-check` los exigia, que es la herramienta dictando la doctrina: el guard clasifica por PREFIJO DE NOMBRE, la doctrina clasifica por NATURALEZA, y las dos solo chocaban porque el gancho llevaba el nombre del componente. Y no era su nombre. Lo estampan tres —`Affix`, `Fab`, `MenuDial`—, asi que no es de ninguno: pasa a **`data-viewport-placement`** / `data-viewport-stretch`. La colision con el guard desaparece por construccion, sin excepcion que escribir. Es el mismo error que `--fab-offset` leido por la capa: nombrar por un participante algo que es de todos. `affixMorfo` se queda con lo que si es contrato: la identidad `data-affix`, que es como el DOM dice «esta caja es un Affix» a quien pregunte. TAMBIEN INTENTADO Y REVERTIDO, con su motivo, para que nadie lo reintente: mover el fichero a `eidos/lib/viewport-placement.css` junto a `list-surface.css`. `recipe-css-contract` lo tumbo y tenia razon — **el sistema de tokens esta indexado por componente**: toda clave de `recipes/base.ts` exige su `components/{c}/{c}.css`. `list-surface` puede vivir en `lib/` porque NO tiene clave de receta (sus `--list-*` viven dentro de su propio CSS, por `data-size`, no como defaults temeables en `:root`); los nuestros si lo son. La parte portante —«esto no es de nadie»— la lleva el nombre del gancho, no la carpeta. Queda escrito en la excepcion E-2.2 del README y en el PLAN. Sin cambio de comportamiento: 68 sustituciones de nombre, mismas reglas, mismos valores. Verificado en las tres rutas. check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 · morfo:check affix PASS · layer:check 0/3 · contrato de capa 13/13 · recipe-css-contract + api-contract 50/50 · rtl 0/178 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
hook: 'data-viewport-placement',
test(eidos): la capa compartida deja de no tener guard — uno de texto y uno de valor computado La auditoria encontro que `affix` no tenia NINGUNA cobertura automatica: nueve zonas, el remapeo RTL, la regla de stretch y el empate de `position` con `[data-button]` estaban verificados solo a mano. Con dos consumidores y un tercero planificado, eso era el hallazgo estructural. Van dos guards, y la division del trabajo entre ellos es medida, no supuesta. `shared-layer-contract.test.ts` (texto, proyecto node) — nueve zonas cubiertas, `stretch` solo en bordes de bloque, ningun eje leido sin la ranura de override, ninguna regla de geometria enganchada en `data-affix` (la identidad), y el contrato de consumidor: importa la capa, estampa el gancho, no acuna tokens propios, y reafirma `position` si el primitivo que compone ya declara uno. Existe por UNA razon que el navegador no puede cubrir: `getComputedStyle` de `--_affix-safe-inline-start` devuelve `"0px"` en escritorio — medido. La sustitucion ya ocurrio, asi que que `env(safe-area-inset-*)` alimento la ranura se perdio. Un remapeo con una sola mitad volteada se lee igual que uno correcto en toda maquina donde corre CI, y solo aparece en un movil con muesca, apaisado y en RTL. El texto es el unico sitio donde ese emparejamiento se ve. `scripts/layer-check.ts` (navegador, `npm run layer:check`) — para todo elemento con el gancho: `position` es el que la capa promete, la z resuelve, los tokens publicos resuelven, y ninguna ranura declarada inline computa a vacio (la firma exacta de un ciclo de custom property). La lista de consumidores es DERIVADA de quien importa la capa, asi que MenuDial entrara el dia que migre sin tocar nada. Lo que NO comprueba, y esta escrito en su cabecera en vez de tapado: la geometria. La asercion obvia —"el inset anclado no es `auto`"— es INUTIL: `getComputedStyle` da el valor usado, asi que un `auto` se lee como pixeles (medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo). No hay forma a nivel de propiedad de distinguir "324px porque el calc murio" de "324px porque lo pidio el autor". Eso exige comparar contra el bloque contenedor, que exige caminar ancestros. Diferido al tercer consumidor. Ambos guards se verificaron POR MUTACION, porque un test en verde no prueba nada hasta que falla sobre el defecto que dice atrapar: texto M1 media mitad del remapeo RTL -> falla M2 zona borrada -> falla M3 vuelta a un solo nivel de token -> falla M4 puente sin `position` -> falla M5 fab reacuna --fab-z -> falla navegador M6 la capa pierde el position de veras -> falla M7 token de z renombrado -> falla M8 la demo deja de ejercitar la capa -> falla M8 no es teorico: la PRIMERA corrida de `layer:check` dio verde habiendo mirado CERO elementos, porque la demo de fab arrancaba en `placement="static"`, que no estampa gancho. Un guard que aprueba porque no habia nada que mirar es peor que no tenerlo, asi que ese caso es ahora un fallo y el default de la demo pasa a `bottom-end` — "floating" es el nombre del componente y su demo deberia ejercitarlo (la caja del stage tiene `contain: layout`, no se escapa). Y una comprobacion mas que dejo dicha: quitar `position: fixed` del puente de fab NO rompe nada en el orden de carga actual — la capa gana el empate igual. Esa linea es un seguro contra un orden que puede cambiar entre builds, no el arreglo de un estado roto; por eso la cubre el guard de texto y no el de navegador. check 69 = base intacta · smoke fab PASS · layer:check 0 violaciones sobre 2 consumidores · suite 120/120. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/** Public token that must always resolve — empty means invalid or renamed. */
fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix Defecto de diseno mio, cortado por el autor. Mientras la capa vivio en `components/affix/affix.css`, sus consumidores la importaban con `'../affix/affix.css'`: **un componente dependiendo del directorio de otro**, que es canon prohibido — «un componente no es libreria de otro». `Fab` y `MenuDial` quedaban esclavizados a `Affix` por una ruta, cuando lo unico que comparten es geometria. Va a `eidos/lib/viewport-placement.css`, junto a `lib/list-surface.css`, y los tres consumidores importan de ahi. Ninguno depende de ningun componente. LO QUE HIZO FALTA PARA PODER MOVERLA, que es lo que me faltaba entender. Ya lo intente esta manana y lo revirti porque `recipe-css-contract` fallaba — deje que el guard dictara la arquitectura en vez de preguntarme por que `list-surface` si puede vivir en `lib/`. La respuesta era el diseno entero: **no tiene clave de receta**. Declara sus `--list-*` dentro de su propio CSS, componiendo primitivas que ya son temeables. Aplicado igual: la capa declara `--viewport-placement-offset` / `-z` sobre el propio gancho, compuestos de `var(--space-4)` y `var(--z-index-affix)`. Sin clave en `recipes/base.ts` no hay exigencia de `components/{c}/{c}.css`, y sin esa exigencia no hay acoplamiento. El retoque ademas queda a la altura correcta: un tema mueve la escala de espacio y la escalera de z, no un alias por componente. Renombres que arrastra, todos hacia nombres de CAPA y ninguno hacia un componente: `--affix-offset`/`-z` -> `--viewport-placement-offset`/`-z`, sus ranuras `--_affix-*` -> `--_viewport-placement-*`, y las dos customs del remapeo de safe-area. La clave `affix` sale de `recipes/base.ts` y sus dos tokens del `:root` generado. Sin cambio de comportamiento, medido en las tres rutas: `Fab` sigue en `fixed`, z 100 (la capa ofrece 150 y su puente la baja por la ranura — los dos niveles haciendo su trabajo) y a 16px de sus dos bordes; `Affix` en sus nueve zonas exactas con z 150; y su elemento lleva solo `data-affix`, la identidad. check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 · layer:check 0/3 · suite eidos 123/123 · rtl 0/178 · docs:check 0/627 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
publicTokens: ['--viewport-placement-offset', '--viewport-placement-z'],
test(eidos): la capa compartida deja de no tener guard — uno de texto y uno de valor computado La auditoria encontro que `affix` no tenia NINGUNA cobertura automatica: nueve zonas, el remapeo RTL, la regla de stretch y el empate de `position` con `[data-button]` estaban verificados solo a mano. Con dos consumidores y un tercero planificado, eso era el hallazgo estructural. Van dos guards, y la division del trabajo entre ellos es medida, no supuesta. `shared-layer-contract.test.ts` (texto, proyecto node) — nueve zonas cubiertas, `stretch` solo en bordes de bloque, ningun eje leido sin la ranura de override, ninguna regla de geometria enganchada en `data-affix` (la identidad), y el contrato de consumidor: importa la capa, estampa el gancho, no acuna tokens propios, y reafirma `position` si el primitivo que compone ya declara uno. Existe por UNA razon que el navegador no puede cubrir: `getComputedStyle` de `--_affix-safe-inline-start` devuelve `"0px"` en escritorio — medido. La sustitucion ya ocurrio, asi que que `env(safe-area-inset-*)` alimento la ranura se perdio. Un remapeo con una sola mitad volteada se lee igual que uno correcto en toda maquina donde corre CI, y solo aparece en un movil con muesca, apaisado y en RTL. El texto es el unico sitio donde ese emparejamiento se ve. `scripts/layer-check.ts` (navegador, `npm run layer:check`) — para todo elemento con el gancho: `position` es el que la capa promete, la z resuelve, los tokens publicos resuelven, y ninguna ranura declarada inline computa a vacio (la firma exacta de un ciclo de custom property). La lista de consumidores es DERIVADA de quien importa la capa, asi que MenuDial entrara el dia que migre sin tocar nada. Lo que NO comprueba, y esta escrito en su cabecera en vez de tapado: la geometria. La asercion obvia —"el inset anclado no es `auto`"— es INUTIL: `getComputedStyle` da el valor usado, asi que un `auto` se lee como pixeles (medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo). No hay forma a nivel de propiedad de distinguir "324px porque el calc murio" de "324px porque lo pidio el autor". Eso exige comparar contra el bloque contenedor, que exige caminar ancestros. Diferido al tercer consumidor. Ambos guards se verificaron POR MUTACION, porque un test en verde no prueba nada hasta que falla sobre el defecto que dice atrapar: texto M1 media mitad del remapeo RTL -> falla M2 zona borrada -> falla M3 vuelta a un solo nivel de token -> falla M4 puente sin `position` -> falla M5 fab reacuna --fab-z -> falla navegador M6 la capa pierde el position de veras -> falla M7 token de z renombrado -> falla M8 la demo deja de ejercitar la capa -> falla M8 no es teorico: la PRIMERA corrida de `layer:check` dio verde habiendo mirado CERO elementos, porque la demo de fab arrancaba en `placement="static"`, que no estampa gancho. Un guard que aprueba porque no habia nada que mirar es peor que no tenerlo, asi que ese caso es ahora un fallo y el default de la demo pasa a `bottom-end` — "floating" es el nombre del componente y su demo deberia ejercitarlo (la caja del stage tiene `contain: layout`, no se escapa). Y una comprobacion mas que dejo dicha: quitar `position: fixed` del puente de fab NO rompe nada en el orden de carga actual — la capa gana el empate igual. Esa linea es un seguro contra un orden que puede cambiar entre builds, no el arreglo de un estado roto; por eso la cubre el guard de texto y no el de navegador. check 69 = base intacta · smoke fab PASS · layer:check 0 violaciones sobre 2 consumidores · suite 120/120. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/** Per-instance override slot the wrappers write. */
fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix Defecto de diseno mio, cortado por el autor. Mientras la capa vivio en `components/affix/affix.css`, sus consumidores la importaban con `'../affix/affix.css'`: **un componente dependiendo del directorio de otro**, que es canon prohibido — «un componente no es libreria de otro». `Fab` y `MenuDial` quedaban esclavizados a `Affix` por una ruta, cuando lo unico que comparten es geometria. Va a `eidos/lib/viewport-placement.css`, junto a `lib/list-surface.css`, y los tres consumidores importan de ahi. Ninguno depende de ningun componente. LO QUE HIZO FALTA PARA PODER MOVERLA, que es lo que me faltaba entender. Ya lo intente esta manana y lo revirti porque `recipe-css-contract` fallaba — deje que el guard dictara la arquitectura en vez de preguntarme por que `list-surface` si puede vivir en `lib/`. La respuesta era el diseno entero: **no tiene clave de receta**. Declara sus `--list-*` dentro de su propio CSS, componiendo primitivas que ya son temeables. Aplicado igual: la capa declara `--viewport-placement-offset` / `-z` sobre el propio gancho, compuestos de `var(--space-4)` y `var(--z-index-affix)`. Sin clave en `recipes/base.ts` no hay exigencia de `components/{c}/{c}.css`, y sin esa exigencia no hay acoplamiento. El retoque ademas queda a la altura correcta: un tema mueve la escala de espacio y la escalera de z, no un alias por componente. Renombres que arrastra, todos hacia nombres de CAPA y ninguno hacia un componente: `--affix-offset`/`-z` -> `--viewport-placement-offset`/`-z`, sus ranuras `--_affix-*` -> `--_viewport-placement-*`, y las dos customs del remapeo de safe-area. La clave `affix` sale de `recipes/base.ts` y sus dos tokens del `:root` generado. Sin cambio de comportamiento, medido en las tres rutas: `Fab` sigue en `fixed`, z 100 (la capa ofrece 150 y su puente la baja por la ranura — los dos niveles haciendo su trabajo) y a 16px de sus dos bordes; `Affix` en sus nueve zonas exactas con z 150; y su elemento lleva solo `data-affix`, la identidad. check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 · layer:check 0/3 · suite eidos 123/123 · rtl 0/178 · docs:check 0/627 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
overrideSlots: ['--_viewport-placement-offset', '--_viewport-placement-z'],
test(eidos): la capa compartida deja de no tener guard — uno de texto y uno de valor computado La auditoria encontro que `affix` no tenia NINGUNA cobertura automatica: nueve zonas, el remapeo RTL, la regla de stretch y el empate de `position` con `[data-button]` estaban verificados solo a mano. Con dos consumidores y un tercero planificado, eso era el hallazgo estructural. Van dos guards, y la division del trabajo entre ellos es medida, no supuesta. `shared-layer-contract.test.ts` (texto, proyecto node) — nueve zonas cubiertas, `stretch` solo en bordes de bloque, ningun eje leido sin la ranura de override, ninguna regla de geometria enganchada en `data-affix` (la identidad), y el contrato de consumidor: importa la capa, estampa el gancho, no acuna tokens propios, y reafirma `position` si el primitivo que compone ya declara uno. Existe por UNA razon que el navegador no puede cubrir: `getComputedStyle` de `--_affix-safe-inline-start` devuelve `"0px"` en escritorio — medido. La sustitucion ya ocurrio, asi que que `env(safe-area-inset-*)` alimento la ranura se perdio. Un remapeo con una sola mitad volteada se lee igual que uno correcto en toda maquina donde corre CI, y solo aparece en un movil con muesca, apaisado y en RTL. El texto es el unico sitio donde ese emparejamiento se ve. `scripts/layer-check.ts` (navegador, `npm run layer:check`) — para todo elemento con el gancho: `position` es el que la capa promete, la z resuelve, los tokens publicos resuelven, y ninguna ranura declarada inline computa a vacio (la firma exacta de un ciclo de custom property). La lista de consumidores es DERIVADA de quien importa la capa, asi que MenuDial entrara el dia que migre sin tocar nada. Lo que NO comprueba, y esta escrito en su cabecera en vez de tapado: la geometria. La asercion obvia —"el inset anclado no es `auto`"— es INUTIL: `getComputedStyle` da el valor usado, asi que un `auto` se lee como pixeles (medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo). No hay forma a nivel de propiedad de distinguir "324px porque el calc murio" de "324px porque lo pidio el autor". Eso exige comparar contra el bloque contenedor, que exige caminar ancestros. Diferido al tercer consumidor. Ambos guards se verificaron POR MUTACION, porque un test en verde no prueba nada hasta que falla sobre el defecto que dice atrapar: texto M1 media mitad del remapeo RTL -> falla M2 zona borrada -> falla M3 vuelta a un solo nivel de token -> falla M4 puente sin `position` -> falla M5 fab reacuna --fab-z -> falla navegador M6 la capa pierde el position de veras -> falla M7 token de z renombrado -> falla M8 la demo deja de ejercitar la capa -> falla M8 no es teorico: la PRIMERA corrida de `layer:check` dio verde habiendo mirado CERO elementos, porque la demo de fab arrancaba en `placement="static"`, que no estampa gancho. Un guard que aprueba porque no habia nada que mirar es peor que no tenerlo, asi que ese caso es ahora un fallo y el default de la demo pasa a `bottom-end` — "floating" es el nombre del componente y su demo deberia ejercitarlo (la caja del stage tiene `contain: layout`, no se escapa). Y una comprobacion mas que dejo dicha: quitar `position: fixed` del puente de fab NO rompe nada en el orden de carga actual — la capa gana el empate igual. Esa linea es un seguro contra un orden que puede cambiar entre builds, no el arreglo de un estado roto; por eso la cubre el guard de texto y no el de navegador. check 69 = base intacta · smoke fab PASS · layer:check 0 violaciones sobre 2 consumidores · suite 120/120. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
expectedPosition: 'fixed'
}
] as const;
/**
* Eidos component dirs whose wrapper imports the layer, plus the layer's OWNER
* (whose wrapper imports it relatively, so the shared regex misses it — and
* missing it meant the layer's own demo went unchecked on the first run).
*/
function consumersOf(importRe: RegExp, owner: string): string[] {
if (!existsSync(EIDOS_COMPONENTS)) return [];
const out: string[] = [];
for (const dir of readdirSync(EIDOS_COMPONENTS)) {
const componentDir = join(EIDOS_COMPONENTS, dir);
let files: string[];
try {
files = readdirSync(componentDir).filter((f) => f.endsWith('.svelte'));
} catch {
continue;
}
const imports = files.some((f) => importRe.test(readFileSync(join(componentDir, f), 'utf8')));
if (imports) out.push(dir);
}
return out.includes(owner) ? out : [owner, ...out];
}
async function probePort(startPort: number, endPort: number): Promise<string | null> {
for (let port = startPort; port <= endPort; port++) {
try {
const res = await fetch(`http://localhost:${port}/`, {
signal: AbortSignal.timeout(500)
});
if (res.status < 500) return `http://localhost:${port}`;
} catch {
/* next */
}
}
return null;
}
type Violation = { route: string; message: string };
const BASE = process.argv[2] ?? (await probePort(5173, 5180));
if (!BASE) {
console.error('Could not find a running dev server on 5173-5180.');
console.error('Start one with `npm run dev`, or pass a base URL as the first argument.');
process.exit(2);
}
console.error(`Using dev server at ${BASE}`);
const browser = await chromium.launch();
const ctx = await browser.newContext();
const violations: Violation[] = [];
let checkedElements = 0;
let checkedRoutes = 0;
for (const layer of LAYERS) {
const consumers = consumersOf(layer.importRe, layer.owner);
if (consumers.length === 0) {
console.log(`SKIP ${layer.name}: no component imports the layer`);
continue;
}
console.log(`Layer ${layer.name} — ${consumers.length} consumer(s): ${consumers.join(', ')}`);
for (const consumer of consumers) {
const route = `/uix/components/${consumer}`;
const page = await ctx.newPage();
try {
await page.goto(BASE + route, { waitUntil: 'networkidle', timeout: 20000 });
await page.waitForTimeout(500);
checkedRoutes += 1;
const found = await page.$$eval(
`[${layer.hook}]`,
(nodes, cfg) =>
nodes.map((el) => {
const cs = getComputedStyle(el);
return {
hookValue: el.getAttribute(cfg.hook),
position: cs.position,
zIndex: cs.zIndex,
publics: cfg.publicTokens.map((t) => [t, cs.getPropertyValue(t).trim()]),
// An inline declaration that computes to nothing is the exact
// signature of a custom-property cycle.
deadSlots: cfg.overrideSlots.filter(
(slot) =>
(el as HTMLElement).style.getPropertyValue(slot).trim() !== '' &&
cs.getPropertyValue(slot).trim() === ''
)
};
}),
{
hook: layer.hook,
publicTokens: [...layer.publicTokens],
overrideSlots: [...layer.overrideSlots]
}
);
if (found.length === 0) {
// NOT a warning. A demo whose default state never stamps the hook
// leaves this consumer unchecked, and the run would go green having
// looked at nothing — which is exactly what happened the first time
// this script ran (fab's demo defaulted to `placement="static"`).
violations.push({
route,
message: `no element stamps ${layer.hook} — the demo's default state does not exercise the layer, so this consumer is UNCHECKED`
});
console.log(`FAIL ${consumer.padEnd(20)} ${route}`);
console.log(` ${violations[violations.length - 1].message}`);
continue;
}
for (const el of found) {
checkedElements += 1;
const where = `${layer.hook}="${el.hookValue}"`;
if (el.position !== layer.expectedPosition) {
violations.push({
route,
message: `${where}: position is "${el.position}", expected "${layer.expectedPosition}" — the layer lost a cascade fight (a composed primitive's own position, or a revert-layer)`
});
}
if (el.zIndex === 'auto') {
violations.push({
route,
message: `${where}: z-index computes to "auto" — the stacking token did not resolve`
});
}
for (const [token, value] of el.publics) {
if (value === '') {
violations.push({
route,
message: `${where}: public token ${token} resolves to nothing (renamed, or invalid)`
});
}
}
for (const slot of el.deadSlots) {
violations.push({
route,
message: `${where}: ${slot} is declared inline but computes to nothing — a custom-property CYCLE; the calc() that consumes it is dead and the inset fell back silently`
});
}
}
const bad = violations.filter((v) => v.route === route);
if (bad.length === 0) {
console.log(`PASS ${consumer.padEnd(20)} ${route} — ${found.length} element(s)`);
} else {
console.log(`FAIL ${consumer.padEnd(20)} ${route}`);
bad.forEach((v) => console.log(` ${v.message}`));
}
} catch (e) {
console.log(`ERROR ${consumer.padEnd(20)} ${route} ${(e as Error).message}`);
violations.push({ route, message: `navigation error: ${(e as Error).message}` });
} finally {
await page.close();
}
}
}
await browser.close();
console.log('');
if (checkedRoutes === 0) {
console.log('No layer consumers with a routed demo.');
process.exit(2);
} else if (violations.length === 0) {
console.log(
`layer-check: 0 violation(s) across ${checkedElements} element(s) on ${checkedRoutes} route(s)`
);
process.exit(0);
} else {
console.log(`layer-check: ${violations.length} violation(s) across ${checkedRoutes} route(s)`);
process.exit(1);
}

Powered by TurnKey Linux.