astra
alpha-0.1-background
alpha-0.1-dir-prefs
alpha-0.1-sec-dom
menubar-v4-safe
active-uix
morfo-runtime
morfo-driven-soma
semantuix
glm-5
main
sium-v1.0
${ noResults }
1921 Commits (alpha-0.1-dir-prefs)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
aa60aba6be |
docs(process): el handoff dice donde estamos y por donde se entra
El documento seguia contando la sesion del 2026-08-10: «fase 7, 7 de 15», «quedan 7 en este orden», el ledger a 51/27/12 y unas puertas «SIN COMMITEAR». Nada de eso era cierto ya, y un handoff rancio es peor que ninguno porque se lee como si fuera el estado. Puesto al dia: - **Fase 7 CERRADA 15/15.** Ninguno de los 15 tiene un defecto de composicion: lo que salio vive una capa mas abajo (canon) o en las FICHAS del ledger. Se conserva escrito COMO se llego ahi, porque me equivoque en el camino — declare «conformes» siete blocks de los que solo habia medido un eje, y se corrigio cuando el pregunto si habia terminado. Un barrido no es la rejilla. - **Ledger a 64 ARREGLADO / 19 CONFIRMADO / 13 REFUTADO** sobre 96 filas. - **§Qué queda reescrita como puerta de entrada**, agrupando las 19 vivas por lo que hace falta para cerrarlas en vez de por severidad: tres bloqueadas por decision suya (A-67 al eje de sema con la puerta ya elegida · A-47, que por doctrina es un VALOR de configuracion y no un cambio de CSS por componente · A-09, cuya disposicion apagaria la deteccion live), una bloqueada por una pieza que no existe (A-95 espera a `app-shell`, F3.1), y quince de trabajo normal — con el aviso de que A-55 no es un defecto visible (1,6% del ancho) y de que A-65/A-75 son el mismo patron. - **Las dos sesiones del 16/17** con sus dos commits (`4caf1100a` el renombrado del eje de tamaño, `d02021aee` los dos arreglos de canon), incluido el residuo del 35,4% de A-68, que es peor que mi estimacion y queda escrito como tal. - **Puertas al parar**, con la advertencia de siempre reforzada: la cifra de `svelte-check` SE MUEVE con otras sesiones vivas en el arbol (72, 74, 75, 76, 77 y 80 en dias distintos), asi que se mide justo antes y justo despues del cambio, nunca contra un numero recordado. Y los 8 rojos vivos se nombran uno a uno como ajenos, con el commit que los introduce. Y tres errores de METODO de estas sesiones, que es lo que un handoff existe para que no se repita: 1. Medir un default en una demo que lo pisa no es medir un default. 2. El panel del navegador oculto suspende el IntersectionObserver, no solo el rAF — una medicion dio «contadores congelados» que era la suspension, no el arreglo. La via fiable es Playwright headless. 3. `Motion` es `once: true`: empujar algo bajo el pliegue DESPUES de cargar no lo hace invisible, su observador ya disparo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d02021aee1 |
fix(eidos): dos animadores que no se hablaban y un cluster que no envolvia
Dos hallazgos del contraste doctrinal del tier, ambos de CANON, ambos medidos antes y despues. El tercero (A-67) queda REGISTRADO y sin ejecutar: cae en el eje de sema. ── A-99 · Group no aplicaba NINGUNO de sus dos defaults ────────────────────── Su README los promete en tres sitios («defaults pensados para action rows — `align: center`, `wrap: wrap`»). No pasaba ninguno a `Flex`, que cae a `nowrap` + `stretch`. De ahi que las acciones de `hero` no apilaran a 375px (A-98): 315 de 327px en una linea, con la segunda etiqueta truncandose. ⚠️ CORRECCION A MI PROPIA FICHA. Habia escrito que «el `align` SI se cumple, lo que descarta que el recipe no cargue». Era FALSO: el `center` que medi salia de la demo, que lo pasa explicitamente en su control. `group.css` no declara ni `align-items` ni `flex-wrap` — no habia nada que cargar. Medir un default en una demo que lo pisa no es medir un default. Y de las tres salidas que la ficha proponia, «que lo declare el recipe» es IMPOSIBLE: `flex.svelte` tiene `wrap = 'nowrap'` como default de JS y siempre escribe `--flex-wrap` inline, asi que el fallback de `var(--flex-wrap, nowrap)` no se evalua nunca. Solo ganaria una declaracion dura, que congela el eje. El arreglo usa el mecanismo que el propio fichero ya tenia para `direction` y `effectiveGap`: `align = 'center'` destructurado (sobreescribible — `align` no esta omitido de `FlexProps`) y un `effectiveWrap` interno, `wrap` salvo `attached`, que lo invierte. Espejo exacto de «`attached` implica `gap=0`», y por la misma razon fisica: el recipe cuadra los radios interiores y tira de un margen negativo asumiendo UNA linea, asi que un segmentado envuelto abriria su segunda linea con una esquina plana y el borde recortado. EL CENSO DECIDIO LA FORMA. 77 de 91 usos de `<Group` ya pasan `align`; de los que no, tres blocks lo reenvian con default propio y **`ButtonGroup` es el unico que voltea `Group` a columna en todo el ecosistema** — donde `center` habria dejado los botones verticales en dientes de sierra. Un solo caso no justifica un default condicional, asi que la excepcion vive en su call site. Medido: `hero` a 375x800 pasa a DOS lineas (tops 416,3 / 472,3) y la accion secundaria de 124px truncados a 156,9px, su etiqueta entera. ButtonGroup horizontal `attached` sigue `nowrap`; el vertical, `stretch` con los tres hijos a 301px. El README se corrige entero: negaba `attached` como prop canonico y listaba `grow` como «gap conocido» cuando ambos llevan tiempo implementados — describia la era del baseline `air`. ── A-68 + A-93 · dos observadores que no se hablaban ───────────────────────── `CountUp` trae su propio IntersectionObserver (threshold 0) y `Motion` usa otro (threshold 0.1, rootMargin −10%). Un IO no mira la opacidad, asi que las cifras contaban su recorrido entero detras de `opacity: 0`: por scroll lento la primera aparecia ya al 97,8% de su valor. ⚠️ EL DILEMA QUE PLANTEE ERA FALSO. Dije que escalonar el arranque y aterrizar juntas eran incompatibles — cierto — y de ahi que exponer el `seen` de `Motion` rompiera A-69. No se sigue: el retardo del stagger vive en `animation-delay` (CSS) y los observadores de los cuatro `Motion` disparan en el MISMO instante, porque los stats estan en una fila, a la misma altura. Gatear en el REVELADO da arranque conjunto, duraciones iguales y aterrizaje conjunto intacto. Presente como decision entre dos males algo que tenia una tercera opcion mejor. Tambien descarto la «alternativa minima» que llegue a recomendar (alinear el observador de `CountUp` con el de `Motion`): es acoplamiento por copia — duplica threshold + rootMargin en otro fichero, la deriva que el framework elimina con builders tipados — y cambia los defaults de `CountUp` para todos sus usos sueltos. `Motion` publica ahora su momento visto por contexto (`motion/context.ts`, calcado de `cascade/context.ts`, que ya exponia `open`), y `stats-band-value` lo pasa a `startWhen` ANTES del spread de `countOptions`, para que el app pueda desactivarlo. Cero cambios en `CountUp`, cero observadores propios en el block, B-6 intacto — la costura era justamente lo que faltaba en canon (A-93). Medido con Playwright headless, 1280x800. ⚠️ La primera corrida fue INVALIDA y se descarta: inserto el espaciador DESPUES de cargar, y como `Motion` es `once: true` su observador ya habia disparado con la banda en pantalla. El espaciador tiene que existir en el primer pintado (`addInitScript`). Segunda corrida, con la banda nacida bajo el pliegue: - fuera de pantalla 2,6 s: pending 4, opacidades [0,0,0,0], contadores 0/0/0 - ventana que discrimina (banda a 6px dentro, donde el IO de CountUp dispara y el de Motion no): 0/0/0 en la llegada Y tras 2,6 s. Antes, ahi mismo, corrian (1144/31/4) y acababan practicamente terminados (12.121/330/47) - al revelarse, la primera cifra al 0% — antes 97,8% - A-69 INTACTA: las tres aterrizan a 1781 ms, dispersion 0 ms RESIDUO, y es peor que mi estimacion: las cifras que el stagger revela despues llegan al 16,8% (+70 ms) y 35,4% (+210 ms). Estime «≤10%» y la medicion dice 35,4% — el muelle es sobreamortiguado y cubre mucho recorrido al principio. Es el precio del aterrizaje conjunto que A-69 firmo y no baja sin reabrirla. Queda documentado, no escondido. ── A-67 · registrada, no ejecutada ────────────────────────────────────────── Contrastadas contra `engine.ts` y el canon, las tres puertas de su ficha fallan: el pack no puede distinguir el `collapse` de un intercambio del de un cierre a secas (mismo evento, mismo tipo de nodo; pediria un `:has()` a mano sobre el hermano); el arbitro ya implementa la mitad del «on a tie, the most recent» que un filtro de LLEGADA permite, y la otra mitad exigiria revocar una nota ya agendada, que `EngineSound` no sabe hacer; y un evento unico rompe los `commits`, porque cada uno estampa `data-state` sobre SU item. La puerta correcta no estaba en la lista: un nuance declarado, `emerge-collapse-swap`. La forma es canonica y el provider lo sabe de forma determinista en `setValue`. Cae en el eje de sema, asi que NO se ejecuta aqui — se deja elegida para su sesion. Puertas: svelte-check 72 errores / 59 avisos = linea base exacta · blocks:check 0 sobre 15 blocks · docs:check 0/0 sobre 629 docs. La suite completa deja 8 rojos en 3 ficheros, todos nombrando waveform / skin-media-player / radio-group mas un timeout de orca — ninguno en lo tocado aqui. Prettier solo se queja de ficheros CRLF preexistentes (el diff seria el fichero entero), asi que no se reformatean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4caf1100a1 |
refactor(blocks)!: el eje de tamano dice de QUIEN es — size deja de significar dos cosas
LA AMBIGUEDAD, medida. En el canon `size` no es ambiguo: es «el eje de tamano de
ESTE componente», y cada uno lo materializa a su manera — padding de bloque en
`Section`, densidad de la tira en `Banner`, escala tipografica en `Text`. La
ambiguedad la creaba el tier: un block no es ninguno de esos componentes, y al
reenviar `size` estaba reenviando el eje de una pieza INTERNA. Desde fuera:
<FeatureGrid size="xl"> cambia el AIRE de la seccion
<SiteBanner size="lg"> cambia la ALTURA de la tira
Misma prop, dos cosas, y una con un valor menos en la escala (`BannerSize` no
tiene `xl`). Un consumidor que aprende el eje en trece blocks se equivoca en el
catorceavo.
EL RENOMBRADO:
- `size` -> `sectionSize` en los 13 que envuelven `Section`
- `container` -> `containerSize` en los 14 que envuelven `Container`
- `size` queda LIBRE y solo lo usa `banner`, donde si es el tamano del block
- `minColumnWidth` -> `minChildWidth` en `site-footer`: el mismo concepto tenia
dos nombres en el tier, y ninguno era el del canon
El patron no se invento: el tier YA nombraba un eje por su pieza (`container`).
`containerSize` + `sectionSize` lo hace explicito y simetrico.
LA REGLA, en `architecture/blocks.md` §Conventions, para que no reaparezca: un
block que reenvia el eje de tamano de una pieza interna lo nombra `{pieza}Size`,
y `size` se reserva para el tamano del block. Corolario: un eje que el canon ya
nombra se reenvia CON SU NOMBRE.
METODO — se renombraron los TIPOS primero y se dejo que `svelte-check` senalara
cada consumidor, en vez de buscar a mano. Cazo los cinco call sites que quedaban
(`CtaSite`, `HeroSite`, `SiteHeaderSite`, `TeamSite` y un uso suelto en
`cta.svelte`). ⚠️ Y cazo tambien un error propio: el primer patron era demasiado
ancho y renombro `size` en SIETE sub-partes donde ese `size` es el del `Card` o
el `Text` que envuelven (`testimonials-item`, `contact-reason`,
`team-member-role`…). Revertidas antes de seguir.
DOCUMENTACION revisada entera: corregidas las tres filas de README que describian
props del block con el nombre viejo (`hero` x2, `site-header`); conservadas las
seis que hablan del `size` de un componente del canon (`Banner`, `Accordion`,
`Heading`), que no cambia. `PLAN-blocks.md` se deja intacto: es bitacora, y
reescribir el registro historico para que cuadre con el presente lo falsearia.
LEDGER — el contraste doctrinal llega a 15/15, y cinco filas nuevas:
- A-95 `banner`: con `affix="top"` el aviso tapa la cabecera pegada (medido:
`elementFromPoint` sobre el header devuelve la tira). NO se arregla aqui: la
pieza que falta es la que POSEE las alturas de pagina — Mantine lo resuelve en
`AppShell`, que declara `header={{ height }}` y desplaza el resto; nuestro
equivalente es el block `app-shell`, sin construir.
- A-96 `banner`: `affixOffset` es prop publica sin control vivo en la demo.
- A-97 `feature-grid`: RETIRADA el mismo dia. Se midio el texto `muted` con el
4,5:1 generico de WCAG y el proyecto tiene OTRA vara — «floors APCA >= 60 ∧
WCAG >= 3», con guard propio, y la tinta secundaria es «marginal sub-4.5:1 by
design». Re-medido con `src/arts/color/apca.ts`: Lc 63,7 y 3,70:1, cumple las
dos. (El primer intento con `apcaLc` devolvio 102666 porque le pase 0..255
donde pide 0..1: un valor fuera de rango no es un hallazgo, es un formato.)
- A-98 `hero` + A-99 `Group`: las acciones no apilan ni envuelven. La causa NO es
del tier — `Group` no aplica el `wrap` que su README promete en TRES sitios:
`group.svelte` nunca se lo pasa a `Flex`, el recipe no lo declara, y
`Omit<FlexProps,'wrap'>` impide compensarlo desde fuera. El `align: center` si
se cumple, lo que descarta que el recipe no cargue.
Y A-25 cae (la demo ya no promete elevacion) y A-47 corrige su disposicion: el
anillo de foco es «a config axis» por doctrina — endurecerlo es una decision de
VALOR en `color.focus.ring`, «never a per-component CSS change», que es
exactamente lo que hacia el intento revertido en `e468e764b`.
Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 72 errores /
59 avisos = linea base exacta, ninguno en blocks · docs:check 0/0 sobre 629 docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
68a48a4401 |
refactor(skin-media-player): un cableado y cuatro pinturas, con el movimiento del sistema
Reescritura completa de la versión A (`bf12b8a57`), que el autor valoró como
duplicación. La medición le daba la razón: `fit()` idéntico en dos ficheros,
`hold()` y `stop()` en otros dos, y `MediaPlayerProvider.require()` en los
cuatro. Eran cuatro componentes compartiendo carpeta, no un skin.
## La forma
Un componente cablea, los aparatos sólo pintan.
- `skin-media-player.svelte` — EL punto de cableado: un `require()`, una
derivación de todo, y las teclas generadas recorriendo un mapa.
- `controls.ts` — datos por aparato: lienzo, teclas `[{id,x,y,w,h,action}]` y la
ranura del volumen.
- `actions.ts` — el vocabulario único (`play·pause·toggle·stop·seek-*·rpm·none`),
con `stop()` y el veto por `preventDefault()` en un solo sitio.
- `type-fit.svelte.ts` — el ajuste de `<text>`, una vez.
- Los cuatro cuerpos: props entran, SVG sale. Cero provider, cero `$effect`,
cero contexto. Añadir un quinto aparato es un SVG y una fila de datos.
`<SkinMediaPlayer>` deja de montar player: es una PIEZA dentro de uno. No monta
`Media`, no monta barra y no decide la forma — `inline/bar/row/card` es el eje
`variant` de `AudioPlayer`, y la piel viaja dentro como hija.
## Movimiento: el del sistema, nada a mano
La versión A traía tres `@keyframes` locales y siete transiciones con números
crudos, ignorando `docs/theming/motion.md` (leída ahora, entera).
- Todo giro continuo es el preset **`spin`**; el respirar de la cinta, **`pulse`**.
El periodo va en la ranura del propio loop (`--motion-loop-spin`: 1.333s a 45
rpm, 1.8s a 33).
- Las transiciones consumen `--duration-*` / `--ease-*` por una ranura `--_smp-*`
por eje.
- DEFECTO FUNCIONAL corregido: el generador apaga los loops por
`[data-motion='reduce']` **y** por la media query; el bloque a mano sólo tenía
la segunda, así que la preferencia UIX en `reduce` sin ajuste del SO no paraba
el plato. Medido en navegador: ahora `animation-name: none`.
- Queda un `@keyframes` local, la aguja del vúmetro, anotado `functional:` y con
el periodo en token: ningún preset representa un instrumento analógico.
## Tres defectos que sólo se ven en movimiento
Los tres pasaban toda comprobación de atributos y sólo aparecieron mirando la
imagen moverse:
1. **La sombra rotaba con el disco.** El `feDropShadow` colgaba del grupo que
gira, así que su desplazamiento orbitaba (se leía como la sombra subiendo y
bajando) y la penumbra de la región del filtro barría las marcas del plato,
que parecían parpadear. El filtro pasa a un padre quieto — un disco es un
círculo, su silueta no cambia. Igual en el brazo.
2. **Las marcas del plato salían disparadas en arco.** Al rehacer se perdió la
regla del estrobo; sin `transform-origin` el preset gira alrededor del centro
del viewBox (500, 440) en vez del centro del plato (406, 468.5).
3. **El tamaño no llegaba.** `max-inline-size` sólo encoge, así que `lg` y `xl`
eran inertes en cuanto la columna era más estrecha (medido: md/lg/xl los tres
a 720px). Ahora `inline-size: min(100%, …)`, base por objeto y factor por
tamaño, una sola fuente en el recipe — la base estaba declarada dos veces,
inline y en CSS, y ganaba la inline.
## Volumen
Primero se compuso el `Knob` del catálogo y era inmanejable: mapea el valor al
ángulo ABSOLUTO del puntero alrededor del centro (`Gesture.rotate`), o sea
orbitar el ratón en torno a un dial de 40px. Sustituido por el `VolumeSlider`
del propio player corriendo en una guía pintada por la máquina. El re-tinte va
sobre el MISMO descendiente que retinta el player (`media-player.css:420`) o su
regla gana por estar más cerca del thumb.
La banda inferior de la pletina se reordenó para alojarlo: vúmetro a media
anchura con el dial recentrado (aguja intacta), deslizador a 42 unidades del
bisel (20px a `md`), contador y piloto +20 a la derecha, la etiqueta REPROD del
piloto fuera, y «MODELO CR-77» al mismo ancho que «Levante» por `textLength` +
`lengthAdjust="spacing"`.
## Prop `labels` y la decisión de idioma
`labels` (default `true`) apaga la serigrafía entera de la máquina. Los rótulos
NO se traducen, y es decisión firmada: son parte del OBJETO, como la marca del
badge. Los nombres accesibles sí siguen el idioma, porque salen del morfo de las
partes compuestas.
## Demo canónica
`web/routes/uix/components/skin-media-player/+page.svelte` con la plantilla v2
de 9 pestañas + su entrada en el sidebar. El banco provisional de
`/otros/players` se borra: duplicaba la demo con DOS players independientes, que
es lo que produjo dos lecturas falsas («no está sincronizado», «fondo negro»).
Los cuatro bocetos del autor se quedan en la carpeta, ya sin ruta.
`preload="none"` en la demo a propósito: un preload de metadata abre una
petición de rango que el navegador aborta, y `smoke` cuenta ese aborto como
fallo same-origin.
## Auditoría — el hueco que queda
`component-guide.md` §5 dice que incluso un átomo pasivo es morfo-first, con un
morfo mínimo `scope: ['eidos']` y `## Passive justification`. Este componente no
lo tiene porque el alcance acordado excluía `src/uix/morfo/`, y la consecuencia
no es cosmética: **tres máquinas no lo ven** — `component:audit` enumera por
ficheros de morfo y no le da fila, `eidos-lint` lo salta («no morfo file») y
`morfo:check` no visita su demo. Los guards transversales sí pasan; los
específicos de componente no se han ejecutado sobre él ni una vez.
Pendiente además: `data-perm-step` en la demo (A37), sin el cual `perm:check` la
salta en silencio.
Verificado: `npm run check` sin errores nuevos (base 72/59) · 77 tests de eidos
(recipe-css-contract · component-api-contract · component-visual-attrs · motion)
· `rtl:check` 0/179 · `docs:check` 0/0 · `layer:check` 0 · `smoke` PASS ·
`morfo:check` 159/159. Y en Chrome, con el ratón: transporte en los dos
sentidos, brazo siguiendo el surco, bobinas por conservación de área, contador
de cinta, y el volumen moviendo `audio.volume` de verdad.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
bf12b8a571 |
feat(skin-media-player): cuatro aparatos sobre el contrato del player — versión A, para el histórico
Se commitea PARA CONSERVARLA: esta versión se borra y se rehace. El autor la valoró como duplicación, y la medición le da la razón. Queda aquí para que la segunda no repita sus errores. Qué hay: - `eidos/components/skin-media-player/` — cuatro objetos dibujados (disco, giradiscos, casete, pletina) sobre el morfo `media-player`. El aparato anida su soporte como `<svg>` interior, así que la pintura del soporte se reutiliza. - La afordancia es un `<button>` HTML superpuesto, colocado en % del viewBox, y NO un `role="button"` sobre un `<g>` como en los bocetos. Precedente: `onion-menu` («a real HTML button overlaid at the SVG centre… which an SVG element can't do») y el knob de `natural-time-picker`. - Transporte compuesto sobre las partes reales: MARCHA/REPROD sólo arrancan y PAUSA sólo pausa, anulando el toggle de soma con `preventDefault()` en la costura (patrón de `RateFloat`). PARO/STOP no existe en el contrato y se compone `togglePlay()` + `seek(0)`. - `engaged()` — `started` se enciende con la primera reproducción y no se apaga nunca, así que por sí solo dejaba el brazo apoyado sobre un disco parado. Con `started && !ended && (currentTime > 0 || !paused)` salen los tres estados de una máquina: parada, sonando, en pausa. - Geometría portada del boceto: brazo por cinemática inversa de forma cerrada (r² = K + A·cosθ + B·senθ) y bobinas por conservación de área (R_izq² + R_der² constante), de donde sale sola la velocidad correcta de cada bobina. El contador de cinta lee el medio en vez de un `setInterval` propio. - Los 83 colores del autor se conservan intactos: `skin-media-player` entra en `FIXED_TONE_COMPONENTS` — la excepción que el guard ya reserva para los componentes que DIBUJAN un objeto físico (`natural-time-picker`, `color-picker`, `proof-of-human`). Acotada a esta carpeta a propósito: dentro de `media-player/` habría eximido al recipe del player real. - `web/routes/otros/players/` — los cuatro bocetos originales del autor (estado simulado, sin audio) y el banco de verificación. Por qué se rehace, medido: 1. DUPLICACIÓN. `fit()` idéntico en record y cassette; `hold()` y `stop()` idénticos en turntable y deck; `MediaPlayerProvider.require()` + `engaged()` en los cuatro. Son cuatro componentes que comparten carpeta, no un skin: cada cuerpo se agarra al provider por su cuenta y recablea las mismas costuras. Lo correcto es al revés — un componente que recibe el player y resuelve las acciones, y a cada aparato sólo su pintura y su mapa de mandos. 2. MOTION A MANO. Tres `@keyframes` locales y siete transiciones con números crudos, existiendo el motor: `smp-turn` ES el preset `spin` (`loop-spin`, periodo por `--motion-loop-spin`) y `smp-tape-pulse` ES `pulse`; las transiciones debían salir de `--duration-*` / `--ease-*`. Y un defecto funcional: el generador apaga los loops por `[data-motion='reduce']` Y por la media query; mi bloque a mano sólo tenía la segunda, así que la preferencia UIX en `reduce` sin ajuste del SO no paraba el plato. 3. La raíz `skin-media-player.svelte` empaqueta `Media + Skin + barra` por dentro, así que la forma del reproductor (inline/bar/row/card, que es el eje de `AudioPlayer`) no se podía elegir y el MediaPlayer no se veía por ningún lado en la demo. Verificado antes de commitear: `npm run check` sin errores nuevos (la base del repo, 72, no se mueve) · guards de eidos 49/49 · `rtl:check` 0/179 · en navegador con audio real, transporte, brazo siguiendo el surco, bobinas por área y contador de cinta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
2447a466b4 |
docs(process): el handoff dice donde quedo publicado
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
65536ae6c8 |
test(eidos): el enum que salio del morfo vuelve, leido de la union de TypeScript
Los 25 knobs visuales salieron del contrato en
|
2 months ago |
|
|
948f7cf7e1 |
feat(blocks): el guard vigila la frontera, y siete filas del ledger caen medidas
FASE 5 DEL SANEAMIENTO — `blocks:check` deja de vigilar solo la forma del tier
y pasa a vigilar su frontera, su documentacion y su catalogo. Tres reglas, cada
una con su fixture negativo en el selfTest():
1. Lista blanca de importaciones (B-4). La frontera dura 1 era prosa; ahora es
codigo: $uix, los arts publicos, $libs/forms, svelte y los relativos del
propio block. Los arts se DERIVAN de src/arts/*, no se listan a mano — una
lista escrita se queda atras el dia que aterriza un art. El tier ya cumplia:
cero violaciones al encenderla.
2. README completo (B-9) + declaracion de landmark (B-8). Salieron 7 de 15 sin
ella; escritas leyendo la fuente de cada block, no la plantilla.
3. Ficha en el catalogo de demos (B-9), en los DOS sentidos: un block que el
catalogo no publica es invisible para el rail, y un slug publicado sin block
detras es un enlace muerto.
De paso el escaner deja de leer los comentarios como codigo: los index.ts
documentan su uso con un `// import { Cta } from '$blocks/cta'`, y una lista
blanca que lee prosa habria empezado a acusar a los ejemplos.
Verificado EN ROJO sobre el arbol real, no solo contra los fixtures: un zod y un
$libs/days metidos en hero/types.ts salen con su linea exacta mientras el mismo
import dentro de un comentario NO salta; el catalogo falla en las dos
direcciones; y al romper isAllowedSpec a proposito el self-test aborta el guard
en vez de dar verde.
ARREGLOS DEL CONTRASTE DOCTRINAL — siete filas, cada una con medicion o gate:
- A-88 team: `height="100%"` en el Stack del Member. El div que pinta Motion es
un bloque pelado, asi que el Stack se quedaba a altura de contenido y el
`margin-top:auto` de los enlaces repartia CERO. Medido: la desviacion entre
filas de iconos pasa de 20px a 0, y el margen reparte 20,297px.
- A-94/A-28 (feature-grid, team, testimonials): los tres declaraban su tipo como
`= AutoGridProps` y ponian {...rest} ANTES de las props que fijan, asi que un
consumidor podia escribir align="start" y no obtener nada. Cerrados con
Pick<AutoGridProps, ...>, que conserva los tipos exactos del canon.
- A-84 feature-grid: el eje `align` llega a las partes por contexto, como en
team, en vez de que el typedoc instruya al app a repetirlo. Medido: con
align=center el align-items computado es center en cabecera Y celdas.
- A-19 faq: `marginX="auto"` en el Header — Box no centra solo. Medido: el Box
de 768px resuelve margin-inline 248px/248px donde antes daba 0.
- A-27 testimonials y A-86 pricing: el typedoc decia `soft` donde el codigo hace
`outline`, y el ejemplo de la puerta de entrada no compilaba.
- A-91 banner (fila nueva): el block hereda el eje intent/color que el canon
declara fuera del sistema abierto en su propio typedoc, sin registrarlo.
LEDGER — dos lecciones de instrumento escritas dentro, porque ambas produjeron
hallazgos falsos publicados:
1. El dev server sirve codigo ANTERIOR a HEAD si reusa cache de Vite. Tres filas
(A-36, A-69, A-85) se dieron por vivas estando arregladas. El delator fue
aritmetico: las duraciones medidas eran EXACTAMENTE las que el comentario del
arreglo cita como estado anterior.
2. Contar nodos WebAudio no es medir sonido: la sintesis crea dos osciladores
por earcon, un sound pack de muestras cambia la aritmetica entera, y crear
nodos no es sonar. El contador responde "hubo actividad" y nada mas.
Y el error de metodo del que ambas son sintoma, tambien registrado: se fue a
re-medir A-67 sin leer su ficha, que ya traia los gains del catalogo, el empate
de applyDominance y la regla de CANON.md incumplida — mejor razonado que la nota
que lo sustituia.
Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 69 errores /
57 avisos = linea base exacta, ninguno en lo tocado · docs:check 0/0 · prettier
limpio en todo lo del commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
998d72191e |
refactor(morfo): los 25 knobs visuales salen del contrato — comentados, no borrados
Morfo es el contrato ENTRE capas, y un knob de pintura lo lee UNA: el CSS del
propio componente. La doctrina lo dice en tres sitios, y ninguno lo escribi yo:
· morfo.md §«What morfo does NOT contain» → «Visual variants / recipes →
src/uix/eidos/ — Layer-specific, not shared»
· morfo.md §Backlog 2026-08-13 → `undeclaredState` es «the escape hatch for
attrs OUTSIDE the contract — a visual wrapper's `data-size`, a presentation
flag like `data-sheet`», y LANZA si el attr resulta estar declarado
· theming/reference.md §39 → la familia `data-variant`/`data-size` es un
wrapper attr eidos-only; regla de pulgar: morfo declara un attr solo cuando
su prop cruza la frontera de SOMA
Ninguno de los 25 cruza: los seis componentes no tienen capa soma.
avatar 9 size · variant x2 · color x2 · radius · position · dot · ring
s-text 6 style · truncate · clamp · canvas · italic · underline
image 5 fit · position · radius · size · placeholder
avatar-group 2 size · stacking
skeleton 2 shape · animated
qr-code 1 cell-shape
COMENTADOS, NO BORRADOS, por orden del autor, y cada bloque lleva su porque con
las tres citas. Dos motivos que valen la pena: son la huella de lo que el morfo
prometia, y son el enum que `eidos-lint` podra volver a leer el dia que sepa
tomarlo de la union TS (`AvatarSize = Extract<Size, …>`), que es donde vive de
verdad. Hasta entonces esos 25 enums pierden la comprobacion del CSS — el codigo
sigue tipado.
LO QUE SE QUEDA, y por que: los `data-status` de `avatar` (x3) e `image` (x4) los
produce `ImageProvider` de `$soma/layers` — `avatar.svelte:24`,
`avatar-image.svelte:3` — asi que su driver SI cruza, que es exactamente la
condicion que nombra la regla de pulgar.
FUERA DEL BARRIDO, y tampoco son knobs: `separator` y `button-group` declaran
`data-orientation` junto a un `aria-orientation` del MISMO prop (es el espejo CSS
de una propiedad ARIA que el morfo ya posee); `chart` usa `data-chart-axis` para
discriminar dos instancias de la misma parte (identidad estructural); y
`barcode` usa `data-symbology` para decir que estandar codifica el simbolo.
Medido: los seis morfos parsean, `eidos-lint` invalid 0 en todos, y el
desplazamiento esperado — avatar pasa de 24 morfo-backed a 7 y de 46 a 63
eidos-only. morfo:check PASS en los seis (nunca los miro: son attrs desnudos,
no `data-{kebab}-*`). component:audit los seis PASS. check 69 = base intacta.
morfo:vocabulary OK. smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b72a564918 |
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 |
|
|
de0615caf2 |
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 |
|
|
a11805d434 |
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia. `testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que atrapa cada script», con su reparto explicito: `layer:check` mira el valor computado (quien gana la cascada) y declara su hueco (la geometria, porque `getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles); `shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio. `component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar. `canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda `--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist de recetas decia «si flota → una rung de overlay», que era incompleto. `eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y `affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un contrato ajeno), un eje = token publico + ranura, el puente reafirma `position` si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna basta sola. `audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia tabla resumen como «pendiente de doctrina explicita». Ya no lo esta. `PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba prevista en el plan; todas salieron de auditar lo construido. docs:check 0/627. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
7ea913ab11 |
refactor(eidos): la rejilla de colocacion logica se canoniza — cinco declaraciones a mano pasan a una
`lib/types.ts` ya tenia `Position`, la rejilla 3x3 FISICA, con su idioma `Extract<Position, …>` y sus consumidores (Dialog, Drawer, Toast). Lo que no tenia nombre era la otra: la LOGICA, escrita a mano cinco veces — `AffixPlacement`, `FabPlacement`, `MenuDialPlacement`, `OnionPlacement` y las cuatro esquinas de `AvatarBadgePosition`—, coincidiendo por mantenimiento manual y no por contrato. Anadir una zona a una no llegaba a las otras cuatro. Ahora hay `LOGICAL_POSITIONS` / `LogicalPosition` junto a `POSITIONS` / `Position`, y las cinco estrechan desde ella. Ambas pasan a ser consts para que `docs:vocabularies` las genere: eran las unicas props visuales compartidas que faltaban en `docs/canon/vocabularies.md`, que ya listaba tamanos, variantes, paletas, arquetipos y el vocabulario sema entero. NO son dos grafias de lo mismo, y por eso ninguna sustituye a la otra: son dos COMPORTAMIENTOS. `top-left` es la izquierda de la pantalla y no espeja nunca; `top-start` sigue la direccion de lectura — medido hoy, de `left: 0` a `right: 0` en RTL. La regla de eleccion queda escrita en los dos typedoc y en el canon generado: ¿tiene que voltearse para un lector de derecha a izquierda? Una tira fijada en `bottom-end` va en el borde de salida en ambas direcciones; un panel que se abre a la derecha fisica porque ahi hay sitio, no. Con eso se cierra EID-3, que el audit de julio dejo asentado como excepcion pero marcado «placement fisico — pendiente de doctrina explicita». La doctrina ya no esta pendiente: son dos rejillas nombradas con la regla de cuando usar cada una. Sin cambio de comportamiento: las uniones resultantes tienen los mismos miembros (`MenuDialPlacement` = la rejilla + `static`; `FabPlacement` = las cuatro esquinas + `static`; `OnionPlacement` y `AffixPlacement` = la rejilla entera; `AvatarBadgePosition` = las cuatro esquinas). check 69 = base intacta · docs:check 0/627 · component:audit los cinco PASS · layer:check 0/3 · rtl 0/178 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ee41b691d9 |
docs(eidos): los 20px del MenuDial son una decision, no una deriva — corregido el registro
El commit anterior (
|
2 months ago |
|
|
c53efc6025 |
refactor(eidos): MenuDial lee la colocacion de la capa — la ultima copia privada, borrada
Cierra la deuda que dejo `Affix`: las tres copias de la geometria de viewport (air/layout/float la tuvo primero, luego Fab con 4 zonas y MenuDial con 9) son ahora una sola. `menu-dial.css` pierde 31 lineas netas. Tres diferencias con la migracion de Fab, y ninguna es cosmetica: 1. `data-placement` SE QUEDA. No es un resto: tiene un segundo lector que el posicionado nunca tuvo — el arco deriva de la zona su apertura y su span (90° en esquina, 180° en borde, 360° en el centro). Un attr dice «que zona», el otro es el gancho de la capa. Verificado tras migrar: `bottom-end` en `arc` sigue computando `arc-start: 270deg` / `span: 90deg`. 2. El puente NO reafirma `position`, y Fab si. Aqui no hay empate que ganar: `[data-menu-dial]` no declara ninguno, y el `relative` que si declara casa solo con `placement="static"`, que no estampa gancho. El empate de Fab viene del `<Button>` que compone; un dial no compone nada en el marco. 3. Su offset es `--space-5` (20px), no el `--space-4` de la capa, y el puente lo PRESERVA. El comentario retirado decia que «mirrors the FAB» y era falso: el FAB va en 16px. Unificar es un movimiento visible de 4px, o sea una decision, no un efecto colateral de migrar — queda declarado en el README hasta que alguien la tome. Es exactamente para lo que existe la ranura de override. El `z-index: var(--z-index-sticky, 1100)` se fue con la copia: el token siempre resolvia, asi que el fallback de 1100 estaba muerto. El dial se queda en la banda `sticky` via `--_affix-z`, igual que Fab. Medidas las diez colocaciones: nueve zonas en `fixed`, z 100, offset 20px conservado (`calc(20px * 1 * 1)`); esquinas a 20 de sus dos bordes; los tres centrados con l = r = 431 y/o t = b = 34; y `static` sin gancho, `position: static`, z `auto`. `layer:check` pasa de 2 a 3 consumidores SIN tocarlo: la lista se deriva de quien importa la capa, que era el punto. El contrato de texto suma menu-dial (13 tests) con la regex de tokens acuñados ampliada a la forma privada `--_x`, que es la que usaba. check 69 = base intacta · component:audit affix/fab/menu-dial PASS · layer:check 0 violaciones sobre 3 · rtl 0/178 · eidos-lint invalid 0 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4b39b718c6 |
fix(eidos): el aviso al pie se lee donde se ve, y la fuente unica deja de mentir sobre si misma
Cierra los dos hallazgos que quedaban de la auditoria.
ORDEN DE LECTURA (a11y). `position: fixed` saca la tira del flujo VISUAL pero no
del DOM: se lee donde esta en el fuente. Con `affix="bottom"` el aviso se ve al
pie y la mini-pagina lo renderizaba primero, asi que un lector de pantalla lo
anunciaba ANTES que la cabecera — medido en la preview del propio block, era el
primer nodo del documento.
Quien decide eso es el APP, no el block: el block coloca, el documento ordena.
Asi que la correccion no es un prop, es que la demo deje de modelar el desajuste
— el aviso se renderiza donde se ve — mas la advertencia en el typedoc de `affix`
y una fila en los Gaps. Verificado en los dos bordes: con `bottom` el aviso va
DESPUES de la cabecera en el DOM y se ve al pie; con `top` va antes y se ve
arriba. En ambos, z 150 y pegado a su borde.
AFIRMACIONES OBSOLETAS. Cinco sitios seguian diciendo que `Fab` "ships a private
copy today" y que su migracion estaba pendiente — falso desde
|
2 months ago |
|
|
acb7f0b416 |
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 |
|
|
c945be23b6 |
refactor(eidos)!: un eje de capa = un token publico + una ranura, y Fab deja de acunar los suyos
BREAKING: se retiran `--fab-offset` y `--fab-z`. No hay shim (regla del tier).
Rompe a quien los sobreescriba en CSS de app; el sustituto es `--affix-offset`
y la ranura `--_affix-z`.
Salio de auditar lo de ayer. `Affix` hacia que el prop `offset` escribiera el
MISMO nombre que la capa lee, y eso trajo dos problemas a la vez:
1. Un ciclo. `offset="var(--affix-offset)"` producia
`--affix-offset: var(--affix-offset)` — custom property auto-referencial, que
CSS resuelve a guaranteed-invalid, matando el calc() de los insets y dejandolos
en `auto`. Medido: la caja aterrizaba a 324px del borde que debia tocar. Y la
demo ofrecia ese valor COMO UNO DE LOS TRES CHIPS de `offset`, con la
intencion de decir «el default».
2. Redundancia. Como el prop pisaba el token publico, Fab no podia usarlo y tenia
que acunar `--fab-offset` / `--fab-z`, cuyo UNICO lector era el puente que los
traducia de vuelta a los nombres de la capa. Un vocabulario paralelo para
valores que la capa ya posee — justo la deriva que la capa venia a borrar.
La forma correcta ya la habla el arbol (`code.css` x6, `display.css` x6,
`calendar-select` x4, `button` x2, `dialog`, `kbd`): DOS nombres por eje.
--affix-offset / --affix-z publico, en :root, desde recipes/base.ts.
el default temeable, un retoque para todos.
--_affix-offset / --_affix-z la RANURA de override. Ahi escribe el wrapper
desde el prop, y ahi escribe un consumidor en su
puente cuando su default difiere.
Consumo: `var(--_affix-offset, var(--affix-offset))`. Con la ranura distinta del
token, el ciclo se vuelve imposible POR FORMA, no por aviso: escrito a mano,
`--_affix-offset: var(--affix-offset)` ahora resuelve a 16px en vez de morir.
El puente de Fab baja a dos lineas y pierde toda traduccion de tokens:
[data-fab][data-affix-placement] {
position: fixed; /* gana el empate con [data-button] */
--_affix-z: var(--z-index-sticky); /* el UNICO valor en que difiere */
}
Lo que Fab necesitaba no era un token propio sino un VALOR distinto: se queda en
la banda `sticky` y no en el peldano `affix` (150), para que un menu o un dialogo
sigan abriendose por encima del FAB.
La regla que esto fija para consumidores futuros queda escrita en los dos README:
un eje de capa tiene dos nombres —default publico y ranura— y el consumidor
escribe la ranura. Acunar `--{componente}-{eje}` al lado recrea la deriva.
Verificado midiendo. Affix: `default` sin style inline y 16px, `0px` a 0, `2rem`
a 32. Fab: `--fab-offset` y `--fab-z` resuelven a cadena vacia (retirados), y las
esquinas siguen a 16px, `fixed`, z 100 via la ranura; `static` sigue `relative` /
`auto`. Identico a antes byte a byte en comportamiento.
component:audit affix PASS 0/0 · fab PASS 0/2 · check 69 = base intacta ·
rtl 0/178 · blocks 0 · eidos-lint invalid 0 · suite eidos 151/151 · smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
1fc0a7a96b |
refactor(eidos): Fab deja de tener su propia copia de la colocacion — la lee de la capa
Primera de las dos migraciones que dejo abiertas `Affix`. `fab.css` tenia cuatro reglas de esquina que eran una de TRES copias privadas del mismo CSS (menu-dial tiene nueve; `air/layout/float` las tuvo antes que las dos). Se borran: ahora `fab.svelte` importa `../affix/affix.css` y estampa `data-affix-placement`, y en `fab.css` queda un puente de tres lineas. Es la forma con la que las recetas de menu/listbox consumen `lib/list-surface.css`. El puente reafirma `position: fixed`, y no es redundancia: `button.css` declara `position: relative` sobre `[data-button]` a la MISMA especificidad (0,1,0) que la regla base de la capa, y los dos ficheros van code-split — en el orden de carga equivocado la capa pierde el empate y un FAB flotante computaria `relative` en silencio. Los ocho insets siguen viniendo de la capa; solo esa propiedad se pelea en local. Queda escrito en los dos README como lo que le debe al puente cualquier consumidor cuyo primitivo compuesto ya declare `position`. Nada cambia de comportamiento ni de API: - `--fab-offset` y `--fab-z` siguen siendo publicos de Fab; el puente apunta la capa a ellos en vez de sustituirlos. `--fab-z` se queda a proposito en la banda `sticky` y NO en el peldano `affix` (150) — un menu o un dialogo tienen que seguir abriendose por encima del FAB. - `placement="static"` ya no estampa gancho ninguno, asi que no casa nada y el consumidor lo coloca. La salida pasa a ser una ausencia en vez de una quinta regla. - `data-placement` desaparece del DOM del FAB; solo lo leia `fab.css`. Medido en las cinco colocaciones sobre la demo: las cuatro esquinas a 16px de sus dos bordes (`--fab-offset` puenteado), `fixed`, z 100 — identico a antes — y `static` sin gancho, `relative`, z `auto`. `MenuDial` intacto: conserva su `data-placement` propio y su trigger es un `Fab placement="static"`. Y queda probado en vivo el motivo del split de attrs: `morfo-check` NO juzga el `data-affix-placement` del `<button>` contra `affixMorfo`, porque se salta todo attr que no empiece por el kebab del propio morfo. component:audit fab PASS 0/2 (sus excepciones documentadas) · affix PASS 0/0 · rtl 0/178 · eidos-lint fab/affix invalid 0 · smoke 311/311. Los dos fallos previos de morfo:check (fab `data-fab-size`, menu-dial `data-state`) siguen exactamente igual: son ajenos a esto. Queda: migrar `MenuDial` (9 zonas), con su fallback `1100` y su offset interno. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
92c8f6466a |
feat(eidos): Affix — la pieza que Sticky no puede ser, y la capa que Fab y MenuDial ya habian copiado dos veces
Un aviso al principio del documento se va con la pagina: `position: sticky` se despega en cuanto termina su contenedor. Medido el 2026-08-10 sobre el block `banner` — `Sticky edge="bottom"` acaba en `top: -8` sin `data-stuck`. No es un cableado roto: es que un elemento anclado al viewport es `fixed`, y eso es otro componente. Pero no es un componente nuevo: la capacidad YA existia dos veces. `fab.css` fija 4 esquinas, `menu-dial.css` nueve, y las dos ya divergian (`--fab-offset` publico vs `--_menu-dial-offset` interno; `var(--fab-z)` vs `var(--z-index-sticky, 1100)`, un fallback de 1100 sobre un token que vale 100). Y antes de eso existio entera: `air/layout/float`, el primitivo de 9 zonas que el refactor retiro sin sustituto — lo dicen los README de `float` y de `Fab`. Asi que `affix.css` no es un tercer ejemplar: es la CAPA, enganchada en `data-affix-placement` y no en `data-affix`. El split es portante — `morfo-check` selecciona `[data-affix]` en toda la pagina y valida cada match contra `affixMorfo`, asi que si `Fab` estampara la identidad quedaria soldado a este contrato. Estampando solo el gancho de capa, no. Es el patron de `lib/list-surface.css`: una fuente, cero nodos extra. Lo que salio midiendo, no razonando: - `--z-index-affix: 150`, peldano nuevo. Con la tira en `top` empataba con `--sticky-z-index` (100 los dos) y el empate lo rompe el ORDEN DEL DOM: el aviso va antes que la cabecera en el fuente, luego perdia. 30,7px de solape con el cromo encima — el fallo original reproducido por su sustituto. Un peldano toca DOS sitios: `STATIC_Z_INDEX` y la lista cerrada `Z_INDEX_KEYS`. - NO compone `<Box>`, aunque sea el patron de los primitivos de layout: `box.css` declara `position: var(--box-position, revert-layer)` a la misma especificidad que el gancho, y eidos no usa `@layer`. En el orden de carga equivocado, `position: static` — un `fixed` que no hace nada. - `stretch` es solo de los bordes de bloque. El rail del eje inline se envio y se retiro el mismo dia: `Affix` no dimensiona a su hijo, asi que era una caja invisible de 380px con el hijo de 47px arriba, identica a `top-start` en pantalla. El marco medido decia otra cosa; la pintura mandaba. - `env(safe-area-inset-*)` es fisico y los anclajes logicos: remapeado bajo `:dir(rtl)`. Emparejar `inset-inline-start` con `safe-area-inset-left` despeja la muesca equivocada en RTL apaisado — que es lo que hacen hoy `fab` y `menu-dial`, registrado. `banner` recupera lo que su README daba por imposible: `affix="top" | "bottom"`. La disposicion anterior decia «app-land, el canon ya trae Sticky» y nombraba un componente que no puede hacerlo. Verificado con recorrido real (342px en la demo, 1247px en la preview del block): desplazamiento 0 en los dos bordes, nueve zonas exactas, RTL espejando, el ancestro transformado derivando -342px como esta documentado, y `elementFromPoint` sobre la tira devolviendo el aviso y no el cromo. component:audit PASS 0/0 · morfo:check PASS · rtl 0/178 · smoke 311/311. Queda: migrar `Fab` y `MenuDial` a la capa. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f1b672da79 |
docs: la doctrina alcanza a los cambios de la jornada — cinco backlogs y una llave de mas
Auditoria de deriva doc<->codigo sobre los 33 commits del 2026-08-13/14: cinco sitios seguian describiendo lo que el codigo dejo de hacer ayer. Se corrigen COMO BITACORA, al final de cada fichero (el cuerpo es el acta de lo que se firmo, no el estado del codigo; reescribirlo falsearia la firma). El formato es el que ya existia en `arts/adom` y `soma/textarea`: `## Backlog`. - `decisions/book-deviations.md` — dos entradas. D.7 nombraba `IntentExpectedFamily`, tipo que M6 ( |
2 months ago |
|
|
e468e764b2 |
revert(eidos): A-47 — el rescope del anillo de foco se deshace, y queda el registro de los errores
Por orden del autor. Mi arreglo del 2026-08-13 (
|
2 months ago |
|
|
c64b6814c2 |
fix(eidos): E2 rompio el boot sin window — el detector de modo vuelve a degradar
REGRESION MIA de
|
2 months ago |
|
|
89e42735e6 |
fix(eidos): A-69 — `duration` de count-up dice la verdad, y las cifras aterrizan juntas
`duration` se documentaba como «approximate count duration in seconds» y alimentaba los parametros del muelle con λ FIJA: el asentamiento real crecia con el logaritmo de la magnitud. Medido por el ledger y reproducido: 12.500 tardaba 7,88 s (3,9x lo prometido), 340 → 5,09 s, 48 → 3,57 s, 4,31 s de dispersion — y la razon 2,21 entre cifras era INVARIANTE con duration, asi que ningun valor global las igualaba. `onEnd` disparaba desde un timer ciego a `delay + duration`, 5,9 s antes de que la cifra grande parase. La ejecucion es la disposicion del ledger, tras el re-analisis que pidio el autor — mi primera propuesta sustituia el muelle por un ease-out temporal, que era cambiar el DISEÑO de tapadillo: la caida exponencial (arranque rapido, aterrizaje suave) es la semilla que el componente porta, y mi cita de motion.md §drivers para descartarla estaba fuera de jurisdiccion (gobierna presets de UI sobre propiedades CSS; esto es numero→formatter→textContent). La forma se queda; el RELOJ se recalibra: - λ derivada de (recorrido, umbral, duration): el asentamiento es analitico (|y(t)| ≈ AMP·|d0|·e^(−λt)), asi que λ = ln(AMP·|d0|/umbral)/duration hace aterrizar la ULTIMA cifra mostrable exactamente en `duration`, para cualquier magnitud — y una banda de contadores aterriza JUNTA por construccion. ζ fija en 2√2 (el caracter del default de la semilla). - `onEnd` desde la parada REAL del muelle (el callback de settle), no del timer; el tope de 30 s se queda; recorrido menor que el umbral aterriza instantaneo; reduced-motion intacto (su rama es previa y no se toca). - types.ts y README dejan de mentir: «asienta en ≈ duration sea cual sea la magnitud». MEDIDO con Playwright headless (sonda espejo de probe-A-69b del ledger: muestreo DIRECTO de textContent cada 40 ms — su propia ficha documenta el sesgo del MutationObserver), sobre la demo real de stats-band: ANTES 12.500 → 7867 ms · 340 → 5083 ms · 48 → 3567 ms (4310 de dispersion) DESPUES 12.500 → 1977 ms · 340 → 1977 ms · 48 → 1977 ms (dispersion 0) con los valores finales exactos y su agrupacion de locale. La fila del ledger pasa a ARREGLADO sin commitear el fichero (WIP de la otra sesion, protocolo A-85). NO tocados, declarado: A-68 (arranques vs entrada escalonada — item propio) y el driver `spring()` de $motion (sustrato distinto; la nota queda: si un contador necesita fisica interactiva algun dia, el driver existe). Con esto §3.6 (blocks) queda COMPLETO: A-47 y A-69 cerrados. Verificado: eidos 393 ✓ · check 69 = base aislada, diff VACIO · prettier: types.ts limpio en HEAD queda limpio; count-up.svelte y README ya fallaban. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
acda70f1e4 |
fix(eidos): A-47 — el anillo de foco sostiene el contraste sobre CUALQUIER superficie solida
El anillo global es primario translucido — suave a proposito — y sobre un `Surface variant="solid"` de color primario compone primario-sobre-primario: medido en la demo real del cta, 1,02:1 (peor aun que el 1,43:1 del ledger; misma pareja, panel rgb(142,78,198)). Los DOS unicos elementos interactivos del block quedaban sin indicador, contra el minimo 3:1 de WCAG 1.4.11. Arreglada la CLASE, no la instancia: cualquier hijo enfocable de cualquier superficie solida, en cualquier color y modo. Una declaracion en la regla solida que ya existia en surface.css rescopa `--focus-ring-color` a la misma tinta que el texto de ese lienzo — el slot `contrast(on-solid)`, elegido por APCA con suelo WCAG2 (rfc-color-engine §8). Como §32 canonizo UN solo modelo de foco (outline, todos los consumidores beben del var: button, calendar, breadcrumb, anchor-nav…), la variable rescopada repara el catalogo entero por cascada; forced-colors conserva su camino `Highlight`, ajeno a los tokens. A plena fuerza y sin `color-mix` — aritmetica sobre un token garantizado re-pierde la garantia (la clase «el silencio es un valor»). Medido antes/despues con sonda de composicion (canvas getImageData — el panel pinta en oklch wide-gamut y el ring en color(srgb …/0.52); un parser rgb da null) y sobre el ELEMENTO real: dentro del panel 1,02:1 → 5,18:1 (outlineColor del boton enfocado) fuera del panel 2,17:1 → 2,17:1 (el token global INTACTO) Dos hallazgos anotados sin decidir: `--focus-ring-color-error` sobre solido (volcarlo a la tinta de contraste borraria la semantica de error) y el 2,17:1 del anillo global contra la PAGINA (el indicador puede satisfacer 1.4.11 contra el boton adyacente; abrirlo es decision de diseño). La fila del ledger pasa a ARREGLADO pero el fichero NO viaja en este commit: es WIP de la otra sesion (protocolo A-85). Verificado: eidos 393 ✓ · check 69 = base aislada, diff VACIO · prettier: surface.css estaba limpio en HEAD y queda limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
23b20fff5c |
feat(active-uix,sema)!: S-19(ii) — announce encendido por defecto: un sumidero, materializado por la raiz
¿Debia encenderse? El analisis dijo que era LA RAIZ O NADIE: la opcion
`announce` se pasa al construir el engine dentro de `createActiveUix(options)`,
y en ese momento `uix.announce` no existe todavia — huevo y gallina. El unico
cableado que un app podia escribir (`announce: { dom }`) caia en las regiones
propias del canal: un SEGUNDO par de regiones vivas, exactamente lo que la
doctrina «ONE sink» (AUX-1) prohibe. La unica puerta alcanzable violaba la
doctrina; la raiz es el unico sitio que sostiene las dos puntas.
El impl registra el canal POST-construccion con cierre tardio —
`events.register(new AnnounceChannel({ announce: (m, p) => this.announce(m, p) }))`
— la misma forma para standalone y attach. Default ON, y la asimetria con
sound/haptic (apagados) es doctrina, no inconsistencia: esos son ORNAMENTO
(opt-in); announce es SUSTITUCION (sema.md §channels — «the one that
substitutes»), la categoria que el framework ya enciende solo (las regiones de
uix.announce se crean solas, el camino de soma esta siempre activo). La norma
del campo hace lo mismo: Angular CDK LiveAnnouncer es singleton por defecto,
React Aria usa una region ambiental de modulo. Un framework de referencia no
hace opt-in la accesibilidad.
Opt-outs estandar: `events: { announce: false }` — el engine gana el flag
legible `announceOptedOut` (las opciones de construccion no eran observables) —
y un `announce` explicito del app gana: la raiz jamas pisa un canal existente.
Cero doble anuncio, por diseño ya verificado: el runtime de soma no mete
`message` en la señal perceptual (su a11y viaja por sources.announce), asi que
el canal no-opea para todos los componentes de soma.
Visto en ROJO via stash del impl (canal sin registrar) y en verde con el:
una señal con `message` por `uix.events` aterriza en la region COMPARTIDA
(`uix-announce-assertive`, prioridad derivada del intent threat) y el
documento tiene UN solo [role='alert'] — el par fantasma nunca nace. El
opt-out respetado. `sema.md` §announce reescrito: el ⚠️ de «ninguna raiz lo
cablea» pasa a documentar el default y sus salidas.
Verificado: sema+morfo+eidos+contracts 947 ✓ / 6 ajenos · soma navegador
1278✓/1 (el timeout ajeno — TODOS los tests de navegador arrancan la raiz
tocada) · check 69 = base aislada, diff VACIO · docs 0/624 · prettier:
engine.ts limpio en HEAD queda limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
3ee17f333c |
docs(sema): los dos hallazgos abiertos quedan DECIDIDOS y escritos donde viven
El autor resolvio los dos hallazgos que salieron al cerrar §3.4/§3.5, y su
decision se documenta EN EL CODIGO, no en un handoff que nadie relee:
1. La rama de politica de `validateSemaEvent` es inalcanzable — SE QUEDA.
`isSemaEvent` es policy-aware (rechaza required-sin-intent y, desde S-33,
forbidden-con-intent), asi que una violacion sale por el throw generico y
el mensaje especifico nunca dispara. MEDIDO, no deducido:
`validateSemaEvent({family:'commit'})` lanza «is not a valid canonical
semantic event». Se conserva porque los dos guards responden preguntas
distintas —predicado de tipo vs validador de politica— y solo su ORDEN hace
redundante a uno; reordenar para hacerla alcanzable cambiaria el mensaje
lanzado sin que nadie lo pida, y borrarla sacaria el enunciado de la
politica de la funcion que lo posee. Si el predicado deja de imponer
politica, la rama ya esta aqui y correcta.
2. `SemaActionEvent` y sus tres funciones no tienen consumidor en produccion —
SE GANAN EL SITIO. No es falta de consumidor: es AUDIENCIA. Es la
superficie publica que usa una app conduciendo `EngineSemantic` SIN soma
para nombrar una ocurrencia — el mismo publico que sirve el canal announce
(dos emisores, dos publicos, §announce de sema.md). Soma nunca toma ese
camino porque baja la declaracion estructurada del morfo hasta abajo; la
asimetria es el diseño. Y por eso se mantiene ATADA al contrato: S-35
estrecho la clave para que la forma etiqueta no regale lo que la
estructurada rechaza, con los tests de politica vigilandolo.
Verificado ademas, a peticion del autor: el cableado del canal announce en las
raices NO estaba hecho — `define-engine-semantic.ts` no menciona `announce` y
`active-uix` solo tiene su propio sumidero. Lo que S-19 cerro fue el
desajuste de firmas que hacia IMPOSIBLE escribirlo; encenderlo sigue abierto
como decision, y queda anotado en el handoff con esa distincion.
Verificado: sema+morfo+contracts 552 ✓ / 6 ajenos · check 69 = base aislada,
diff VACIO · docs 0/624 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f8dd8ff810 |
fix(morfo,sema)!: M1(ii) gradient-picker — el reset delegado deja de declararse dos veces (S-14)
El morfo declaraba `commit-reset`, el pack le dedicaba una regla (settle + tick) y los dos README lo tableaban — y el provider no lo emitia. Lo unico que suena en el Clear es el `commit-reset` del Picker generico COMPUESTO, con la firma del pack `picker`. La ficha S-14 ofrecia dos salidas y su premisa comparativa era falsa, medido: los hermanos color/date/time-picker SI emiten su propio reset (date-picker cableado el 2026-08-11) — pero NO componen el Picker generico: definen su propio pickerShellHandle y son dueños de su transaccion. Gradient-picker la DELEGA entera (su docblock: «The transaction — open / commit / cancel / clear — lives in the composed generic PickerProvider»), asi que la emision sigue a la propiedad: el reset es del picker. Cablear el gemelo habria dado DOBLE firma por un gesto (dos estampados + dos sonidos casi simultaneos — la clase que file-upload pago), y PickerProvider no ofrece hook para silenciar el suyo. Retirada completa, con la palabra explicita del autor: el evento fuera del morfo (una nota en su lugar dice por que y hacia donde), la regla muerta fuera del pack, las filas fuera de los dos README (el de eidos gana la frase correcta: los eventos propios son los del DOMINIO — presets — y el Clear es del picker), la excepcion fuera del censo y el ultimo id fuera de la deuda D9. Con esto la parte (ii) de M1 esta COMPLETA — las 7 resoluciones de la cola medida el 2026-08-12: tooltip x3 CABLEADOS (receta F4 + silencio visual minimo medido), virtual-list/grid x3 `emission: 'host'` (la decision escrita de sus providers, ahora expresable), gradient-picker RETIRADO (delegacion). INERT_EVENT_DEBT y EMISSION_EXCEPTIONS quedan VACIAS por primera vez, cada una con su lapida narrando las resoluciones. M1 entero cerrado: el contrato dice quien dispara cada evento, y ningun evento declarado miente. Sin cambio de conducta: el Clear sonaba por el picker y sigue sonando igual. Verificado: guards 339 ✓ / 6 ajenos · check 69 = base aislada, diff VACIO · docs 0/624 · prettier: los dos ficheros limpios en HEAD quedan limpios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
c73cae0227 |
feat(morfo,soma): M1(ii) virtual-list/grid — el scroll declara su emisor: el host
Los tres `handle-scroll*` de virtual-list/grid estaban en la deuda del D9 como «declarados y nunca sonados». Mal clasificados: la resolucion YA estaba escrita en los dos providers como decision con su porque — el listener de scroll dispara por pixel y la familia handle es ['sound','haptic'] (trinquete `step` + tick), asi que una emision por defecto seria un clic-clic continuo bajo scroll con inercia; «apps that want scroll-driven sema feedback should wire it themselves with their own throttle / debounce policy». Lo que faltaba no era el emisor: era un sitio DECLARABLE para esa decision. Es exactamente el caso de diseño del valor `host` de D.2, estrenado aqui: el contrato declara la superficie, el emisor es la aplicacion anfitriona. - `emission: 'host'` en los tres eventos, con el porque en el docblock. - Los dos comentarios de provider apuntan al flag y corrigen la cita desfasada (`activeChannels=['haptic']` — hoy son dos canales, lo que hace la conclusion MAS cierta, no menos). - La promesa del host es viable, verificado: `provider.runtime` es campo publico — un app puede emitir con su propia politica. - Deuda D9: -3 ids. Queda UNA entrada en INERT_EVENT_DEBT: gradient-picker commit-reset (S-14), la ultima resolucion de la parte (ii). Sin cambio de conducta: cero emisiones nuevas, cero navegador que medir. Verificado: guards 309 ✓ / 6 ajenos · check 69 = base aislada, diff VACIO · prettier: virtual-list.ts limpio en HEAD queda limpio; los otros 3 ya fallaban. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
daa6461d62 |
feat(soma,morfo,eidos): M1(ii) tooltip — los tres eventos existen por fin, y el silencio queda donde estaba firmado
El morfo declaraba emerge-present / emerge-dismiss / emerge-dismiss-escape y
nadie los emitia: el SILENT del pack nunca casaba, la puerta de temas que su
propio docblock documenta no podia funcionar, y el preset present-rise que
declara `event: ['emerge-present',...]` jamas pintaba. La excepcion del censo
lo decia desde agosto: «Today's silence is accidental, not the declared
design».
El re-analisis pedido por el autor cazo lo que la primera propuesta no vio:
la firma present-rise es GLOBAL (base.css:6520), la entrada por data-state es
el workaround de la emision ausente (el comentario del provider lo dice
literal), y la semantica autorizada distingue hover (entrada) de focus
(instant-open, «appears with no entrance — its semantics») — cosa que el
evento no distingue. De ahi la opcion B firmada: el estado conserva la
entrada; la emision añade la superficie que faltaba.
Cableado con la receta F4: `present` pre→post — y SIN su `commits` (fijaba
'delayed-open' y pisaria el 'instant-open' del camino focus; los overlays
binarios conservan el suyo porque su valor es total, desviacion declarada);
dismiss ×2 ganan `targetFallback: [trigger]` (Presence sostiene el content por
la salida; el fallback cubre la carrera). Provider: emision SOLO en
transiciones reales — un open-timer cancelado no presenta nada que retirar, un
re-hover abierto no re-emite — y `handleClose` lleva la causa para distinguir
el Escape.
MEDIDO en navegador, los tres caminos y la fuga exacta:
hover present CONTENT · entrada autorizada corriendo (scale-in+fade-in),
present-rise NO — el preset gana la cascada; miedo al doble
movimiento REFUTADO en el camino delayed
focus present CONTENT con instant-open y present-rise CORRIENDO — la fuga
predicha, confirmada → silencio visual MINIMO en la receta, scoped a
[data-state='instant-open'][data-event-phase='active'];
re-medido: anims [] y la entrada delayed intacta
leave dismiss CONTENT via Presence · salida autorizada (scale-out+
fade-out), dismiss-fade no
Escape dismiss-escape CONTENT · desestampado del hold a ~240ms
Deudas limpiadas en el mismo commit (el guard lo exige): 3 ids de
INERT_EVENT_DEBT + 3 excepciones de EMISSION_EXCEPTIONS. Quedan 4 de la parte
(ii): virtual-list/grid ×3 y gradient-picker commit-reset (S-14).
Verificado: soma navegador 1278✓/1 (el timeout ajeno) · tooltip 4/4 ·
sema+morfo+contracts 552✓/6 ajenos · check 69 = base aislada, diff VACIO ·
prettier: morfo/tooltip.ts limpio en HEAD queda limpio; los 3 avisos ya
fallaban en HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
1c4303b10f |
feat(morfo,sema,contracts): M1(i) — el contrato dice QUIEN dispara cada evento (D.2)
El morfo no podia decir quien emite un evento declarado: todo se presumia del runtime, y la verdad de las excepciones vivia fuera del contrato — en la lista de deuda del guard D9 y en las excepciones del censo de packs. Las tablas de eventos de los README prometian percepcion que no ocurre (la ficha M1, P1). Ejecuta la decision firmada D.2 (IMPLEMENTATION_CONTRACT): - `emission: 'runtime' | 'host' | 'external' | 'declared-only'` en MorfoEvent + literal en el schema sium. Ausente = 'runtime': las ~250 declaraciones existentes no se tocan y la presuncion sigue siendo la norma. - El guard D9 exime por DECLARACION en vez de por lista: solo los 'runtime' exigen emisor. Y si un evento eximido sigue en INERT_EVENT_DEBT, el guard FALLA con «FIXED — remove it», para que la deuda no sobreviva a su resolucion. - El pack-census gana el chequeo simetrico: una regla cuyo alcance son SOLO eventos 'declared-only' afina una percepcion que jamas estampara — muerta por definicion, sin lista de excepciones. 'host' NO cuenta como muerto: el anfitrion dispara por el runtime y la regla casa normal. Fixture con los dos casos (positivo y negativo con hermano runtime al alcance). - `morfo.md` §Step 5.5 lleva el apendice tecnico en los terminos que D.2 exige: la tabla de los cuatro valores × sus guards, y la nota doctrinal de por que NO es doctrina del libro — marcar un evento inerte como declared-only para callar al guard es ensanchar la deuda con otro nombre. La sonda temporal (flag sobre tooltip.emerge-present → correr → revertir) destapo un agujero real y lo cerro: el flag entre `name` y `semantic` hacia INVISIBLE el evento al regex del guard — ni censado ni exento. El regex tolera ahora la linea opcional, y la sonda termino dando la conducta disenada exacta: exencion + exigencia de limpiar la deuda. NADIE estrena el flag: los 7 de INERT_EVENT_DEBT quedan intactos y son la parte (ii) — siete decisiones perceptuales del autor, una a una (la de tooltip: ¿el componente mas ubicuo merece firma, o silencio declarado?). La instancia original de la ficha (los 5 pickers con close inerte) esta muerta desde la normalizacion de nombres. Verificado: suites 945 ✓ / 6 ajenos · docs 0/624 · check 69 = base aislada, diff VACIO · prettier: mis dos limpios siguen limpios, pack-census formateado (regresion mia), contracts y morfo.md ya fallaban en HEAD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
846d201693 |
fix(eidos): E2+E3 — el modo de sistema pasa por ActiveDom y un re-derive fallido ya no desviste la pagina
E2. `createSystemColorSchemeSource` usaba `globalThis.matchMedia` +
`addEventListener` crudos: la regla de la casa (listeners de window/document
pasan por ActiveDom) y la ventana EQUIVOCADA en iframe/popup — globalThis es la
global, no la del dom. Ahora, con dom inyectado, el media query sale de
`dom.getWindow().matchMedia` y la suscripcion va por `dom.listen` (ciclo de
vida gestionado); sin dom, el camino crudo queda de fallback (serializacion
CSS / SSR). Descartada la opcion preferida de la ficha (tracker en adom,
espejo de prefers-reduced-motion) por la regla de ≥2 consumidores: leidos los
6 boots reales de web/, TODOS pasan su propio modeSource — el camino de
sistema tiene hoy cero consumidores vivos; si algun dia gana un segundo, se
promociona, y queda dicho en el comentario.
E3. `#renderSchemeCss` atrapaba el error del re-derive y devolvia '' — y
`apply()` lee '' como «sin esquema» y BORRA el <style> anterior: un cambio de
modo con semilla que no deriva no solo fallaba sin log, desvestia la pagina
del bloque que ya estaba bien puesto. Ahora `#lastSchemeCss` conserva el
ultimo bloque bueno, el catch avisa por `#uix?.logger.warn('eidos.scheme',…)`
(la superficie que ya usa sema; eidos no tiene logger propio), y retirar el
spec sigue limpiando. `applyColorScheme` no cambia: construye EAGER y una
semilla invalida sigue reventando en la cara del llamador — el silencio era
solo del re-derive.
Dos hallazgos del proceso, anotados: el validador de config es FAIL-CLOSED y
rechaza escalas donantes rotas en la puerta (por eso el rojo de E3 no puede
fabricarse via config: la inyeccion va sobre `buildScheme` mismo, passthrough
real hasta que el flag del test lo revienta — el sitio exacto del throw que la
ficha nombraba); y el stub de matchMedia del test de E2 debe dar un mql POR
QUERY, porque el tracker de reduced-motion del propio dom consulta la misma
ventana.
Ambos tests vistos MORDER el codigo viejo (stash de la implementacion, no del
test): E2 porque el query de color iba a la ventana global (jsdom: sin
matchMedia), E3 porque el bloque desaparecia.
Verificado: eidos 28/28 · sema+morfo+eidos+contracts 943 ✓ / 6 ajenos · check
69 = base aislada, diff VACIO · prettier: active-eidos.svelte.ts estaba limpio
en HEAD y queda limpio; el test ya fallaba en HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
58695cd08a |
fix(morfo): M4 — la pareja [true,false] solo se infiere donde es exacta
`compileDataPlan` precalculaba el par [etiqueta-true, etiqueta-false] para todo
stateRef+enum con `falseLabel = values.find(v => v !== trueLabel)`: exacto en un
enum BINARIO (el otro miembro, da igual el orden) y dependiente del ORDEN de
declaracion desde 3 valores.
El diagnostico de la ficha se corrigio dos veces con medida:
1. «Hoy no muerde porque los stateRef booleanos usan enums binarios» — falso de
premisa: hay 12 declaraciones con 3+ (aura x3, checkbox x2, image, meter x2,
progress x2, tooltip x2). Pero tampoco muerden, por OTRA razon: bindean
strings y el runtime solo usa la pareja con `typeof raw === 'boolean'`. En
los 12 la pareja se calculaba y jamas se usaba.
2. El arreglo de la ficha («stateRef+enum exige exactamente 2 valores») habria
PROSCRITO esas 12 declaraciones legitimas — la misma clase de error que M5.
El peligro real es el emparejamiento booleano↔enum-no-binario, hoy inexistente,
y el arreglo lo hace imposible EN SILENCIO: con 3+ valores la pareja no se
calcula, un string pasa intacto (la clase viva), y un booleano LANZA
MorfoInvariantError nombrando componente-attr y la salida declarativa — que no
es un campo nuevo sino `v.mapRef(source, { true, false })`, que ya existia en
el vocabulario. Doctrina S-09: un path malo lanza, no inventa (la alternativa
era estampar `data-state="true"`).
Del re-analisis, dos rectificaciones propias que quedan anotadas: mi primera
propuesta invocaba un «guard de contratos» que NO existe (el mecanismo honesto
es el throw), y mi censo de bindings verifico 2 de 12 — la prueba real es la
suite entera: soma navegador 1278✓/1 (el timeout ajeno) con la pareja ya
retirada de los 12, ninguno lanzo.
Tests nacidos en rojo (2/3; el binario paso porque es la conducta de hoy):
binario exacto con orden invertido · 3+ con string = passthrough · 3+ con
booleano = throw con /mapRef/.
Verificado: compile 44/44 · sema+morfo+contracts 550 ✓ / 6 ajenos · soma
navegador 1278✓/1 · check 69 = base aislada, diff VACIO · prettier: ambos
ficheros ya fallaban en HEAD, no se reformatean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b53c93e422 |
feat(morfo,sema): M5 — el matcher `state` habla el contrato de datos del morfo
El matcher aceptaba pares de strings sueltos con la promesa escrita de que «una
iteracion futura los tipara contra el contrato». La iteracion es esta, y no es
UNA puerta sino DOS — el precedente de dos ejes del framework
(allowedTargets/targetFallback, intentRequirement/intentGuidance):
- `state` se tipa con `DataPairOf<M>`, la union discriminada derivada de
`parts[].data`: attr declarado, y donde hay enum, valor del enum. Un typo en
`data-last-action` deja de compilar — la clase de deriva que semaSelector
existe para matar. La union atraviesa intacta el idioma
`Parameters<typeof semaSelector<M>>[2]` de los 67 packs: cero migraciones.
- `undeclaredState` es la puerta ABIERTA con nombre: los attrs que el morfo no
declara A PROPOSITO (data-size/data-sheet del dialog, eidos-only por
docblock). La regla que la usa dice lo que hace, en vez de colarse por un
string abierto. Un solo slot no podia imponer enum Y quedar abierto — la
rama abierta se traga a la estricta (la clase S-11).
El gate de disyuncion vive en el BUILDER, no en el censo (desviacion declarada
del punto 4 firmado, a mas fuerte): attr declarado por la puerta abierta lanza,
attr no declarado por `state` lanza, valor fuera de enum lanza — y como los
packs son modulo, revienta al IMPORTAR: ninguna suite queda verde encima.
`aria` queda abierto con la razon real escrita: su unico uso en el framework
casa el `role` del dialog, que el morfo deliberadamente no declara
(variant-dependent, lo pone el provider).
Nacidos en rojo: 4 tests runtime (enum, attr no declarado via state, attr
declarado via puerta abierta, puerta abierta funcionando) + probes
`@ts-expect-error`. Migrados los 2 usos de dialog y los 3 tests de escaping que
usaban attrs no declarados. Una arista de implementacion documentada: dentro
del cuerpo generico `DataPairOf<M>` es condicional diferido y TS no deja leer
`.attr` — una lectura estructural local, como ya hace el resto del builder.
⚠️ Nota de proceso: mi primera propuesta fue un guard de censo + corregir el
comentario — el mismo error que S-33 (lint donde el tipo puede hablar), y cai
en el en el mismo dia. El re-analisis contra las decisiones del framework lo
invirtio. La ficha M5 queda anotada como INCOMPLETA, no refutada.
Verificado: selectors 15/15 · sema+morfo+contracts 547 ✓ / 6 ajenos · check
69 = base aislada, diff VACIO · docs 0/624 · prettier: dialog.ts era mio y
queda limpio; selectors.ts/test/index ya fallaban en HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
13246a2c23 |
refactor(sema,morfo)!: M6 — los alias deprecados mueren; un nombre por concepto
`IntentExpectedFamily` y `SemaEventLabel` eran alias de compatibilidad de `IntentRequiredFamily` y `SemaEventKey`, y el codigo nuevo seguia importando el nombre viejo — la mitad de los tipos hablaba el vocabulario de antes del renombrado. La regla dura del repo (no backward-compat shims: actualiza los consumidores y borra el camino viejo) decide el destino: 13 usos migrados a los canonicos y los DOS alias borrados del tipo y del barrel. La unica duda doctrinal se resolvio antes de tocar: D.3 (book-deviations:489) nombra `IntentExpectedFamily`, pero DESCRIBE la implementacion con el nombre que existia entonces — registro historico, no prescripcion. El precedente es la casa misma: se ha renombrado vocabulario entero sin reescribir los registros de decisiones. Ademas, y declarado en la exposicion: `semaIntentExpectedFamilySchema` (const local de morfo/schema.ts, mismo vocabulario viejo) pasa a `semaIntentRequiredFamilySchema` — y el guard S-34 de contracts.test.ts, que lo busca por nombre LITERAL, se actualiza en el mismo commit; separarlos habria dejado el guard ciego un commit entero. Los 2 `as never` de compile.ts se retiran: eran vestigio de antes de que existiera `DepSink` (un `() => void` encaja en `(value: string) => void`), y el compilador lo confirma. Y `_resetCompileCache`, tercera pata de la ficha, resulto YA borrado — cero apariciones en src/; se anota para que no vuelva a la cola. Censo previo al borrado: cero consumidores de los alias en src/, web/ y scripts/ fuera de los 13 migrados. Las funciones `is/parse/toSemaEventLabel` y el tipo `ParsedSemaEventLabel` conservan su nombre: son API propia, no el alias, y su renombrado quedo explicitamente FUERA de lo firmado. Verificado: sema+morfo+contracts 541 ✓ / 6 ajenos · check 69 = base aislada, diff VACIO · prettier: types.ts era mio y queda limpio; compile.ts y contracts.test.ts ya fallaban en HEAD y no se reformatean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4e7d0be670 |
fix(sema,docs): S-19 — el puerto de anuncios encaja con su sumidero sin adaptador
`AnnounceFn` pedia la prioridad en una bolsa de opciones y `ActiveUix.announce`
la toma POSICIONAL, asi que el cableado que la doctrina prescribe —una sola
region viva compartida, este canal como uno de sus dos emisores— no se podia
escribir: `{ announce: uix.announce }` no compilaba. Y forzarlo era peor que no
tenerlo: el sumidero recibia un OBJETO donde lee una prioridad,
`liveRegionIds[obj]` es `undefined`, y TODO aterrizaba en la region polite — un
`threat` dejaba de interrumpir, que es justo lo que un aviso critico no puede
hacer.
La firma pasa a posicional, como el sumidero y como la fuente equivalente de
soma. Tres puertos, una sola forma. Cambio de conducta: CERO — el canal es
opt-in y ninguna raiz lo enciende hoy.
Visto en rojo antes de tocar: un test con la forma EXACTA de `uix.announce`
(`(message, priority?, timeout?)`) recibia `{priority:'assertive'}` en el hueco
de la prioridad. Tres aserciones existentes migran de bolsa a posicional — su
contrato cambia, y era el contrato defectuoso.
⚠️ Mi analisis inicial estaba equivocado y el autor lo mando revisar. Habia
concluido que el canal estaba muerto porque el runtime de soma no mete `message`
en la señal. No lo mete, cierto, pero es DISEÑO: son dos emisores para dos
publicos —soma cubre sus componentes, el canal cubre a quien usa
`EngineSemantic` sin soma y escribe su propia señal— y precisamente por eso
nada se anuncia dos veces. La propuesta de fusionarlos habria roto ese diseño;
retirada antes de escribir una linea.
`sema.md` §announce reescrito: fuera el aviso del adaptador (ya no hace falta),
dentro la razon de los dos publicos, y el aviso que SI queda — ninguna raiz
enchufa el canal, y encenderlo sin inyectar el anunciador añadiria un segundo
par de regiones vivas junto al compartido. Eso es decision aparte, sin firmar.
Verificado: announce 6 ✓ · sema+morfo+contracts 541 ✓ / 6 ajenos · check 69 =
base aislada, diff VACIO · docs 0/624 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
455260ca34 |
fix(sema): S-30/S-38 — las cinco reglas hapticas abren la puerta que habian olvidado
Cinco reglas escribian una firma haptica sobre familias cuyo activeChannels es
['sound'] — alertdialog (pulse 0.7/60), sheet movil (tap 0.4/24), editable
shift-enter-mode, stepper shift-step y timeline emerge-reveal. El resolver
instalaba la firma, HapticChannel salia por la puerta de entrada: vibrate() 0
veces desde su nacimiento, con la intencion comentada en los propios packs.
La decision se tomo contrastando con el mundo real, no por limpieza: mi
propuesta inicial era borrar los bloques y el autor la corrigio con las
preguntas correctas — ¿que hacen las plataformas? ¿quien decide? Verificado en
las fuentes: Apple HIG prescribe haptic de warning cuando aparece una alerta
importante y tick de seleccion para cambios de valor discretos; Android pide
moderacion pero con constantes CONFIRM/REJECT. O sea: el alertdialog y el
stepper SON patrones de plataforma, y la intencion escrita en los packs era
diseño, no deriva. El framework ademas ya deja la ultima palabra a quien toca:
la puerta por regla (`channels`), la per-emit, y la haptica entera es opt-in
del producto — ensanchar estas reglas no impone vibracion a nadie.
Arreglo: `channels: ['sound', 'haptic']` en las cinco, cada una con su razon
perceptual escrita. Los 5 waivers de CHANNEL_EXCEPTIONS se retiran: el guard
del censo (punto 2, que estos waivers silenciaban desde 2026-08-06) queda
re-armado y ES el aviso automatico para la proxima vibracion con puerta
cerrada. El aviso del waiver («widening requires completing the firma») estaba
ya pagado por la defensa S-31 de kindToPattern: un {kind:'tick'} parcial
resuelve a valores finitos, nunca vibrate(NaN).
Visto en rojo DOS veces antes de verde: el censo sin waivers listando
exactamente las 5, y el e2e nuevo de resolver.test.ts (resolucion con el pack
real + HapticChannel real: 1 vibracion por regla, patron finito) ejecutado
contra los packs sin ensanchar via stash.
Verificado: censo 67/67 · resolver 30/30 · sema+morfo+contracts 540 ✓ / 6
ajenos · check 69 = base aislada, diff VACIO · prettier limpio · docs 0/624.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
6a75544158 |
fix(sema): S-35 — la etiqueta deja de regalar lo que la forma estructurada exige
`SemaEventKey` incluia la familia PELADA para las ocho familias, asi que
`'commit'` era un `SemaActionEvent` valido aunque `commit` declare
`intentRequirement: 'required'`. La rama estructurada si cerraba —el
`@ts-expect-error` sobre `{ family: 'commit' }` se consume— y la de string
regalaba lo mismo: `normalizeSemaEvent('commit')` devolvia `{ family: 'commit',
intent: null }`, justo el documento que la politica declara invalido.
El tipo pasa a expresar la subordinacion que su propio docblock ya prometia
(«subject to SEMA_FAMILY_POLICY»): la forma BARE solo donde la politica no
REQUIERE intent, la forma PAREJA donde no lo PROHIBE. Lo segundo va mas alla de
lo que pedia la ficha y se declara: es la simetria de S-33, cuesta cero hoy
(`IntentForbiddenFamily` es `never`) y se activa sola el dia que una familia
pase a `'forbidden'`.
Y el runtime se cierra por el MISMO mecanismo, sin tocar `validation.ts`:
`SEMA_EVENT_LABELS` se deriva ahora de la politica en vez de listar todas las
familias, `LABEL_SET` sale de ese array, `isSemaEventLabel('commit')` pasa a
`false`, y el retorno temprano de `validateSemaEvent` deja de tragarselo — cae a
`isSemaEvent` y lanza. Cero codigo muerto anadido: la rama gemela de
`validation.ts` sigue siendo inalcanzable y sigue siendo hallazgo aparte.
⚠️ Un test CAMBIA de contrato, y era el que fijaba el defecto:
«SEMA_EVENT_LABELS includes family-only forms for every family» afirmaba
exactamente lo que S-35 cierra. Ahora deriva de la politica y afirma tambien el
negativo (`'commit'` y `'signal'` NO son claves). El array pasa de 56 a 54
entradas.
Alcance medido antes de tocar, y mas amplio que el de la ficha: `SemaActionEvent`
no tiene consumidores fuera de sema, y ademas `normalizeSemaEvent`,
`toSemaEventLabel` y `validateSemaEvent` NO los llama nadie en produccion — sus
unicos call sites son tests. Es superficie publica sin consumidor interno; la
pregunta de si ese vocabulario se gana el sitio queda ABIERTA como item propio,
no la decide este commit.
Verificado por METODO: base aislada 69 errores, con mis cambios 69, diff VACIO ·
sema+morfo 539 ✓ · contracts 6 fallos = los 6 ajenos · prettier: `event.ts`
estaba limpio en HEAD y lo dejo limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
1a174d5a6e |
fix(sema,morfo): S-33 — el eje de intent declara tres valores y ahora implementa tres
`IntentOptionalFamily` era `Exclude<SemaFamily, IntentRequiredFamily>`: una
derivacion de mundo abierto —«todo lo que no es required»— que se tragaba el
tercer valor en silencio. Una familia marcada `'forbidden'` caia en el cubo
OPCIONAL, donde el intent esta PERMITIDO, asi que el dia que el libro endurezca
`contact` (lo anticipa el propio docblock) se editaria una linea del const
creyendo cerrada la puerta y `contact + threat` seguiria compilando.
Los tres cubos se derivan ahora POSITIVAMENTE, uno por valor del eje, y detras
va la rama `{ family: IntentForbiddenFamily; intent?: never }` en `SemaEvent` y
en `MorfoEventSemantic`. Coste hoy: CERO, medido — `IntentForbiddenFamily` es
`never`, asi que la rama esta deshabitada y no toca la puerta de compilacion de
los 172 morfos. Lo que cambia es que la promesa del canon («editing the const
reshapes the discriminated unions») pasa a ser cierta.
Runtime: `isSemaEvent` gana la condicion simetrica. Y aparecio una arista que la
ficha del audit no vio — leer `SEMA_FAMILY_POLICY[f].intentRequirement` directo
ESTRECHA a `'required' | 'optional'` (no hay familia forbidden hoy), asi que la
comparacion no compila: «no overlap». Anotar el `const` no basta, el flujo lo
vuelve a estrechar. De ahi `intentRequirementOf(family)`, cuyo tipo de retorno ES
el eje; queda documentado en su docblock para que nadie lo deshaga.
⚠️ Desviacion DECLARADA de la receta firmada: NO se toca `validation.ts`. La
receta pedia la condicion simetrica «en los dos guardianes», pero medido antes de
escribir nada, la rama de politica que ya existe alli es INALCANZABLE:
`validateSemaEvent({family:'commit'})` lanza el mensaje generico «is not a valid
canonical semantic event», porque `isSemaEvent` rechaza aguas arriba. Anadir la
simetrica seria anadir mas codigo muerto. Va como hallazgo aparte.
Test nuevo `intent-policy.test.ts`: el probe de tipos con una replica ENDURECIDA
del const (tres `@ts-expect-error`, uno de ellos el que ayer se quedaba sin
consumir), la particion de los tres cubos contra el const, y el guard de runtime
derivado del const — vacuo hoy A PROPOSITO, vivo el dia que una familia pase a
`'forbidden'` sin que nadie tenga que acordarse.
Verificado por METODO, no por cifra: base aislada con stash = 69 errores, con mis
cambios 69, diff VACIO. sema+morfo 537 ✓ (532 + 5 nuevos) · contracts 6 fallos =
los 6 ajenos de la base · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
cb1fd54d10 |
docs(process): handoff nuevo para la cola de auditorias, con el protocolo de trabajo delante
El eje perceptual quedo completo hoy (F1-F6), asi que lo que sigue ya no es «su» cola: es la cola de auditorias — sema, fable, blocks, eje B, chronos C5. Este handoff la lleva, y pone PRIMERO lo que cambio hoy de verdad: el protocolo. §0 · las cinco preguntas del autor antes de CADA accion, un item por mensaje, y parar a esperar firma. Sin puerta que permita auto-autorizarse lo mecanico: lo propuse y se corrigio en el acto. Con las dos precondiciones que hoy costaron dos rectificaciones completas — el SPEC del COMPONENTE es doctrina y se busca ANTES de diagnosticar (chronos), y se barre `docs/` antes de proponer retirar nada (S-33 esta en CANON.md y en la D.3 firmada). La regla que las resume: el codigo nunca corrige al canon; se le pone al autor la contradiccion delante. §1 · S-33 FIRMADO y sin ejecutar, con la receta y la evidencia del probe de `tsc`: el defecto es el `Exclude`, no «falta una rama». Derivar los tres cubos positivamente cierra la puerta sola al flipar el const, y la tercera rama es inerte hoy porque se teclea sobre `never` — medido, delta de conducta CERO. §2 los 6 commits de hoy · §3 las tres decisiones abiertas con su evidencia ya medida (S-35 · S-30/S-38 · S-19, esta ultima partida en dos porque (ii) cambia conducta) · §4 la cola en orden · §5 la base de verificacion, con los 6 fallos ajenos de contracts nombrados uno a uno · §6 las trampas medidas hoy, para no re-descubrirlas: la laxitud DELIBERADA del guard D9 (endurecerlo convierte 27 emisiones reales en violaciones), el panel oculto congelando la linea de tiempo, y los ficheros ya sucios en HEAD para prettier. docs:check 0/624. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
eda3b977b1 |
test(contracts): S-34 — el validador de runtime queda atado al const de politica de intent
CLAUDE.md declara `SEMA_FAMILY_POLICY` fuente unica y promete que «editing it reshapes the discriminated unions». El lado del TIPO lo cumple (`IntentRequiredFamily` deriva del const); el lado del RUNTIME no: el esquema sium que valida los 172 morfos reescribe la misma particion con literales fijos (`morfo/schema.ts:205-216`). Hoy coinciden literal a literal — no hay bug vivo — pero editar el const moveria la puerta de compilacion y dejaria la de runtime en la particion vieja, en silencio. Es la clase de deriva que costo dos meses y medio con `form.*`: copiar el canon en vez de leerlo. El arreglo «obvio» —derivar el esquema del const— se descarta con su razon: eso compra una importacion de VALOR morfo → sema, y esa arista es hoy deliberadamente solo de tipos (morfo es «the contract / DNA; pure TypeScript, no runtime», theming/channels.md §3). Pagar acoplamiento de runtime entre capas para cerrar una deriva latente es mal cambio. Los dos lados se atan donde YA conviven: el test de contratos entre capas. El guard expande las uniones del esquema (incluida la anidada `transitional`) y las compara con las dos particiones derivadas del const. Visto FALLAR antes de darlo por bueno: moviendo `handle` a `required` en el const, el guard reporta «expected ['commit','signal'] to deeply equal ['commit','handle','signal']». Verificado: contracts 6 fallos = los 6 AJENOS de la base, sin nuevos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
14a169357b |
fix(contracts): el guard D9 mira tambien la vista de eidos, que en los componentes-vista ES el emisor
El guard de eventos inertes construia su blob con `soma/components/<comp>` mas
libs/datetime/layers. Para los componentes-vista (metrics, menu-dial,
onion-menu) no hay clase provider: la vista de eidos registra las partes y
emite. Censados los 255 eventos declarados, son 4 los que se emiten SOLO desde
eidos — menu-dial.commit-select, onion-menu.commit-select y los dos de drill de
onion-menu.
Dos de ellos pasaban por casualidad: el nombre estaba entrecomillado en un
COMENTARIO del factory de soma. Al reescribir ese docblock en
|
2 months ago |
|
|
44af38e6e8 |
fix(sema): S-10 — el canal visual honra los servicios de su propia bolsa
`new EngineSemantic({ visual: { dom } })` y `{ visual: { projector } }` —dos
configuraciones que el TIPO publica— reventaban el boot con SemaConfigError: la
rama visual hacia spread de la bolsa del canal y despues asignaba `dom`,
`projector` y `timers` con los del engine SIN `??`, asi que un servicio puesto
en la bolsa quedaba sustituido por `undefined` y VisualChannel tiraba.
Era la unica de las cuatro ramas que lo hacia: sound (`soundOptions.dom ??
opts.dom`), announce y haptic ya implementan «anidado ?? raiz» — override por
canal, default del engine. Asi que esto no cambia doctrina, la restaura.
Descartada la alternativa fuerte (estrechar el tipo con Omit<..., 'dom' |
'projector' | 'timers'> para hacer inexpresable el estado ilegal, la doctrina
que F3 acaba de aplicar): aqui romperia la simetria, porque en los otros tres
canales la bolsa anidada SI es una via legitima de override. El defecto es que
visual no la honraba, no que exista.
Matiz que la ficha del audit no vio: la documentacion nunca bendijo la bolsa
anidada para los servicios — `sema.md:668` documenta `{ dom, visual: {
defaultHold } }`, servicio en la raiz. Quien bendecia la configuracion que
revienta era el tipo. Es un desajuste tipo↔conducta.
Test nacido en ROJO con el error exacto («VisualChannel requires a dom service
or projector»), cubriendo las dos rutas anidadas y la de raiz.
Verificado: sema+morfo 532 ✓ · check 69 = base, 0 propios.
⚠️ contracts.test.ts pasa de 6 fallos ajenos a 7, y el septimo es MIO pero de
otro commit: el guard D9 daba onion-menu.emerge-expand/collapse por emitidos
porque encontraba el nombre entrecomillado en un COMENTARIO del factory de
soma; al reescribir ese docblock en
|
2 months ago |
|
|
a157454913 |
docs(process): la 6a fila de chronos no era un defecto — decision revocada por el SPEC
El item llevaba un mes en la cola como «la 6a fila fantasma» y estaba FIRMADO para arreglarse. Al ir a ejecutarlo aparecio el SPEC del propio componente, que dice lo contrario en TRES sitios coordinados: rejilla de 6 filas constante «estabiliza la altura» (SPEC.md:270), inventario de reuso pidiendo «rejilla de mes 6x7» (:492), y `data-outside-month` como estado declarado de `day-cell` CON su propio token de tema (:311 y :461). Las celdas de fuera de mes son diseno, no relleno. La asimetria con la familia —chronos `true`, los otros cuatro `false`— se leyo como deriva y es al reves: chronos es el unico que escribio su decision, y coincide con el default de sus dos referentes de libreria, verificados en la fuente y no citados de memoria: FullCalendar `fixedWeekCount` viene `true` («the calendar will always be 6 weeks tall») y Toast UI `month.isAlways6Weeks` viene `true`. Google/Apple/Outlook si varian las filas, pero porque son aplicaciones que llenan el viewport; chronos no tiene contrato de altura, asi que la receta propuesta tenia que inventarse la altura con `calc(6 * cell-min-block)` — la altura de 6 filas. Era la misma altura con 5 filas mas gordas, y menos informacion. Medido antes de opinar: hoy son 115 px por fila y 724 px de rejilla en todos los meses; con `fixedWeeks:false`, jul→dic 2026 da 609/724/609/609/724/609, o sea el salto de 115 px que la decision queria evitar. Y crecer las filas no ensenaria ni un evento mas: `maxLanes` es `$state(3)` fijo. Corregidos ademas dos errores de la ficha original: el default vive en DOS sitios (chronos.svelte:20 y engine/state.svelte.ts:64), y chronos.css:278 no es la altura de las filas del mes sino la pista de carriles de chips dentro de una fila. La conducta de Google queda anotada como FUNCIONALIDAD con firma propia (contrato de altura + maxLanes derivado), no como flip de flag. docs:check 0/623. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
c00e97b91a |
docs(process): F5 cerrada — los tres sitios de las tres auditorias, medidos DESPUES de la campana
F5 no era «volver a medir cada ola»: es la comprobacion de regresion del eje
entero sobre los tres sitios que fable S1, sema S-17 y blocks A-36/A-65
encontraron por separado — y ahora con la API de emision ya borrada.
site-header 375px contact-activate TRIGGER +8,4ms · emerge-open CONTENT +10,3ms,
press-squeeze corriendo SOBRE el trigger, close en el content
knob handle-drop y commit-set separados por 245,4 ms
dropdown+Button +109 / +112,7 ms, close en content, commit-select en el item
PULSADO («Log out», 3.º de 3)
El numero que cierra S-17 es 245,4: el commit encolado entra justo tras el hold
de 240 del drop — ni aplastado a los 24 ms que S-17 midio, ni 1,6 s tarde como
midio el falso verde de `queue`. Y los tres sitios son la misma geometria: un
elemento con DOS morfos (trigger+button, control para drop+commit).
Anotados los dos artefactos del entorno para que no vuelvan como hallazgos: con
el panel sin componer, rAF esta suspendido (el handle-drag continuo del knob no
estampa nunca) y la linea de tiempo de las animaciones no avanza (el
desestampado cae SIEMPRE en el tope de 1500 ms de la espera de expresion, de ahi
los ~1,7 s de limpieza en las tres trazas). getAnimations() dice que corre sobre
que; para duracion de pintura hace falta Chrome real.
docs:check 0/623.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b8aa333fd1 |
feat(soma,morfo)!: targetOverride BORRADO — el estado ilegal del eje es inexpresable
El endgame que §3.0 firmo al abrir F3: con el censo a 0, la opcion de elemento anonimo sale de TriggerOptions, y con ella assertTargetOverride (existia solo para vigilarla; queda assertAnchor, el guard de la puerta que la sustituye). Las dos resoluciones `opts.targetOverride ?? resolveEmitTarget(...)` (emit y foco a11y) quedan en la resolucion unica, y los Omit<..., 'targetOverride'> de los triggers anclados se vuelven vacuos y desaparecen. El tipo lleva LAPIDA doctrinal: la opcion nacio fallbackTarget, se renombro el 2026-08-10 como precondicion del censo, y compensaba un registro sin identidad de instancia — es lo que dejo a tres auditorias (fable S1, sema S-17, blocks A-36/A-65) encontrar la misma deriva sin poder cerrarla. No volver a añadir una opcion de elemento. Y la tesis se probo sola al ejecutarla: el compilador, ya como censo, cazo 9 usos en TESTS que el grep del scratch nunca escaneo. De ellos, 3 eran redundantes (la parte registraba el mismo elemento), 2 del test de metrics migran al registro + trigger plano, y 4 fijaban la conducta borrada — el describe «targetOverride contract» entero y el test del override preferente, retirados con lapida; el guard A-36 de los overlays (morfo sin redireccion + post) se conserva, des-anidado. El scratch del censo, retirado: el censo es `npm run check`. Docs vivos adjudicados: morfo.md §allowedTargets describe la puerta anclada (EventNameTargeting + assertAnchor) y §repeated-part nombra partInstance donde decia targetOverride; los docblocks de las 3 factorias de vistas de eidos dicen la forma nueva; errors.ts deja de ofrecer la opcion muerta como remedio. Las menciones historicas (A-36, «the retired...») se conservan como historia. Verificado: soma navegador 1278✓/1 (el timeout ajeno preexistente de soma-attr-audit, documentado en la base) · runtime+metrics 53/53 · check 69 = base, 0 propios · docs:check 0/623 · prettier limpio en lo que estaba limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4161a23d66 |
refactor(morfo,soma): ola 7 de F3 — palabras, pagination y toolbar: el censo llega a CERO
palabras (11 sitios, el diseño fino que el handoff reservaba) resuelto por clases, honrando las declaraciones salvo donde la propia declaracion mentia: - TERMINALES ya declarados en el provider (save-content ×2, restore-history ×2, reset-content) y no-ops (set-check ×2 con e.currentTarget = el content, toggle-drawer ×2 con el ref propio del drawer, signal-warn-invalid): override retirado. - CHROME que puede no estar montada (link-editor, find-replace, slash-menu): el fallback al provider que el codigo hacia a mano pasa a targetFallback en el morfo. Es estado de montaje — la API publica corre el mismo comando sin panel abierto — no eleccion del llamante. - commit-set-format: el morfo declaraba `command-button` como TARGET, y eso es un destino que solo acierta en uno de sus dos caminos (un ctrl+B no tiene boton). Invertido a la forma del precedente rating-group: target = provider (donde vive el valor) + allowedTargets = [command-button] (lo que el gesto nombra), con un unico triggerFormat() que discrimina por identidad. La conducta no cambia — boton al boton, teclado al provider — pero ahora la declaracion dice la verdad. Y con ellos caen los dos ultimos redirigidos DECLARADOS, que ya no necesitan elemento: toolbar group-item emite por su propio handle, y los cinco controles de pagination entregan su emisor anclado en vez de un HTMLElement (puerto estructural PaginationCommitAnchor; un setPage(3) programatico sigue cayendo al target declarado). Sus tests dejan de fabricar eventos sinteticos. CENSO: 0 UNDECLARED, 0 DECLARED. Ningun elemento anonimo viaja ya por la API de emision, que era el defecto de clase que abrio F3. Verificado: palabras 575/575 · pagination+toolbar 6/6 · check 69 = base, 0 propios · prettier limpio en lo propio (el provider de palabras ya fallaba en HEAD; no se reformatea). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e1cedb81ba |
fix(soma): el dropzone de file-upload deja de robar clicks ajenos — un gesto, una ocurrencia
Lo habia dejado ANOTADO en la ola 6 por no cascadear, y estaba mal clasificado: es la clase que este eje persigue (una ocurrencia por gesto), encontrada por mi propia medicion y en un fichero que ya estaba editando. Y al medirlo de verdad, el mecanismo resulto MAS ANCHO que el diagnostico de la nota. No es solo el Trigger burbujeando: el <input type=file> oculto vive tambien DENTRO del Dropzone, asi que el click() SINTETICO que dispara openPicker vuelve a entrar por el mismo manejador. Medido en navegador: UN click en el dropzone llegaba DOS veces, la segunda con target = INPUT[hidden-input] — solo el guard de reentrada que la plataforma pone en click() lo paraba ahi. O sea que TODO click al dropzone duplicaba, no unicamente el del boton. Arreglo: guard por PROPIEDAD en el onclick del dropzone — `closest` sobre los marcadores de `trigger` y `hidden-input`, construidos con partMarkerAttr y nunca a mano, para que un renombrado de parte rompa aqui en vez de ensanchar el alcance en silencio. Es el idioma que table ya practica. Test visto FALLAR con la anidacion real del DOM (trigger + hidden input dentro del dropzone) y los tres casos: la superficie propia abre, el input oculto no re-abre, el boton no re-abre. Medido despues en navegador: click en el dropzone → UN contact-trigger-picker en DROPZONE; click en el boton → UNO en TRIGGER. file-upload 5/5 · check 69 = base, 0 propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4854df3c47 |
refactor(morfo,soma): ola 6 de F3 — las colecciones, y el censo baja a 11 (solo palabras)
Ocho componentes, cuatro clases, cada sitio con la pregunta A-36 primero:
NO-OP DE SINGLETON, retirados: virtual-list ×2 y virtual-grid (viewportRef lo
escribe la propia registracion del viewport) y css-field signal-warn-invalid
(el elemento ERA runtime.partRef('input'), el destino declarado — se lee para
clearTarget, no para redirigir).
ANCLAJE POR IDENTIDAD en las repetidas: menubar trigger · navigation-menu
trigger ×2 + link · tags-input item · file-upload item. En color-field y
time-field no es adorno: DOS partes registran el kebab `input` (grupo visible
+ input oculto), y fieldNode es el ref de la visible, asi que partInstance la
nombra sin depender de quien monto ultimo.
TERMINALES devueltos al provider declarado: tags-input commit-set-add ×3 y
commit-reset; file-upload commit-set-add, signal-warn-reject y commit-reset.
El dropzone que recibe el drop y el boton que ordena el borrado son la
superficie del GESTO, no el sujeto de la ocurrencia — la fila de la doctrina
del sello. Con ellos caen los parametros `target` muertos y sus llamantes.
allowedTargets NUEVO, y es el eje correcto: file-upload contact-trigger-picker
declara [dropzone] junto a su target `trigger`. Es familia contact — el gesto
se sella donde esta la MANO — y las dos superficies son eleccion del llamante
en operacion normal, no estado de montaje.
Medido en navegador: nav-menu abre y cierra sobre el trigger PULSADO
(«Solutions», el 2º de 2, que es la prueba de la identidad) y commit-select
sobre el link «Pricing»; file-upload sella DROPZONE al pulsar la zona y
TRIGGER al pulsar el boton.
⚠️ Hallazgo anotado, NO tocado (preexistente, verificado byte a byte contra
HEAD): pulsar el Trigger de file-upload emite contact-trigger-picker DOS veces
— vive dentro del Dropzone, cuyo onclick no filtra el click burbujeado. Ficha
en el handoff; el arreglo es un guard de propiedad y no se cascadea aqui.
Verificado: suites 46/46 · censo 29 → 11 (solo palabras) · check 69 = base, 0
propios · docs:check 0/623 · prettier limpio en lo que estaba limpio en HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
aa8a630ed2 |
fix(morfo,soma): ola 5 de F3 — select y combobox dejan de sellar el trigger, y present-rise suelta el boton
El superviviente A-36 mas puro de la campaña: el open YA era post (el content
monta antes del emit) pero el provider seguia forzando targetOverride al
trigger — un vestigio de la era pre LEGALIZADO por el allowedTargets del
2026-08-10, que era el eje equivocado (estado de montaje, no eleccion del
llamante). Con el, la firma de motion present-rise corria SOBRE el boton: la
regresion firmada en §1.2 del handoff de normalizacion, medida hoy.
A/B en navegador (la disposicion firmada mandaba medir primero):
ANTES emerge-open TRIGGER · emerge-close TRIGGER · present-rise en el boton
AHORA emerge-open CONTENT · emerge-close CONTENT · present fuera del boton
commit-select → el ITEM elegido («Pear Soft»), anclado por identidad
Receta (la de F4/ola-1): el open pierde allowedTargets y el override; el close
declara targetFallback ([trigger] en select, [trigger, input] en combobox — un
combobox puede vivir sin boton); la seleccion ancla por partInstance al item
registrado, con el nombre computado de ListSelection (SelectionEvent) por la
puerta tipada.
Verificado: suites 24/24 · censo sin un solo sitio de select/combobox ·
check 69 = base, 0 propios · consola limpia (404s = raiz/favicon del arranque).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |