feat(morfo,soma): la emisión deja de aceptar un elemento anónimo
El defecto que tres auditorías encontraron y ninguna cerró no era que N eventos
redirigieran sin declararlo: era que la API de emisión aceptaba un HTMLElement
suelto, compensando que el registro de partes perdía la identidad de instancia.
Censar y arreglar a mano reproduce el modo de fallo. Esto lo hace inexpresable.
F1 — SomaRuntime<M> genérico, SIN default. Nombres de evento, kebabs de parte y
claves de events/actions tipados contra la declaración (EventNameOf /
PartKebabOf / ActionNameOf, los extractores que semaSelector ya usaba del otro
lado). 104 anotaciones migradas + 3 wrappers que devolvían tipado pero tomaban
sources ANCHO. SomaRuntime<Morfo> queda sólo en la tabla de contratos.
F2 — registro por instancia: Map<part, PartRegistration[]>, pertenencia
mantenida por el stream del attachment (poda al desmontar, re-entrada al
remontar), resolución liveReg = la instancia VIVA más nueva. Mata de paso la
clase "la más nueva muerta ensombrece a una viva".
F3 — tres formas legales de destino: el target declarado; el anclaje por
instancia (SomaRuntimePart.trigger, tipado por EventNameTargeting, +
runtime.partInstance por identidad de referencia); y targetFallback, la cadena
por estado de montaje declarada en el morfo y resuelta por resolveEmitTarget —
LA resolución única para validación, emit y foco a11y, que ya no pueden
discrepar. ~30 componentes migrados; censo de targetOverride 148 -> 89.
F4 — A-36 tenía un superviviente. Medido en dropdown-menu: bajo `pre` el `open`
no colisionaba con el contact-activate del Button compuesto, se PERDÍA entero
porque su content no había montado. Aplicada la receta de float-panel a
dropdown-menu Y context-menu. Ahora: contact-activate en trigger, open en
content — dos superficies, ambas expresando.
Submenús mudos, tres componentes. No era cableado: el morfo no declaraba el
evento. onion-menu recibe emerge-expand/emerge-collapse (la forma de TreeView:
un nivel revela sus hijos en el sitio); dropdown-menu y context-menu reciben
sub-open/sub-close sobre sub-content, distintos del open raíz a propósito
porque el sub es su propia superficie flotante. Nacen con la receta A-36 para
no repetirla en código nuevo.
chronos — declaraba 26 partes y registraba UNA; 17 role= a mano y el ARIA
declarado no llegaba nunca al DOM. Se anula la mitad sobre-extendida de la
decisión de 29ffe902a: la reutilización sigue viajando por el motor puro, pero
la ESTRUCTURA se compone como en Table y Calendar. C1 (las 6 partes que emiten,
chronos con cero targetOverride) + C2 (8 singletons) + C3 8/9.
Registrar una parte DESTAPA declaraciones falsas, tres veces en un pase: el chip
declaraba defaultElement button y ponía type= sobre un div (anida el asa de
resize); grid-row declaraba archetype item y pintó la semana entera como fila de
lista; y un aria por propRef desaparece en SILENCIO si el provider no publica la
fuente. En los tres el arreglo fue la DECLARACIÓN, no el DOM.
Verificado: check 74 = línea base medida con stash, DELTA CERO sostenido en cada
lote · 458 pasan (chronos+morfo+sema) · navegador limpio en las tres vistas de
chronos, con el contrato ARIA vivo por primera vez (aria-labelledby resolviendo
a «junio de 2026», aria-readonly/disabled que no se habían emitido nunca).
Handoff en docs/process/CONTINUE-perceptual-surface.md. Aparte, y decidido por
el autor: docs/process/PLAN-event-name-normalization.md — prefijo de familia
siempre; 36 de 252 sin prefijo, cero ambiguos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
# PLAN — un nombre de evento, una convención: `{familia}-{verbo}[-{matiz}]`
|
|
|
|
|
|
|
|
|
|
> **✅ CERRADO 2026-08-12 en `f44d1486d`.** La §6 Aceptación se cumplió entera y
|
|
|
|
|
> medida: **256 eventos declarados / 256 con prefijo de familia / 0 ambiguos**,
|
|
|
|
|
> guard en `validateMorfo` **visto fallar** con un nombre pelado inyectado a
|
|
|
|
|
> propósito, barrido de catálogo sobre los 172 morfos (`components` + `internal`
|
|
|
|
|
> + fixtures), las cuatro escenas de F4 medidas en navegador, `eidos-lint-all`
|
|
|
|
|
> sin inválidos nuevos y `docs:check` en 0.
|
|
|
|
|
>
|
|
|
|
|
> **Lo que este eje DESTAPÓ no es este eje.** El barrido encendió una luz sobre
|
|
|
|
|
> catorce defectos que ya estaban ahí, y viven en
|
|
|
|
|
> [`AUDIT-docs-code-ledger.md`](./AUDIT-docs-code-ledger.md). Ese ledger es un
|
|
|
|
|
> **artefacto de traspaso, no una lista de trabajo**: se escribió para que una
|
|
|
|
|
> sesión futura los tome con su propio plan y su propia puerta. Ejecutarlo a
|
|
|
|
|
> goteo desde aquí fue el error de método del 2026-08-12 — la jornada se cerró a
|
|
|
|
|
> las 03:52 y siguió trabajando hasta las 20:43 sobre una iniciativa que nunca se
|
|
|
|
|
> nombró, hasta acabar borrando la librería de sonido (revertido, `0d10b983e`).
|
|
|
|
|
>
|
|
|
|
|
> **Handoff**: [`CONTINUE-event-name-normalization.md`](./CONTINUE-event-name-normalization.md).
|
|
|
|
|
>
|
|
|
|
|
> **Kickoff (histórico)**: _«Lee `docs/process/PLAN-event-name-normalization.md` y ejecútalo
|
feat(morfo,soma): la emisión deja de aceptar un elemento anónimo
El defecto que tres auditorías encontraron y ninguna cerró no era que N eventos
redirigieran sin declararlo: era que la API de emisión aceptaba un HTMLElement
suelto, compensando que el registro de partes perdía la identidad de instancia.
Censar y arreglar a mano reproduce el modo de fallo. Esto lo hace inexpresable.
F1 — SomaRuntime<M> genérico, SIN default. Nombres de evento, kebabs de parte y
claves de events/actions tipados contra la declaración (EventNameOf /
PartKebabOf / ActionNameOf, los extractores que semaSelector ya usaba del otro
lado). 104 anotaciones migradas + 3 wrappers que devolvían tipado pero tomaban
sources ANCHO. SomaRuntime<Morfo> queda sólo en la tabla de contratos.
F2 — registro por instancia: Map<part, PartRegistration[]>, pertenencia
mantenida por el stream del attachment (poda al desmontar, re-entrada al
remontar), resolución liveReg = la instancia VIVA más nueva. Mata de paso la
clase "la más nueva muerta ensombrece a una viva".
F3 — tres formas legales de destino: el target declarado; el anclaje por
instancia (SomaRuntimePart.trigger, tipado por EventNameTargeting, +
runtime.partInstance por identidad de referencia); y targetFallback, la cadena
por estado de montaje declarada en el morfo y resuelta por resolveEmitTarget —
LA resolución única para validación, emit y foco a11y, que ya no pueden
discrepar. ~30 componentes migrados; censo de targetOverride 148 -> 89.
F4 — A-36 tenía un superviviente. Medido en dropdown-menu: bajo `pre` el `open`
no colisionaba con el contact-activate del Button compuesto, se PERDÍA entero
porque su content no había montado. Aplicada la receta de float-panel a
dropdown-menu Y context-menu. Ahora: contact-activate en trigger, open en
content — dos superficies, ambas expresando.
Submenús mudos, tres componentes. No era cableado: el morfo no declaraba el
evento. onion-menu recibe emerge-expand/emerge-collapse (la forma de TreeView:
un nivel revela sus hijos en el sitio); dropdown-menu y context-menu reciben
sub-open/sub-close sobre sub-content, distintos del open raíz a propósito
porque el sub es su propia superficie flotante. Nacen con la receta A-36 para
no repetirla en código nuevo.
chronos — declaraba 26 partes y registraba UNA; 17 role= a mano y el ARIA
declarado no llegaba nunca al DOM. Se anula la mitad sobre-extendida de la
decisión de 29ffe902a: la reutilización sigue viajando por el motor puro, pero
la ESTRUCTURA se compone como en Table y Calendar. C1 (las 6 partes que emiten,
chronos con cero targetOverride) + C2 (8 singletons) + C3 8/9.
Registrar una parte DESTAPA declaraciones falsas, tres veces en un pase: el chip
declaraba defaultElement button y ponía type= sobre un div (anida el asa de
resize); grid-row declaraba archetype item y pintó la semana entera como fila de
lista; y un aria por propRef desaparece en SILENCIO si el provider no publica la
fuente. En los tres el arreglo fue la DECLARACIÓN, no el DOM.
Verificado: check 74 = línea base medida con stash, DELTA CERO sostenido en cada
lote · 458 pasan (chronos+morfo+sema) · navegador limpio en las tres vistas de
chronos, con el contrato ARIA vivo por primera vez (aria-labelledby resolviendo
a «junio de 2026», aria-readonly/disabled que no se habían emitido nunca).
Handoff en docs/process/CONTINUE-perceptual-surface.md. Aparte, y decidido por
el autor: docs/process/PLAN-event-name-normalization.md — prefijo de familia
siempre; 36 de 252 sin prefijo, cero ambiguos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
> por fases, empezando por F0.»_
|
|
|
|
|
> **Fecha**: 2026-08-10 · Decidido por el autor en la sesión del eje de
|
|
|
|
|
> superficie perceptual. **Eje independiente**: no bloquea ni lo bloquea
|
|
|
|
|
> `CONTINUE-perceptual-surface.md`, pero comparte ficheros — no ejecutar los
|
|
|
|
|
> dos a la vez sobre los mismos componentes.
|
|
|
|
|
> **Rama**: `alpha-0.1-dir-prefs` es COMPARTIDA (otra sesión trabaja en
|
|
|
|
|
> `blocks`): `git reset -q` + `git add` sólo lo propio, siempre.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 1 · Por qué
|
|
|
|
|
|
|
|
|
|
El framework llama a la misma cosa de dos maneras. Medido:
|
|
|
|
|
|
|
|
|
|
| Convención | Componentes |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| `open` / `close` pelados | context-menu, dialog, drawer, dropdown-menu, float-panel, popover, menu-dial, onion-menu |
|
|
|
|
|
| `emerge-open` / `emerge-close` | combobox, navigation-menu, select |
|
|
|
|
|
|
|
|
|
|
Son el mismo evento con dos dialectos. En un framework de este tamaño eso no es
|
|
|
|
|
una inconsistencia estética: obliga a recordar qué componente habla qué dialecto
|
|
|
|
|
antes de escribir una regla de pack o una receta, y hace que un `grep` por
|
|
|
|
|
`emerge-open` mienta por omisión.
|
|
|
|
|
|
|
|
|
|
**La decisión (autor, 2026-08-10): prefijo de familia SIEMPRE.** El nombre pasa
|
|
|
|
|
a declarar la familia que el motor va a resolver, y deja de ser un dialecto por
|
|
|
|
|
componente.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 2 · El censo — el trabajo es más pequeño de lo que parece
|
|
|
|
|
|
|
|
|
|
`scratchpad/event-name-census.mjs` (recréalo si hace falta; lee los morfos
|
|
|
|
|
textualmente, sin importarlos):
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
eventos declarados : 252 (91 morfos)
|
|
|
|
|
ya con prefijo : 216 ← 86 % ya cumple
|
|
|
|
|
SIN prefijo : 36
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Cero nombres ambiguos**: ningún nombre significa dos familias distintas en dos
|
|
|
|
|
morfos. Eso hace que el mapeo sea **1:1 y determinista**, que es lo que permite
|
|
|
|
|
automatizarlo con seguridad. Compruébalo otra vez antes de empezar: si aparece
|
|
|
|
|
un ambiguo, el codemod deja de ser seguro y hay que resolverlo a mano primero.
|
|
|
|
|
|
|
|
|
|
### Los 36, con su renombrado
|
|
|
|
|
|
|
|
|
|
| Familia | Actual → nuevo | Morfos |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| emerge | `open` → `emerge-open` | context-menu, dialog, drawer, dropdown-menu, float-panel, popover |
|
|
|
|
|
| emerge | `close` → `emerge-close` | los mismos 6 |
|
|
|
|
|
| emerge | `expand` → `emerge-expand` | accordion, collapsible |
|
|
|
|
|
| emerge | `collapse` → `emerge-collapse` | accordion, collapsible |
|
|
|
|
|
| emerge | `present` → `emerge-present` | toast, tooltip |
|
|
|
|
|
| emerge | `dismiss` → `emerge-dismiss` | toast, tooltip |
|
|
|
|
|
| emerge | `dismiss-escape` → `emerge-dismiss-escape` | tooltip |
|
|
|
|
|
| emerge | `sub-open` → `emerge-open-sub` | context-menu, dropdown-menu |
|
|
|
|
|
| emerge | `sub-close` → `emerge-close-sub` | context-menu, dropdown-menu |
|
|
|
|
|
| handle | `drag-start` → `handle-drag-start` | drawer, float-panel |
|
|
|
|
|
| handle | `drag-end` → `handle-drag-end` | drawer, float-panel |
|
|
|
|
|
| handle | `drag-progress` → `handle-drag-progress` | drawer |
|
|
|
|
|
| handle | `resize` → `handle-resize` | drawer |
|
|
|
|
|
| handle | `resize-start` → `handle-resize-start` | float-panel |
|
|
|
|
|
| handle | `resize-end` → `handle-resize-end` | float-panel |
|
|
|
|
|
| contact | `trigger-picker` → `contact-trigger-picker` | file-upload |
|
|
|
|
|
| commit | `select` → `commit-select` | tabs |
|
|
|
|
|
| signal | `announce` → `signal-announce` | toast |
|
|
|
|
|
|
|
|
|
|
**El matiz va al final** (`emerge-open-sub`), no al principio. Precedente vivo:
|
|
|
|
|
`commit-toggle-check` / `commit-toggle-uncheck` (checkbox),
|
|
|
|
|
`commit-select-range` / `commit-select-start` (range-calendar),
|
|
|
|
|
`commit-set-add` (tags-input). Así el prefijo sigue siendo familia+verbo y el
|
|
|
|
|
`[data-event^='emerge-open']` de una receta sigue capturando la variante.
|
|
|
|
|
|
|
|
|
|
⚠️ **`select` → `commit-select` en tabs**: comprobar antes que tabs no tiene ya
|
|
|
|
|
un `commit-select` (hoy no lo tiene). Es el único renombrado que podría colisionar.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 3 · Los consumidores, y cuáles los caza el compilador
|
|
|
|
|
|
|
|
|
|
Esto es lo que hace el eje viable HOY y no hace seis meses: **F1 tipó los
|
|
|
|
|
nombres de evento** (`SomaRuntime<M>` sin default, `EventNameOf<M>`).
|
|
|
|
|
|
|
|
|
|
| Superficie | ¿La caza el compilador? | Cómo |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| `morfo/components/*.ts` — la declaración | — | es el origen del cambio |
|
|
|
|
|
| `soma/**` — `runtime.trigger('name')` | **SÍ** | `trigger` está tipado con `EventNameOf<M>` |
|
|
|
|
|
| `soma/**` — `events: { name: handler }` | **SÍ** | mapped type sobre `EventNameOf<M>` |
|
|
|
|
|
| `sema/components/*.ts` — packs | **SÍ** | `semaSelector(morfo, part, { eventName })` está tipado |
|
|
|
|
|
| `SomaRuntimePart.trigger` anclado | **SÍ** | `EventNameTargeting<M, K>` |
|
|
|
|
|
| **`eidos/lib/motion/presets/css.ts`** | **NO** | strings sueltos — ver abajo |
|
|
|
|
|
| **CSS a mano** (`[data-event=...]`) | **NO** | `scripts/eidos-lint.ts` es la red |
|
|
|
|
|
| Tests, demos, READMEs | NO | grep |
|
|
|
|
|
|
|
|
|
|
**El único punto ciego real** son los presets de movimiento:
|
|
|
|
|
`src/uix/eidos/lib/motion/presets/css.ts` declara `event: 'present'`,
|
|
|
|
|
`event: ['dismiss', 'close']`, `event: 'announce'`, `event: 'expand'`,
|
|
|
|
|
`event: 'collapse'`… y `signatureSelectorList()` (en `lib/render-css.ts`) los
|
|
|
|
|
convierte en `[data-event^='...']`. Son **~10 declaraciones** que generan
|
|
|
|
|
`eidos/generated/base.css`.
|
|
|
|
|
|
|
|
|
|
Detalle que juega a favor: el operador es `^=` (prefijo). Tras el renombrado,
|
|
|
|
|
`[data-event^='emerge-open']` sigue capturando `emerge-open-sub`. Pero un
|
|
|
|
|
`[data-event^='open']` sin actualizar deja de casar con NADA — falla en
|
|
|
|
|
silencio, sin error. **Por eso F4 (medir en navegador) no es opcional en este
|
|
|
|
|
eje**: un preset olvidado no rompe ninguna prueba, sólo deja de animar.
|
|
|
|
|
|
|
|
|
|
Fuera de ahí: 1 sola regla a mano en `eidos/components/collapsible/collapsible.css`
|
|
|
|
|
(`[data-event^='expand']`).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 4 · Fases
|
|
|
|
|
|
|
|
|
|
### F0 · Reconfirmar el censo y congelar el mapa
|
|
|
|
|
Re-ejecutar el censo (los números de arriba son de 2026-08-10). Escribir el mapa
|
|
|
|
|
36→36 en un fichero de datos que el codemod y la verificación compartan, para
|
|
|
|
|
que no haya dos listas que puedan divergir. Verificar el cero-ambiguos.
|
|
|
|
|
|
|
|
|
|
### F1 · Renombrar en los morfos
|
|
|
|
|
Sólo `morfo/components/*.ts`. Al terminar, `npm run check` debe **explotar** con
|
|
|
|
|
los call sites rotos: **esos errores SON el worklist**, no un problema. Anotar
|
|
|
|
|
cuántos salen — es la medida de cobertura del compilador.
|
|
|
|
|
|
|
|
|
|
### F2 · Seguir al compilador hasta cero
|
|
|
|
|
Soma, sema, `SomaRuntimePart.trigger`. No usar grep aquí: el compilador es
|
|
|
|
|
exhaustivo y el grep no. Terminar cuando `check` vuelva a su línea base.
|
|
|
|
|
|
|
|
|
|
⚠️ **Medir la línea base con `git stash`, no de memoria.** En esta sesión el «74»
|
|
|
|
|
resultó ser 74 posiciones únicas y 87 líneas del output `machine`: comparar por
|
|
|
|
|
MÉTODO, nunca por cifra.
|
|
|
|
|
|
|
|
|
|
### F3 · El punto ciego: presets de movimiento + CSS a mano
|
|
|
|
|
`eidos/lib/motion/presets/css.ts` (~10) + la regla de collapsible. Regenerar
|
|
|
|
|
`base.css` (`npm run generate:eidos-css`). Pasar `scripts/eidos-lint-all.ts`.
|
|
|
|
|
|
|
|
|
|
### F4 · Navegador — NO OPCIONAL
|
|
|
|
|
Un preset olvidado no rompe ninguna prueba. Medir con `MutationObserver` ligado
|
|
|
|
|
al NODO (no a un selector) + `getAnimations()`, al menos:
|
|
|
|
|
- **dialog / drawer / popover** — `emerge-open` / `emerge-close` animan.
|
|
|
|
|
- **accordion / collapsible** — `emerge-expand` / `emerge-collapse` animan.
|
|
|
|
|
- **toast** — `signal-announce` (5 presets lo usan: es el más expuesto).
|
|
|
|
|
- **tabs** — `commit-select`.
|
|
|
|
|
|
|
|
|
|
Trampas ya medidas en esta rama, no relearnearlas:
|
|
|
|
|
- Al exportar algo nuevo de un barrel, el grafo SSR de Vite va por detrás y la
|
|
|
|
|
página da 500 con `X is not defined` mientras el cliente ya pinta bien. Se
|
|
|
|
|
arregla navegando otra vez. **No reiniciar el dev server.**
|
|
|
|
|
- Probar el gesto correcto: en esta sesión probé `Escape` esperando un collapse
|
|
|
|
|
cuando la tecla era `Backspace`, y un `pointerleave` sintético que la
|
|
|
|
|
coordinación SafePolygon no dispara. El código estaba bien; la prueba, mal.
|
|
|
|
|
|
|
|
|
|
### F5 · Docs, demos y guards
|
|
|
|
|
READMEs de los ~15 componentes tocados, `docs/CANON.md` si nombra alguno,
|
|
|
|
|
`docs/architecture/sema.md`. Las 68 demos que emiten. `npm run docs:check`.
|
|
|
|
|
|
|
|
|
|
### F6 · Cerrar la puerta
|
|
|
|
|
Que esto no pueda volver a pasar: un guard que falle si un
|
|
|
|
|
`morfo.events[].name` no empieza por su `semantic.family`. Es una prueba de
|
|
|
|
|
~10 líneas sobre el censo que ya existe, y convierte la convención en contrato.
|
|
|
|
|
**Sin esta fase el eje se re-abre solo** — es la lección de los tres audits que
|
|
|
|
|
encontraron el mismo defecto sin cerrarlo.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 5 · Riesgos
|
|
|
|
|
|
|
|
|
|
1. **Los presets de movimiento fallan en silencio** (§3). Mitigación: F4.
|
|
|
|
|
2. **La rama es compartida.** `git reset -q`, `git add` explícito, verificar con
|
|
|
|
|
`git diff --cached --name-only`.
|
|
|
|
|
3. **`sub-open`/`sub-close` son de hoy mismo** (2026-08-10, sesión del eje
|
|
|
|
|
perceptual): renombrarlos a `emerge-open-sub` es coherente y barato, pero
|
|
|
|
|
revisar que su prueba en navegador siga en pie.
|
|
|
|
|
4. **No mezclar con el eje perceptual**: F3 de aquel plan aún migra emisiones en
|
|
|
|
|
los mismos providers. Ejecutar uno, verificar, y luego el otro.
|
|
|
|
|
|
|
|
|
|
## 6 · Aceptación
|
|
|
|
|
|
|
|
|
|
- Censo: **252 declarados / 252 con prefijo de familia / 0 sin prefijo**.
|
|
|
|
|
- `check` en la línea base medida con stash · suites server + navegador iguales.
|
|
|
|
|
- `eidos-lint-all` sin selectores inválidos nuevos · `docs:check` 0.
|
|
|
|
|
- Las cuatro escenas de F4 medidas y animando.
|
|
|
|
|
- El guard de F6 en verde, y **visto fallar antes** con un nombre pelado
|
|
|
|
|
introducido a propósito.
|