13 KiB
CONTINUE — Affix, la capa de colocación, y el eje que abrió: los knobs visuales
Kickoff: «Lee
docs/process/CONTINUE-affix.mdy sigue por §3.» Fecha: 2026-08-15 (cierre de jornada) · Ramaalpha-0.1-dir-prefs, pusheada en65536ae6c— limpia contra el remoto, 0 por delante. ⚠️ RAMA COMPARTIDA, y hoy se notó: otra sesión trabaja enblocksy su commit948f7cf7ees la punta actual. En el árbol quedan 23 ficheros suyos sin commitear (src/uix/blocks/*,docs/process/*blocks*,scripts/blocks-check.ts). Stagear por lista explícita, nuncagit addde un directorio: hoy ungit add docs/se llevó cinco ficheros ajenos. Doctrina viva:architecture/morfo.md§What morfo does NOT contain + §BacklogundeclaredState·architecture/eidos.md·theming/reference.md§39 ·canon/vocabularies.md§Placement grids (2 × 9). El relato completo del ejeAffixestá enPLAN-affix.md§6 y §7 — este documento no lo repite.
0 · Dónde estás
Catorce commits publicados en la jornada; trece son de este eje (el
catorceavo, 948f7cf7e, es de la sesión de blocks — se publicó con el push
porque la rama es una sola).
| Commit | Qué |
|---|---|
92c8f6466 |
Affix — la pieza que Sticky no puede ser, y la capa que Fab y MenuDial ya habían copiado dos veces |
1fc0a7a96 |
Fab deja su copia privada de la colocación y lee la capa |
c945be23b |
breaking — un eje de capa = un token público + una ranura; --fab-offset / --fab-z retirados sin shim |
acb7f0b41 |
la capa deja de no tener guard: uno de texto y uno de valor computado |
4b39b718c |
el aviso al pie se lee donde se ve |
c53efc602 |
MenuDial lee la colocación de la capa — la última copia privada, borrada |
ee41b691d |
los 20px del MenuDial son una decisión, no una deriva |
7ea913ab1 |
la rejilla lógica se canoniza — cinco uniones a mano pasan a una; cierra EID-3 |
a11805d43 |
la doctrina alcanza a la jornada |
de0615caf |
affix deja de declarar lo que sólo lee una capa; el gancho pasa a nombre de CAPA |
b72a56491 |
la capa sale de components/ — tenerla ahí acoplaba Fab y MenuDial a Affix |
998d72191 |
los 25 knobs visuales salen del contrato — comentados, no borrados |
65536ae6c |
el enum que salió del morfo vuelve, leído de la unión de TypeScript (§2), y este handoff |
1 · El eje Affix: CERRADO
Está entero en PLAN-affix.md §6 (lo construido vs lo planeado)
y §7 (lo que vino después: el peldaño --z-index-affix: 150, el patrón de dos
tokens por eje, la canonización de la rejilla, los dos guards, y las dos
correcciones del autor). Lo único que sigue abierto ahí:
- Test de geometría de la capa — diferido al tercer consumidor. El tercero ya
existe (
Affix,Fab,MenuDial), así que la condición se cumplió. El hueco y por qué ninguna aserción a nivel de propiedad lo cubre está escrito en la cabecera descripts/layer-check.ts:getComputedStyleda el valor USADO, y uninset: autose lee como píxeles (medido:-1976.7pxen una sonda,324pxen el fallo real del ciclo). Discriminarlo exige geometría — el borde contra su bloque contenedor —, y eso exige recorrer los ancestros buscandotransform/filter/contain/will-change.
2 · El eje que abrió: los knobs visuales fuera del contrato
No estaba previsto. Nació de la corrección de de0615caf (§7 del plan): affix
declaraba dos attrs que lee una sola capa. Al aplicarle la misma vara al
resto del árbol salieron 25 declaraciones más, en seis morfos.
La sentencia del autor, literal
«la doctrina prevalece, porque no tiene sentido declarar atributos que sólo se usan en una capa, morfo funciona como una capa declarativa entre capas, eso es el diseño.»
Y el criterio de retirada:
«no los retires, déjalos comentados y con la explicación del porqué.»
Lo aplicado (998d72191)
25 entradas comentadas en sitio — avatar 9 · s-text 6 · image 5 · avatar-group 2
· skeleton 2 · qr-code 1 —, cada bloque citando tres fuentes de doctrina
(morfo.md §What morfo does NOT contain, morfo.md §Backlog undeclaredState,
theming/reference.md §39). Comentadas y no borradas porque son el registro de
lo que el morfo prometía y de los enums que un guard podría volver a leer.
De todo lo que declaraban esos seis morfos, sobrevive sólo data-status
(avatar ×3, image ×4): lo mueve ImageProvider desde $soma/layers, así que
sí cruza la frontera de capa. Ese es el criterio, y se sostiene solo.
Lo que se perdió al comentarlos, y se ha devuelto hoy
Un values: [...] en el morfo le daba a eidos-lint con qué rechazar
[data-size='huge']. Al salir, esa validación se quedó sin fuente. Se ha
devuelto leyendo la unión de TypeScript, que es donde el enum vive de verdad.
No en eidos-lint: en recipe-css-contract.test.ts, donde el mecanismo ya
existía para variant desde antes (línea ~790, «Variant canon enforcement»).
Lo hecho es extenderlo, no inventarlo:
- el colector de valores CSS pasa a tomar el attr por parámetro;
- un extractor nuevo entiende
Extract<Base, 'a' | 'b'>— donde viven cinco de los ejes (AvatarSize,ImageSize,SkeletonSize,QrCodeSize,AvatarBadgePosition) — y el miembrobooleandeAvatarRing; - 11 ejes en la tabla
KNOB_AXES, inspeccionando 44 valores CSS reales.
Dos cosas que importan más que el conteo:
qr-code.cell-shapequedó FUERA, con motivo escrito. Estampadata-cell-shapeen el<svg>raíz pero ninguna regla CSS lo lee: la forma se dibuja en la geometría del<path>. Con él dentro, el test pasaba inspeccionando el conjunto vacío. De ahí la aserciónused.size > 0: un eje ciego es fallo, no verde. Su prop la sigue guardandotscen el call site.- Probado por mutación, 4/4: typo en un valor CSS · unión estrechada · unión reescrita a un alias ilegible · eje cegado por renombrado de reglas. Los cuatro detectados; el árbol restaurado byte a byte después.
El hallazgo, y la decisión que espera
Buscando si los ejes con alias estaban cubiertos en otro sitio apareció
src/uix/eidos/component-visual-attrs.test.ts.
Es la otra mitad del mismo problema, y lleva ahí desde antes de esta jornada:
verifica que el wrapper siga estampando el attr, para 60+ componentes, y guarda
data-size / data-variant / data-color enteramente fuera de morfo. Es la
prueba, en el propio árbol, de que la sentencia del autor no inaugura nada:
describe cómo estaba construido el sistema ya.
Las dos mitades, ahora explícitas:
| Mitad | Pregunta | Dónde |
|---|---|---|
| Presencia | ¿el wrapper sigue estampando el attr? | component-visual-attrs.test.ts |
| Enum | ¿el valor es uno que el tipo permite? | recipe-css-contract.test.ts (§2) |
Y el hueco en la mitad de presencia, medido: image, skeleton, qr-code
y s-text no tienen ninguna fila (0 de 4); avatar tiene 3, pero no lista
data-ring, data-position (badge) ni data-stacking. Añadir filas ahí es una
decisión del autor, no una consecuencia mecánica: está presentada y parada.
3 · Qué queda, en orden
- DECISIÓN PENDIENTE — cerrar el hueco de la mitad de presencia. Cuatro
componentes sin fila y tres ejes de
avatarsin cubrir (arriba). Es añadir filas aVISUAL_ATTRS; lo que no es mecánico es decidir si los 7 knobs booleanos (data-dot,data-truncate,data-clamp,data-canvas,data-italic,data-underline,data-animated) entran ahí o no se guardan. - Los dos ejes con alias, sin cubrir por el guard de enum:
data-color(→ComponentColorProp, eneidos/lib/types.ts) ydata-styledes-text(→TextStyle, encomponents/text/types.ts). El parser no sigue imports a propósito. Resolverlos es leer el módulo destino; decidir si vale la pena está abierto. - Test de geometría de la capa (§1) — el tercer consumidor ya existe.
- Ajenos a este eje, preexistentes y NO tocados:
morfo:checkfalla enfab(data-fab-sizesin declarar) ymenu-dial(data-stateausente en el trigger). ⚠️ Sobre el primero:data-fab-sizeno está en ningún morfo —grep -rn "data-fab-size" src/uix/morfo/no devuelve nada, ygit log -Sconfirma que nunca lo estuvo. Lo mismodata-picker-size(×7) ydata-marker-size. No son deuda: son el modelo. Lo que chirría esmorfo:check, que clasifica por PREFIJO mientras la doctrina clasifica por NATURALEZA. La salida limpia sería renombrar adata-_*(prefijo reservado, «intentionally outside morfo») — sin firmar.
4 · Cómo medirlo (estado al cerrar, verificado hoy)
npx vitest run src/uix/eidos
| Guard | Estado |
|---|---|
npx vitest run src/uix/eidos |
421/421 en 32 ficheros (recipe-css-contract aporta 42) |
suite completa (npm run test) |
4960/4969; los 9 fallos = 6 reales + 3 falsos (abajo) |
contracts.test.ts |
6 fallos PREEXISTENTES: waveform, media-player, audio-player, menubar, aura, radio-group, tabs — ninguno tocado por este eje |
npm run morfo:check |
6/159 fallan: color-field, combobox, fab, gradient-builder, menu-dial, palabras (preexistentes) |
npm run docs:check |
0 errores / 627 docs |
npm run rtl:check |
0 errores / 178 ficheros |
npm run layer:check |
requiere dev server; 0 violaciones sobre 3 consumidores en la última medida |
npm run check |
69 errores / 6899 ficheros — la línea base heredada, sin mover en toda la jornada |
5 · Tres trampas de la jornada, para no repetirlas
- Un test en verde no prueba nada hasta que falla sobre lo que dice atrapar.
layer-checkpasó en su primera ejecución habiendo inspeccionado cero elementos (la demo defabvenía conplacement="static", que no estampa gancho). El guard de enum estuvo a punto de repetirlo conqr-code. De ahí que ambos traten el conjunto vacío como violación, y que los dos estén probados por mutación (8 defectos inyectados en los de la capa, 4 en el de enum; 12/12 detectados). prettier --writesobre un fichero preexistente sucio reformatea el fichero entero. Pasó tres veces hoy (66 líneas por 9 mías; 227 por 1). 7 de los 14 tests desrc/uix/eidos/están sucios en HEAD — todos sin punto y coma. La regla de CLAUDE.md manda: match existing style. No formatear al pasar.- Correr guards pesados en paralelo con la suite produce fallos fantasma.
npm run testdio 9 fallos con cuatro scriptstsxcorriendo a la vez; los 3 desoma-attr-audit/orca/storageeran timeouts por inanición de CPU — 161/161 en aislamiento. Medir en serio es medir solo.