fix(sema,morfo,soma): una superficie no admite dos ocurrencias, y el nombre ya no miente
`data-event-*` es UNA RANURA por elemento. Tres auditorias independientes
encontraron el mismo defecto y ninguna lo cerro: fable S1 (2026-07-01, con el
README diciendo «findings executed»), sema S-17 (2026-08-05, midio el estampado
del knob muriendo a los 24ms de un hold de 240) y blocks A-36/A-65 (reproducido
en navegador, dos estampados a 1,1ms). Lo que faltaba no era diagnostico: nadie
podia distinguir una redireccion legitima de una deriva, porque la opcion que
redirigia se llamaba `fallbackTarget` y hacia lo contrario de lo que decia.
EL NOMBRE PRIMERO. `fallbackTarget` -> `targetOverride` (202 apariciones, 64
ficheros, dirigido con `git ls-files`). Siempre GANO sobre el ref registrado; el
nombre honesto es la precondicion para auditarlo. Las cronicas conservan el viejo
por diseño.
LA PROPIEDAD. `unstampEventAttrs` recibia el signal y lo DESCARTABA, asi que la
ocurrencia que terminaba antes borraba a la que tenia la ranura. Ahora comprueba
`data-event-id`. Con eso, `replace` deja de ser el comportamiento roto y pasa a
ser el suelo correcto.
A-36, SIN `regime`. Los cuatro overlays cedian el estampado del `open` al
trigger por un workaround de la era `sequence: 'pre'`; con `post` el content ya
esta montado. Retirado el override (float-panel migrado `pre`->`post`, ultimo
fuera de la doctrina que sema.md escribe). Medido en /blocks/site-header/preview
a 375px: contact-activate en el TRIGGER +18,7ms y open en el CONTENT +20,6ms,
con press-squeeze y slide-from-right-full corriendo. Antes: los dos en el
trigger, press-squeeze jamas.
`regime` DEFINIDO — y son dos valores, no cuatro. `replace` (default) y `queue`,
este solo para las 4 parejas irreducibles: los tres toggles (el provider ES el
boton) y el knob. `collapse` retirado (su caso murio con la propiedad: medido, 0
huecos en 5 emisiones a 72ms) y `lock` retirado (significaria «un cierre que no
se anuncia»), con sus 4 declaraciones muertas.
El guard de `queue` PASO EN VERDE SIENDO INCORRECTO: con timers falsos no hay
animaciones, asi que awaitExpression volvia al instante. El navegador midio el
commit encolado llegando 1,6s tarde. `queue` espera el HOLD, no la expresion.
Medir la envolvente no es medir la salida.
`renderAttrs` — los attrs que no son partes. Vaciar ACTIVE_DEV_TRACK (palabras y
chronos ya no estan excluidos) destapo 23 `data-palabras-*` que ningun morfo
declaraba y que palabras.css SI estiliza: aterrizan en el arbol que el usuario
escribio, asi que no pueden ser partes. Campo nuevo con la linea afilada —si el
consumidor puede componerlo, es una PARTE— y 3 consumidores (palabras, waveform,
aura). Declarado en types.ts Y en el esquema sium: la leccion de MorfoElement.
Ademas: 5 selectores muertos en el predicado del focus-scope de palabras
(barrido de todo el repo: aparecian solo ahi) · RTL-1 real en palabras-chrome
(inset logico + translate fisico = doble volteo; rtl:check 1->0) · el union
inline al `Direction` canonico, que destapo que el censo de direccion grepea el
NOMBRE del tipo · deuda de chronos (scope, barrel, README).
Y de la cola: S-36 (barrel 68->71 + guard), S-31 (vibrate(NaN)), S5 (la
preferencia de reduced-motion del usuario ya gana a los channels del morfo), SO2
(el warn que el JSDoc prometia), SO4 (validar el target antes del prewrite).
Todos los guards nuevos vistos fallar antes de arreglar nada.
Base: check 74 (= base) · sema+morfo+contracts 454 verdes / 6 ajenos
preexistentes · soma navegador 1258/1259 (el rojo es un timeout ajeno,
reproducido con los cambios en stash) · docs:check 0/618 · rtl:check 0/177.
Handoff: docs/process/CONTINUE-perceptual-surface.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
# CONTINUE — la superficie perceptual es UNA RANURA
> **Kickoff**: _«Lee `docs/process/CONTINUE-perceptual-surface.md` y sigue por §3,
> empezando por la campaña de emisión.»_
> **Fecha**: 2026-08-10 · Rama `alpha-0.1-dir-prefs` (COMPARTIDA — otra sesión
> trabaja en `blocks`; `git reset -q` + add sólo lo propio, siempre).
> **Doctrina viva**: [`architecture/sema.md` §The surface is ONE SLOT](../architecture/sema.md)
> y [`architecture/morfo.md` §`renderAttrs`](../architecture/morfo.md).
> Este documento sólo dice **qué se cerró, qué queda y cómo medirlo**.
---
## 1 · El hallazgo, y por qué costó tanto verlo
`data-event-*` es **una ranura por elemento** : dos ocurrencias sobre un nodo no
pueden expresarse las dos. El defecto lo encontraron **tres auditorías
independientes y ninguna lo cerró**:
| Auditoría | Fecha | Qué dijo |
| --- | --- | --- |
| fable **S1** | 2026-07-01 | «Race de señales solapadas sobre el mismo target», con el arreglo mínimo escrito. El README de `old-deprecated` afirmaba «findings executed». |
| sema **S-17** | 2026-08-05 | Re-descubierto solo. Midió el knob: el estampado de `commit-set` moría a los 24 ms de un hold de 240. |
| blocks **A-36 / A-65** | 2026-08-01/09 | Reproducido en NAVEGADOR: dos estampados a 1,1 ms, `press-squeeze` jamás. |
Tres veces el mismo defecto, tres veces sin arreglar. Lo que faltaba no era
diagnóstico: era que **nadie podía distinguir una redirección legítima de una
deriva**, porque la opción que redirigía se llamaba `fallbackTarget` y decía lo
contrario de lo que hacía.
---
## 2 · Lo cerrado (todos los guards vistos fallar antes de arreglar)
| # | Qué | Dónde |
| --- | --- | --- |
| 2.1 | ** `fallbackTarget` → `targetOverride` ** (202 apariciones / 64 ficheros). No cosmético: es la PRECONDICIÓN para auditar la redirección | `soma/runtime.svelte.ts` + todo el árbol |
| 2.2 | **El desestampado comprueba propiedad** (`data-event-id`). Rojo: `expected null to be 'sig-B'` | `sema/stamp.ts` · `sema/projection/dom.ts` |
| 2.3 | ** `assertTargetOverride` ** — el runtime lee el marcador de parte del elemento y avisa si no es `target` ni `allowedTargets` . Runtime porque el override es una expresión | `soma/runtime.svelte.ts` |
| 2.4 | **A-36 CERRADO, y sin `regime`** | los 4 overlays |
| 2.5 | ** `regime` definido y cableado**, reducido a `replace \| queue` | `sema/types.ts` · `engine.ts` · `signal.ts` |
| 2.6 | ** `renderAttrs` ** — los attrs que no son partes | `morfo/types.ts` + `schema.ts` |
| 2.7 | `ACTIVE_DEV_TRACK` vaciado + deuda de chronos (scope · barrel · README) | `contracts.test.ts` |
| 2.8 | **S-36** barrel 68→71 + guard · **S-31** `vibrate(NaN)` · **S5** · **SO2** · **SO4** | varios |
### 2.4 A-36 — la lección: no era arbitrar, era dejar de redirigir
Los cuatro overlays cedían el estampado del `open` al trigger. La razón escrita
en el código («el content no ha montado») era cierta **cuando `open` era
`pre` **. Dialog / Drawer / Popover migraron a `post` hace tiempo y nadie retiró
el override; `float-panel` seguía en `pre` (último fuera de la doctrina que
`sema.md` escribe). Con `<Drawer.Trigger>` compuesto sobre `<Button>` —que es UN
solo elemento— el `open` caía encima del `contact-activate` .
Medido en `/blocks/site-header/preview` a 375 px:
```
ANTES contact-activate TRIGGER +329ms · emerge/open TRIGGER +331ms · press-squeeze: NUNCA
AHORA contact-activate TRIGGER +18,7ms · open CONTENT +20,6ms
getAnimations() → press-squeeze running (120ms) + slide-from-right-full
```
### 2.5 `regime` — dos valores, no cuatro
- **`replace`** (default) no había que construirlo: **2.2 lo terminó** . Antes era
el comportamiento *roto* .
- **`queue`** es el único nuevo. Aplicado a las 4 parejas irreducibles: los tres
toggles (el provider ES el botón) y el knob (`handle-drop` + `commit-set` ).
- **`collapse` RETIRADO** — su caso (el parpadeo continuo) ya lo mató 2.2:
medido, **0 huecos** en 5 emisiones a 72 ms con hold de 240. Y reutilizar el
`data-event-id` habría minado la propiedad que lo curó.
- **`lock` RETIRADO** — bajo la semántica limpia significaría «un cierre que no
se anuncia si hay algo proyectando». Con él, sus 4 declaraciones muertas.
> ⚠️ **EL GUARD DE `queue` PASÓ EN VERDE SIENDO INCORRECTO.** Con timers falsos
> no hay animaciones, así que `awaitExpression` volvía al instante. El navegador
> midió que el commit encolado llegaba **1,6 s tarde**, porque esperaba la
> expresión entera (tope 1500 ms) en vez del hold. `queue` espera el **hold**;
> el desplazado está a salvo porque su limpieza ya es consciente del dueño. El
> test corregido inyecta un `getAnimations()` que nunca termina.
> **Medir la envolvente no es medir la salida** — otra vez.
### 2.6 `renderAttrs` + los fixes de palabras
Al vaciar `ACTIVE_DEV_TRACK` salieron 23 `data-palabras-*` que ningún morfo
declaraba y que `palabras.css` **sí estiliza** . No son partes ni pueden serlo:
aterrizan en el árbol que el usuario escribió. Campo nuevo, con la línea afilada
—*si el consumidor puede componerlo o direccionarlo, es una PARTE*— y ≥3
consumidores (palabras 23 · waveform 2 · aura 2).
Y tres más: **5 selectores muertos** en el predicado del focus-scope (barrido de
todo el repo: aparecían sólo ahí) · **RTL-1 real** en `palabras-chrome.css`
(`inset-inline-start` lógico + `translate` físico = el doble volteo; centrado con
margen derivado de un token, `rtl:check` 1→0) · el `dir?: 'ltr'|'rtl'` inline al
`Direction` canónico — lo que destapó que **el censo de dirección grepea el
NOMBRE del tipo**, así que un union inline lo escondía (exención firmada, misma
razón que `field-langs` ).
---
## 3 · LA COLA — por dónde seguir, en orden
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
### 3.0 ⚠️ GIRO DE ENFOQUE (2026-08-10, aprobado por el autor)
La campaña de §3.1 quedó SUPERADA como mecanismo: censo + campaña + aviso de
runtime es la misma defensa que ya falló tres veces (depende de que alguien
mire), y el propio censo de hoy demostró el modo de fallo al declarar
supervivientes de más y de menos. El plan vigente hace inexpresable el estado
ilegal — el defecto de clase no es que N eventos redirijan sin declarar, es que
la API de emisión acepta un `HTMLElement` anónimo, compensando que el registro
de partes pierde la identidad de instancia (indexa por nombre, gana el último
montado):
- **F1 ✅ (2026-08-10)** — `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
de `semaSelector` ). 104 anotaciones migradas + 3 wrappers estrechados
(`createMenuDialRuntime` / `createOnionMenuRuntime` / `createMetricsRuntime` —
devolvían tipado pero tomaban sources ANCHO: agujero). `SomaRuntime<Morfo>`
sólo en la tabla de contratos. Doc: `soma-architecture.md` §"typed by ITS
morfo". Delta de check contra base medida con stash: **CERO** (74/74 únicos,
87/87 brutos); server 454✓/6 ajenos; navegador 1258✓/1 ajeno.
Los tipos ya desmontaron DOS filas del censo: los `'clear-document'` /`'unlink'`
de palabras eran nombres de COMANDO en un ternario que mapea a eventos reales
— falso positivo del censo, resuelto por inferencia.
- **F2 ✅ (2026-08-10)** — registro por instancia: `Map<part, PartRegistration[]>` ,
la pertenencia la mantiene el stream del attachment (poda al desmontar, se
re-añade al final al re-montar — sin crecimiento en listas virtualizadas),
resolución `liveReg` = la instancia VIVA más nueva con caída a la más nueva
a secas (pre-attach conserva la conducta anterior). Arregla de paso la clase
«la más nueva muerta ensombrece a una viva». Delta de check CERO; suites
idénticas (454✓/6 · 1258✓/1).
- **F3 — framework ✅, migración EN CURSO (2026-08-10)**:
- `PartRef<T = string>` genérico (el builder `v.partRef` preserva el literal;
default `string` = cero ondas, doctrina «ensancha el tipo primero»).
- `targetFallback` declarado en el morfo (tipo + schema con 3 invariantes
testeados: parte desconocida / re-declarar el target / duplicado) y
pre-aplanado en `ActionPlan` ; `resolveEmitTarget` es LA resolución única
para validación, emit y foco a11y (no pueden discrepar).
- `SomaRuntimePart<M, K>` sin default + `trigger` anclado tipado por
`EventNameTargeting<M, K>` (target ∪ allowedTargets; el fallback EXCLUIDO a
propósito — anclar al degradado forzaría el aterrizaje pobre con el
primario montado) + `runtime.partInstance(part, el)` (identidad por
referencia contra el registro; elemento ajeno → null) + guard dev
`assertAnchor` para llamantes JS. Codemod: 101 ficheros + 6 uniones a mano
(clases parametrizadas por parte: paginación de calendarios, sidebar).
- Tests nuevos: cadena de fallback resuelta por el runtime · anclaje estampa
SU instancia (no la más nueva) · `partInstance` identidad. 71✓.
- **Ola 1 ✅ — los 4 overlays**: su `allowedTargets: [trigger]` del close era
el EJE EQUIVOCADO (es estado de montaje, no elección del llamante) →
`targetFallback: [trigger]` en dialog/drawer/popover/float-panel, y los 4
providers dejan de resolver `content ?? partRef ?? trigger` a mano. El
prewrite de `data-last-action` queda SOLO en content (verificado: todos
los selectores de eidos que lo leen son `[data-*-content]` ; escribirlo en
el trigger era incidental). `stampPartsOf` del pack-census cuenta ahora
`targetFallback` como estampable. 218✓ (overlays + census + morfo).
- **Olas 2-3 EN CURSO** (2026-08-10, censo 145 → **107** ). Migrados y en
verde (`check` DELTA CERO · navegador 1258✓+2 nuevos/1 ajeno):
* **Anclado al handle propio** — dropdown-menu item · context-menu item ·
command item (ratón) · table row · timeline item (emerge-reveal).
* **`partInstance` por identidad** — listbox · toggle-group (las TRES
emisiones del gesto anclan a UNA superficie, lo que el régimen `queue`
del par contact+commit asume) · tag-group select/remove · tree-view
(commit-select discrimina item/branch por identidad) · tree-grid ·
command (camino Enter) · radio-group · rating-group (item si el gesto
nombró estrella, provider si teclado — `partInstance` discrimina) ·
grid-list · feed · tabs · carousel.
* **Gesto DENTRO de la superficie** — table sort-trigger→column-header y
detail-trigger→row: `closest(partMarkerAttr(…))` + `partInstance` ; el
marcador sale de la función canónica, nunca string a mano.
`partMarkerAttr` exportado del barrel de morfo para esto.
⚠️ El elemento de partida es `opts.ref.current` de la clase, NO
`e.currentTarget` — los tests fabrican eventos sin currentTarget.
* **No-op retirados** — proof-of-human (3) · tag-group clear · checkbox.
* **CROSS retirados (lección A-36: sobra la redirección)** — editable
(shift-enter-mode/commit-save/commit-cancel estampan el PROVIDER
declarado; el subparte era deriva sin declarar).
* **aura → `targetFallback` ** — sus 4 `allowedTargets:[provider]` de la
mañana eran también eje equivocado; provider sin condicionales.
- **Más migrados en el mismo pase** (censo **107 → 99** , check DELTA CERO,
21✓): search-field y stepper (CROSS retirados — el provider declarado ES
la superficie; params de gesto eliminados de `clear` /`submit`/`setStep`/
`goTo*Step` ) · form (no-op retirados de submit/warn/reset) · clipboard
(`copy(triggerEl?)` ancla por identidad al Trigger pulsado — varios
Triggers por provider quedan bien — y el README lo documenta).
- **QUEDA de las olas 2-4** (99 sitios): tags-input · file-upload · month/year-grid ·
calendar (day + shift-navigate ya declarado) · range-calendar ·
navigation-menu · menubar · virtual-list/grid · pagination (declarado;
anclar desde los 5 controles) · css-field · number-field (exponen
`targetOverride` en su API pública de `commit()` — retirarla) ·
drag-drop · path-trace · rotate-align (gestos handle con campos de
elemento propios) · float-panel drag/resize (header/resize-handle:
¿allowedTargets o retirar?) · metrics/menu-dial/onion-menu (vistas de
eidos: deben REGISTRAR sus partes con `runtime.part()` — hoy emiten con
elementos locales no registrados, `partInstance` daría null) ·
select/combobox (elección del llamante trigger/input: leer providers) ·
dropdown/context open+close (F4, gestureTarget) · **palabras** (13 filas,
diseño fino por evento) · **chronos** (BLOQUEADO por decisión de autor:
botones del editor no son partes; sus chips tampoco REGISTRAN — plain
trigger fallaría igual). Al llegar a 0: BORRAR `targetOverride` +
`assertTargetOverride` + retirar el script scratch.
`allowedTargets` (elección del llamante) y `targetFallback` (estado de
montaje) son dos ejes, precedente `intentRequirement` /`intentGuidance`.
- **F4** — dropdown `open` pre→post (receta float-panel) + decisiones de autor.
- **F5** — navegador, no opcional: site-header 375px · knob · dropdown+Button.
- **F6** — docs (`sema.md` §ONE SLOT · `morfo.md` capítulo de emisión).
El censo scratch queda como herramienta de MEDIDA hasta que F3 lo deje en 0 y
se retire. Lo de abajo (§3.1) se conserva como inventario de hallazgos.
### 3.1 Campaña de emisión — censada, clasificada — SUPERADA POR §3.0
fix(sema,morfo,soma): una superficie no admite dos ocurrencias, y el nombre ya no miente
`data-event-*` es UNA RANURA por elemento. Tres auditorias independientes
encontraron el mismo defecto y ninguna lo cerro: fable S1 (2026-07-01, con el
README diciendo «findings executed»), sema S-17 (2026-08-05, midio el estampado
del knob muriendo a los 24ms de un hold de 240) y blocks A-36/A-65 (reproducido
en navegador, dos estampados a 1,1ms). Lo que faltaba no era diagnostico: nadie
podia distinguir una redireccion legitima de una deriva, porque la opcion que
redirigia se llamaba `fallbackTarget` y hacia lo contrario de lo que decia.
EL NOMBRE PRIMERO. `fallbackTarget` -> `targetOverride` (202 apariciones, 64
ficheros, dirigido con `git ls-files`). Siempre GANO sobre el ref registrado; el
nombre honesto es la precondicion para auditarlo. Las cronicas conservan el viejo
por diseño.
LA PROPIEDAD. `unstampEventAttrs` recibia el signal y lo DESCARTABA, asi que la
ocurrencia que terminaba antes borraba a la que tenia la ranura. Ahora comprueba
`data-event-id`. Con eso, `replace` deja de ser el comportamiento roto y pasa a
ser el suelo correcto.
A-36, SIN `regime`. Los cuatro overlays cedian el estampado del `open` al
trigger por un workaround de la era `sequence: 'pre'`; con `post` el content ya
esta montado. Retirado el override (float-panel migrado `pre`->`post`, ultimo
fuera de la doctrina que sema.md escribe). Medido en /blocks/site-header/preview
a 375px: contact-activate en el TRIGGER +18,7ms y open en el CONTENT +20,6ms,
con press-squeeze y slide-from-right-full corriendo. Antes: los dos en el
trigger, press-squeeze jamas.
`regime` DEFINIDO — y son dos valores, no cuatro. `replace` (default) y `queue`,
este solo para las 4 parejas irreducibles: los tres toggles (el provider ES el
boton) y el knob. `collapse` retirado (su caso murio con la propiedad: medido, 0
huecos en 5 emisiones a 72ms) y `lock` retirado (significaria «un cierre que no
se anuncia»), con sus 4 declaraciones muertas.
El guard de `queue` PASO EN VERDE SIENDO INCORRECTO: con timers falsos no hay
animaciones, asi que awaitExpression volvia al instante. El navegador midio el
commit encolado llegando 1,6s tarde. `queue` espera el HOLD, no la expresion.
Medir la envolvente no es medir la salida.
`renderAttrs` — los attrs que no son partes. Vaciar ACTIVE_DEV_TRACK (palabras y
chronos ya no estan excluidos) destapo 23 `data-palabras-*` que ningun morfo
declaraba y que palabras.css SI estiliza: aterrizan en el arbol que el usuario
escribio, asi que no pueden ser partes. Campo nuevo con la linea afilada —si el
consumidor puede componerlo, es una PARTE— y 3 consumidores (palabras, waveform,
aura). Declarado en types.ts Y en el esquema sium: la leccion de MorfoElement.
Ademas: 5 selectores muertos en el predicado del focus-scope de palabras
(barrido de todo el repo: aparecian solo ahi) · RTL-1 real en palabras-chrome
(inset logico + translate fisico = doble volteo; rtl:check 1->0) · el union
inline al `Direction` canonico, que destapo que el censo de direccion grepea el
NOMBRE del tipo · deuda de chronos (scope, barrel, README).
Y de la cola: S-36 (barrel 68->71 + guard), S-31 (vibrate(NaN)), S5 (la
preferencia de reduced-motion del usuario ya gana a los channels del morfo), SO2
(el warn que el JSDoc prometia), SO4 (validar el target antes del prewrite).
Todos los guards nuevos vistos fallar antes de arreglar nada.
Base: check 74 (= base) · sema+morfo+contracts 454 verdes / 6 ajenos
preexistentes · soma navegador 1258/1259 (el rojo es un timeout ajeno,
reproducido con los cambios en stash) · docs:check 0/618 · rtl:check 0/177.
Handoff: docs/process/CONTINUE-perceptual-surface.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Redirigen el estampado con `targetOverride` y su morfo no lo declara. El guard
del runtime los señala uno a uno al ejecutarse.
**No anotar a ciegas.** Por cada uno, la pregunta primero es *¿sobra la
redirección?* — como en A-36, donde el arreglo fue retirarla. Anotar una
redirección innecesaria la fosiliza. Sólo cuando es legítima (parte repetida: el
`day` pulsado, el `item` clicado) se declara `allowedTargets` .
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
#### El censo, ahora reproducible
`scripts/__scratch-target-override-census.ts` (SCRATCH, sin commitear):
```
node --import tsx/esm scripts/__scratch-target-override-census.ts
```
Cruza cada `trigger(name, { targetOverride })` de `soma/components` +
`eidos/components` + `soma/layers` contra `target` + `allowedTargets` , **y
resuelve en qué parte aterriza cada override** con el AST de TypeScript en dos
fases. Hace falta interprocedimental: el elemento llega casi siempre como
PARÁMETRO (`select(date, target?)`), y quien lo pasa vive en otro fichero.
Marcas del informe: `~` la parte se dedujo del `e.currentTarget` siguiendo dónde
se cablea el handler · `↑` la trajo un llamante · `«markup»` la trae el template
de un `.svelte` , que el censo NO lee.
**El número real no era 71/33 sino 119 eventos / 46 componentes** (13
declarados). La diferencia no es un error del censo viejo: incluye
`eidos/components` y los nombres computados.
Tres trampas medidas al construirlo, por si se re-hace:
- Resolver el nombre del método **globalmente** importa partes de otro
componente: `select` / `prev` / `next` los tienen doce, y Calendar heredó el
`branch-control` de TreeView. El salto no puede salir del componente.
- `opts.ref.current` como RECEPTOR de una consulta (`…current.querySelector(…)`)
es la raíz donde se busca, no el aterrizaje. Contarlo inventaba un `provider`
en carousel, command y todo el que busca su item desde la raíz.
- `e.currentTarget` NO es la parte de la clase que declara el handler:
`CalendarProvider` aloja el keydown del día y su clase registra `provider` .
Hay que seguir el handler hasta dónde se cablea.
Punto ciego conocido: un override pasado en una VARIABLE
(`trigger(name, opts)`) no se detecta — se mira el texto del segundo argumento.
#### Las cuatro clases
| Clase | Qué | Qué se hace |
| --- | --- | --- |
| **A · no-op** | el override re-dice el destino declarado, que es singleton | RETIRAR — mientras esté, el guard no distingue una redirección real |
| **B · parte repetida** | `day` · `item` · `row` pulsado | nada: la parte YA es el `target` |
| **C · partes hermanas** | `checkbox-item` / `radio-item` junto a `item` ; `prev-trigger` / `next-trigger` junto al provider | declarar `allowedTargets` |
| **D · contradicción** | aterriza donde el morfo no dice, o en algo que NO es parte | decidir: retirar, o extender el morfo |
#### Cerrado en este pase
- **aura** — `delegate-offer` y `delegate-escalate` estampaban con el ref del
provider, que es exactamente lo que el runtime resuelve solo: no-op retirado.
`sustain-processing` (declara `indicator` ) y los tres terminales (declaran
`ring` ) caen al provider cuando la parte decorativa no está montada: override
**sólo en la ausencia** + `allowedTargets: [provider]` en los cuatro.
⚠️ Retirar el override de `sustain-processing` del todo PARECÍA correcto —
«es `post` , sólo se pierde el ornamento»— y el navegador lo desmintió: la
prueba monta el Aura sin Indicator y salieron 10 rechazos de
`SomaRuntimeTargetError` . El ornamento no se pierde en silencio.
- **checkbox** — el provider ES el botón (`archetype: 'trigger'`), así que los
dos handlers le pasaban al runtime el mismo elemento que ya resuelve. Retirado
el parámetro `triggerEl` de `toggle()` y actualizados los 6 llamantes.
- **Verificados SIN cambio** (clase B, el override elige la instancia correcta
de una parte repetida): calendar `commit-select` /`unselect` · carousel ·
clipboard · color-field (dos partes registran `input` ; pasa la visible a
propósito) · command · listbox · radio-group · range-calendar `commit-*` ·
toggle-group · tree-grid · tree-view · month-grid `commit-set` ·
table `commit-select` · tabs · feed · proof-of-human · form `commit-submit` .
#### ⚠️ Clase NUEVA — submenús mudos (onion-menu · dropdown-menu · context-menu)
No es deriva de destino: **el morfo no declaraba el evento** , así que el
provider no tenía qué emitir. Tres componentes, el mismo agujero, y en los tres
la anatomía YA lo anticipaba (`item` de onion-menu declaraba
`states: ['expanded','collapsed']` ; `sub-content` de los menús declaraba
`states: ['open','closed']` y su `data-state` ).
- **onion-menu** — desplegar un submenú, el gesto que DEFINE un menú radial,
emitía nada: `clickItem()` sólo emitía en la rama hoja. Añadidos
`emerge-expand` / `emerge-collapse` sobre `item` , con la forma de TreeView
(misma familia+verbo: un nivel revela sus hijos en el sitio, libro cap. 26).
Tocar fondo del drill sigue siendo `close` , no un collapse.
Medido: `emerge-expand` estampa el sector (depth 1) y desestampa limpio;
`Backspace` → `emerge-collapse` , depths 2→1.
⚠️ Probé primero con `Escape` esperando el collapse y salió `close` : la
tecla de `drill-up` es ** `Backspace` ** según el mapa del propio morfo. El
código estaba bien; la prueba estaba mal.
- **dropdown-menu y context-menu** — `MenuSubProvider.handleOpen/Close` sólo
volteaban estado. Añadidos `sub-open` / `sub-close` sobre `sub-content` en
AMBOS gemelos (arreglar uno solo era la incoherencia que este eje persigue).
Eventos distintos del `open` raíz a propósito: el sub es su propia superficie
flotante, su `commits` escribe su propio `data-state` , y un pack debe poder
distinguir «se abrió el menú» de «se abrió el submenú».
`sub-open` nace ** `post` ** y `sub-close` `pre` + `targetFallback:
[sub-trigger]` — la receta de float-panel, para no repetir A-36 en código
nuevo. Medido: `sub-open` estampa `data-dropdown-menu-sub-content` .
⚠️ `sub-close` NO se ejerció en navegador: su camino es la coordinación
SafePolygon (`onPointerExit`), que un `pointerleave` sintético no dispara.
Queda por medir.
#### ✅ F4 CERRADO (2026-08-10) — el A/B de A-36, ahora en verde
Aplicada la receta de float-panel a **dropdown-menu Y context-menu** (los dos
gemelos; arreglar uno era la incoherencia de siempre): `open` `pre` →`post`,
`close` sigue `pre` + `targetFallback: [trigger]` declarado, y los providers
dejan de resolver `contentRef ?? gestureTarget ?? partRef('trigger')` a mano.
El `gestureTarget` desaparece de la firma: pasarlo era justo lo que pisaba el
`contact-activate` del Button compuesto.
Medido en `/uix/components/dropdown-menu` , el A/B que define el eje:
```
ANTES contact-activate TRIGGER · contact-activate TRIGGER (+1 ms) · open PERDIDO
AHORA contact-activate TRIGGER +9903ms · open CONTENT +9906ms
close CONTENT (Escape)
```
**Dos superficies, ambas expresando.** El `open` ya no se pierde: bajo `pre` su
destino declarado no había montado y no aterrizaba en ningún sitio.
`check` DELTA CERO · 440✓ (ambos menús + morfo + sema) · chronos sigue sano
tras el pase (42 celdas · 17 chips · 3 botones · SSR ok).
#### ⚠️ F4 — cómo se descubrió (medida previa, 2026-08-10)
Instrumentando el submenú salió la prueba del superviviente de A-36: al abrir
el menú raíz se estampan **dos `contact-activate` a 1 ms** sobre
`data-dropdown-menu-trigger` , y el `open` de la raíz **no aparece en absoluto**
— es `pre` con `target: content` , y a esa altura el content no ha montado. No
es que colisione: es que se pierde. La receta de F4 (open `pre` →`post`) sigue
siendo la correcta y ahora está medida, no inferida.
#### ⚠️ Hallazgo — A-36 tiene UN superviviente: `dropdown-menu`
Dos componentes comparten la FORMA del código de A-36 —`open`/`close` con
`target: content` en `sequence: 'pre'` y el provider resolviendo
`contentRef ?? gestureTarget ?? partRef('trigger')` — pero sólo uno comparte el
DEFECTO, y la diferencia está en la composición, no en el provider:
- **`dropdown-menu` SÍ.** `eidos/…/dropdown-menu-trigger.svelte` renderiza el
Trigger de soma por su snippet `child` con un `<Button>` dentro, y su propia
cabecera lo dice: «el `[data-dropdown-menu-trigger]` aterriza en el MISMO
`<button>` que emite Button». Un elemento, dos morfos — la geometría exacta
del Drawer en A-36.
- **`context-menu`, no POR DEFECTO — pero alcanzable.** Su Trigger renderiza un
`<span>` envoltorio, así que lo que el consumidor mete dentro es un HIJO: dos
elementos, no uno. Pero el Trigger acepta snippet `child` , y con él el
consumidor pone su propio control en el MISMO nodo — la colisión vuelve. Y el
`<span>` es a su vez un defecto aparte, ver abajo.
⚠️ Esta distinción salió de MIRAR la composición. Al primer pase los agrupé por
el parecido del provider y di los dos por supervivientes — el mismo error de
clase que dejó vivo a `dropdown-menu` cuando se cerró A-36: casar patrones en
vez de medir.
#### ⚠️ Hallazgo aparte — el Trigger de `context-menu` viola su propio contrato
Encontrado al comprobar lo anterior. Tres fuentes, dos respuestas:
- el morfo declara `defaultElement: 'div'` · `archetype: 'trigger'` ;
- `components/context-menu-trigger.svelte` renderiza un ** `<span>` **, y el README
de soma y la cabecera del componente de eidos lo documentan como `<span>` ;
- no hay decisión escrita en `docs/` que justifique el span.
Y el elemento no cumple lo que su arquetipo promete: sus props son
`data-state` + `data-disabled` + `oncontextmenu` , **sin `tabindex` , sin `role` y
sin `onkeydown` ** — mientras el morfo le cuelga `aria-haspopup="menu"` y
`aria-expanded` , que son estado de widget y no significan nada sobre un elemento
no interactivo ni enfocable. Tampoco hay camino de teclado (APG: Shift+F10 y la
tecla de menú contextual); el componente es sólo ratón / pulsación larga. Los
demás `archetype: 'trigger'` del árbol (checkbox, dialog, dropdown-menu) sí son
controles reales.
Para `dropdown-menu` , la receta ya está escrita en float-panel:
- morfo `open` : `sequence: 'post'` (el content monta durante el trigger y el
trigger conserva su superficie para sus propios eventos).
- morfo `close` : sigue `pre` + `allowedTargets: [v.partRef('trigger')]` .
- provider: el flip de estado pasa a `events: { open, close }` y el `open` emite
**sin override** ; el `close` resuelve `content ?? partRef('trigger')` — sin
`gestureTarget` , que es lo que hoy arrastra `item` / `checkbox-item` /
`radio-item` / `sub-trigger` al aterrizaje.
No lo he tocado: son dos overlays muy usados, el cambio de `pre` a `post` mueve
el `commits` de sitio y §5 exige medirlo en navegador. Es lo siguiente.
#### ⚠️ chronos — el diagnóstico real y la decisión revocada (2026-08-10)
Auditado con `scratchpad/chronos-parts-audit.mjs` : chronos declara **26 partes y
registra UNA** (`provider`). Los ~25 marcadores restantes, y **17 `role=`** , están
escritos a mano en el template de eidos, así que el ARIA declarado en el morfo
(p. ej. `grid` : `aria-labelledby` →heading, `aria-readonly` , `aria-disabled` )
**no llegaba nunca al DOM**. Por eso chronos dependía de `targetOverride` : sin
registro, `partInstance` devuelve `null` para todo.
**Decisión revocada.** `29ffe902a` (2026-07-13) fijó «reuse of a piece of chronos
rides the pure engine, not a public compound API». La mitad de REUTILIZACIÓN es
correcta y se mantiene (un componente no es librería de otro). La mitad que el
comentario de `exports.ts` extendió a «no public compound parts» como regla de
API de consumidor **se anula** : contradecía la doctrina composicional
(`feedback-compositional-not-data-driven`, con precedente de rechazo en
OnionMenu), dejaba a chronos como el ÚNICO de su clase con estructura cerrada
(Table y Calendar exponen la suya), y produjo el monolito que causó lo de arriba.
Su cautela declarada («hasta que RRULE/resources estabilicen las firmas») ya
había caducado. Los datos siguen siendo datos (como el `TableInstance` de Table);
lo que se abre es la ESTRUCTURA.
**Hecho: `event-chip` de punta a punta** — `ChronosEventChipProvider` + componente
de soma + barrels + recableado del chip de mes. Cadena
`Tooltip.Trigger → EventChip → DragDrop.Draggable → un solo elemento` , con los
tres morfos fundidos en él. Medido en navegador (17 chips): el contrato sale ya
del morfo — `role` , `aria-label` , `data-event-id` y ** `data-color` , que no se
había emitido nunca**. Consola limpia; 228✓; `check` sin errores nuevos.
⚠️ **Y el navegador cazó un defecto que la declaración escondía** : el morfo daba
al chip `defaultElement: 'button'` + `type` literal, y el primer render por el
runtime puso `type="button"` sobre un `<div>` . La causa es de anatomía, no de
cableado: **el chip CONTIENE el asa de redimensionado** , y el modelo de contenido
de `<button>` no admite un control anidado. Corregida la DECLARACIÓN (div +
`role='button'` , `tabindex` propiedad de soma), no el DOM. Era invisible mientras
nadie registraba la parte.
**C1 CERRADO (2026-08-10)** — las 6 partes que emiten registran ya:
`event-chip` · `event-resize-handle` · `day-cell` (× 42) · `prev` /`next`/
`today-button` . Cuatro clases de provider (los tres botones comparten una:
mismo contrato, parte distinta — patrón de paginación de Calendar), cuatro
componentes de soma, barrels, y la vista recableada por `child` .
**chronos ya no usa `targetOverride` en ninguna emisión** (censo global 99 → 91):
- `shift-navigate` — redirección RETIRADA. El morfo declara `provider` sin
`allowedTargets` ; ceder el botón pulsado era deriva (lección A-36).
- `commit-save` / `commit-delete` / `commit-move` — anclados en el CHIP que
nombran vía `partInstance` , con `targetFallback: [provider]` declarado para
cuando no hay chip (un alta, o el borrado que acaba de quitarlo). Esto
responde al bloqueo original: los botones Guardar/Eliminar son `IconButton`
compuestos dentro de un `Dialog` compuesto y no son partes de chronos — el
SPEC ya decía que el morfo del editor lo aporta Dialog.
- `commit-restore` / `commit-apply` — parámetros muertos retirados.
Medido en navegador: las 4 partes con `id` del runtime, `aria-label` traducido
saliendo del morfo («Hoy», «lunes, 1 de junio de 2026»), 0 errores de consola,
0 quinientos. 260✓ (6 fallos = base ajena de `contracts.test.ts` ); check DELTA
CERO.
⚠️ **Trampa de verificación medida dos veces** : al exportar una parte nueva del
barrel, el grafo de módulos SSR de Vite va por detrás y la página revienta con
`X is not defined` (500) mientras el cliente ya renderiza bien. Se resuelve
navegando de nuevo. No confundirlo con un fallo del código — y NO reiniciar el
dev server (ver [[reference-dev-server-restart-orphans-browser]]).
**C2 CERRADO (2026-08-10)** — clase `ChronosRegionProvider` + componente
`Region` para los 8 singletons estructurales (una clase para todos, como
`SidebarRegionProvider` : misma forma, parte distinta, sin ramas). Migrados
**toolbar · heading · grid · grid-head · grid-body · time-grid · all-day-row ·
agenda**, medidos en las TRES vistas (mes 42 celdas · semana 168 slots ·
agenda 18 grupos), porque cada vista monta regiones distintas y una sola no
las cubre.
Y con eso **el contrato ARIA del morfo está vivo por primera vez** . Medido en
`/uix/components/chronos` :
```
grid → role="grid" · aria-readonly="false" · aria-disabled="false"
aria-labelledby="uix-chronos-region-c519" → «junio de 2026»
grid-head → role="row" (el template ya no lo escribe)
grid-body → role="rowgroup" (idem)
```
`aria-readonly` y `aria-disabled` **no se habían emitido nunca** . Y los `role`
de grid-head/grid-body se han BORRADO del template: los declara el morfo, que
es el objetivo de C2.
⚠️ **La trampa que costó el hallazgo, y que se repetirá en C3** : registrar la
parte NO basta para un `partRef` . `aria-labelledby` en `grid` es
`v.partRef('heading')` gateado por `part-present` , y un `partRef` lee el bag
`parts` **del provider** , no el registro. Chronos construía su runtime con
`{}` , así que el grid registraba, su `role` y sus dos aria aparecían, y
`aria-labelledby` se quedaba **nulo en silencio** . Arreglado publicando
`headingId` como fuente (`parts: { heading: … }`), que es lo que Dialog hace
desde siempre. Cualquier parte que otra referencie por `partRef` necesita lo
mismo.
⚠️ **La misma trampa mordió DOS veces, por el otro extremo** : omitir una fuente
que la parte SÍ declara hace desaparecer el atributo en silencio. Al quitar el
`aria-label` a mano del `time-grid` —que el morfo declara como
`propRef('ariaLabel')` — la región se quedó sin nombre accesible hasta añadir
`ariaLabel` al bag de props del Region. Igual con el `aria-label` del grid, que
depende de que `heading` esté registrado. **Regla** : al migrar una parte, cruzar
sus `aria` /`data` declarados con las fuentes que el provider publica, y
comprobar el VALOR en el DOM — no que el atributo exista.
Por eso la medida fue en las tres vistas: el `time-grid` y el `all-day-row` sólo
existen en semana/día, y la agenda en la suya. Verificar en la vista por defecto
habría dado verde con dos regiones sin comprobar.
**C3 EN CURSO** — las repetidas se parten en dos por una línea que conviene ver:
- **Sin fuentes de valor** (sólo `role` , o un attr literal): `grid-row` ,
`head-cell` , `now-indicator` . **Migradas** reusando la MISMA clase `Region` ,
sin componente nuevo: el registro por instancia de F2 es lo que hace segura
una clase para muchos nodos. Medido: 6 filas + 7 cabeceras registradas por
instancia, con `role="row"` / `role="columnheader"` saliendo del morfo.
- **Con `propRef` ** (necesitan valores por instancia): en vez de una clase por
parte, `Region` recibe un bag `sources` — un **thunk, no una instantánea** ,
porque el runtime lo re-lee y así los attrs siguen siendo reactivos cuando el
mes cambia debajo. Migrados así ** `day-number` ** (42/42, `data-value` desde el
`propRef` ), ** `day-column` **, ** `time-slot` ** y ** `agenda-group` **.
- **`agenda-item`**: clase propia (`ChronosAgendaItemProvider` + componente),
porque renderiza un `<button>` REAL — su morfo declara `role` + el literal
`type` , y a diferencia del chip del mes no anida ningún control, así que aquí
el elemento nativo es el honesto.
feat(morfo,soma,eidos): chronos deja de tener partes que el morfo no conoce
C3 cierra con `more-link` y C4 con los seis marcadores que eidos estilizaba
sin declaracion. Los tres botones sin fuentes (`day-add`, `edit-action`,
`search-button`) comparten una clase; `undo`/`redo` se cablean sobre la del
nav; `timegrid` pasa a `time-view` porque estaba a un guion de la parte
`time-grid` que vive dentro; `peek-panel` y `day-peek` quedan como partes de
display puro con marcador inline. Cada una renderiza un `<button>` real
cuando el morfo lo declara: una `Region` que cae a `<div>` pondria el `type`
declarado sobre un no-boton.
La lupa deja de ser un adorno sin `onclick`: abre una paleta `Command.Dialog`
sobre TODOS los eventos, no un filtro de la vista, con `goToDate` nuevo en el
provider — la generalizacion de la que `goToday` era un caso particular. El
texto buscable viaja en `keywords` porque el scorer de Command lee value +
keywords y NUNCA los hijos renderizados, asi que se busca por titulo, por tipo
y por fecha.
Y tres etiquetas que mentian en silencio: `translationRef` / `commonRef`
normalizan la clave LITERAL y no consultan ningun mapa, asi que
`prevMonth` / `nextMonth` / `moreEvents` y `action.undo` / `action.redo` caian
al fallback ingles en una pagina `es`. Corregidas contra el catalogo, con
`common.buttons.undo` / `redo` anadidas.
Verificado: check 74 = base (delta cero) · 458 vitest · docs:check 0/621 ·
rtl:check 0/177 · eidos-lint chronos morfo-backed 32 -> 44, invalid 0 ·
navegador limpio en las tres vistas, con el A/B del undo medido
(18 jun -> drag -> 25 jun -> undo -> 18 jun -> redo -> 25 jun) y el salto de
la paleta (noviembre -> elegir Retro -> junio).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**C3 CERRADO 9/9 (2026-08-10)** — `more-link` migrado. Clase propia
(`ChronosMoreLinkProvider` + componente), NO `Region` : el precedente es
`agenda-item` — renderiza un `<button>` real y `Region` cae a `<div>` , que
pondría el `type` declarado sobre un no-botón (el defecto que destapó el chip).
Cadena `Popover.Trigger → MoreLink → Button → un solo elemento` .
⚠️ El bag del popover va **el último** en el spread: el `aria-labelledby` de su
content cae al id del trigger, así que esa identidad tiene que ganar.
Medido en `/uix/components/chronos` : `data-count="6"` (nunca se había emitido),
`role` , `type` y `aria-label` del morfo conviviendo con `aria-haspopup` /
`aria-expanded` / `aria-controls` / `data-state` del popover en el MISMO nodo.
`check` 74 = base, DELTA CERO · 458✓.
⚠️ **CUARTA declaración falsa destapada al registrar** — y esta no es de chronos:
`v.translationRef('moreEvents')` **normaliza la clave literal**
(`compile.ts:normalizeTranslationRef` → `#?components.chronos.moreEvents` ), NO
consulta el mapa `texts` del morfo. El catálogo la tiene como `more-events` , así
que el lookup falla y sale el FALLBACK inglés. Medido con la página en `es` :
```
today-button aria-label="Hoy" ← clave 'today', coincide con el catálogo
prev-button aria-label="Previous month" ← 'prevMonth' ≠ 'prev-month' → fallback
next-button aria-label="Next month" ← 'nextMonth' ≠ 'next-month' → fallback
more-link aria-label="Show more events" ← 'moreEvents' ≠ 'more-events' → fallback
day-add aria-label="Nuevo evento" ← eidos tr() con clave guionada: SÍ traduce
```
El guionado no es el problema: `tr()` lo resuelve. La regla real es que
`translationRef` exige **la clave del catálogo** , y un alias camelCase muere en
silencio. ** `npm run translations:check` —el guard que lo cazaría— CASCA** antes
de comprobar nada (`SyntaxError` en `evalCatalogFile` , una `url:` multilínea de
algún catálogo), así que la clase entera lleva tiempo pasando desapercibida.
Censar los 166 morfos después de arreglar el script.
⚠️ **Label-in-Name (WCAG 2.5.3) en `more-link`** : el texto visible es
`{count} more…` (además SIN traducir, a diferencia del resto del fichero) y el
nombre accesible pasa a ser el `aria-label` del morfo, que no lleva el número.
Decisión de C4: o el `aria-label` se hace `propRef` con el conteo, o se retira y
nombra el contenido — pero entonces el texto visible tiene que traducirse.
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
`ChronosRegionProps` admite `class` y `style` explícitos: el morfo posee
`role` /aria, la receta posee la caja. Declarados uno a uno en vez de tragados
por la firma de índice, para que el reparto se lea en el tipo.
#### ⚠️ REGISTRAR UNA PARTE DESTAPA DECLARACIONES FALSAS — tres veces ya
El patrón del pase, y hay que contar con él en lo que queda: mientras nadie
registraba la parte, el morfo podía mentir sin consecuencia. En cuanto el
runtime emite lo declarado, la mentira se materializa.
| Parte | Declaraba | Qué pasó al registrarla |
| --- | --- | --- |
| `event-chip` | `defaultElement: 'button'` + literal `type` | `type="button"` sobre un `<div>` — el chip ANIDA el asa de resize y el modelo de contenido de `<button>` no lo admite |
| `grid-row` | `archetype: 'item'` | **La semana entera se pintó como fila de lista** : `archetypes.css` da a `[data-archetype='item']` cursor pointer, suelo de altura y FONDO EN HOVER. Una semana es estructura; el item es la celda |
| `grid` / `time-grid` | aria por `propRef` | el atributo desaparecía en SILENCIO si el provider no publicaba la fuente |
En los tres casos el arreglo fue **la declaración** , no el DOM ni el CSS. Y sólo
el del `grid-row` era visible a simple vista — los otros dos exigieron mirar el
valor concreto en el DOM.
#### ✔ El botón «Hoy» NO tiene bug (verificado 2026-08-10)
Reportado como que sólo movía el día y no el mes. **Medido: desde noviembre de
2026 → pulsar Hoy → agosto de 2026.** Correcto.
Lo que despista: **hoy es 2026-08-10** y el demo arranca en junio porque le pasan
`date` fijo. Estando ya en agosto, Hoy no tiene mes que cambiar y sólo mueve el
foco. ⚠️ Mi primera medición dio un falso positivo por esto mismo — avancé dos
meses desde junio y aterricé sin darme cuenta en el mes actual. **Al probar
navegación temporal, salir primero del mes de hoy.**
#### Estado de chronos al cerrar la sesión del 2026-08-10
| | |
| --- | --- |
| C1 · partes que emiten | ✅ (6) — chronos con CERO `targetOverride` |
| C2 · singletons | ✅ (8) — medidos en las TRES vistas |
feat(morfo,soma,eidos): chronos deja de tener partes que el morfo no conoce
C3 cierra con `more-link` y C4 con los seis marcadores que eidos estilizaba
sin declaracion. Los tres botones sin fuentes (`day-add`, `edit-action`,
`search-button`) comparten una clase; `undo`/`redo` se cablean sobre la del
nav; `timegrid` pasa a `time-view` porque estaba a un guion de la parte
`time-grid` que vive dentro; `peek-panel` y `day-peek` quedan como partes de
display puro con marcador inline. Cada una renderiza un `<button>` real
cuando el morfo lo declara: una `Region` que cae a `<div>` pondria el `type`
declarado sobre un no-boton.
La lupa deja de ser un adorno sin `onclick`: abre una paleta `Command.Dialog`
sobre TODOS los eventos, no un filtro de la vista, con `goToDate` nuevo en el
provider — la generalizacion de la que `goToday` era un caso particular. El
texto buscable viaja en `keywords` porque el scorer de Command lee value +
keywords y NUNCA los hijos renderizados, asi que se busca por titulo, por tipo
y por fecha.
Y tres etiquetas que mentian en silencio: `translationRef` / `commonRef`
normalizan la clave LITERAL y no consultan ningun mapa, asi que
`prevMonth` / `nextMonth` / `moreEvents` y `action.undo` / `action.redo` caian
al fallback ingles en una pagina `es`. Corregidas contra el catalogo, con
`common.buttons.undo` / `redo` anadidas.
Verificado: check 74 = base (delta cero) · 458 vitest · docs:check 0/621 ·
rtl:check 0/177 · eidos-lint chronos morfo-backed 32 -> 44, invalid 0 ·
navegador limpio en las tres vistas, con el A/B del undo medido
(18 jun -> drag -> 25 jun -> undo -> 18 jun -> redo -> 25 jun) y el salto de
la paleta (noviembre -> elegir Retro -> junio).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| C3 · repetidas | ✅ **9 / 9** |
| C4 · morfo | ✅ **7 / 7** |
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
| C5 · composición pública | pendiente |
feat(morfo,soma,eidos): chronos deja de tener partes que el morfo no conoce
C3 cierra con `more-link` y C4 con los seis marcadores que eidos estilizaba
sin declaracion. Los tres botones sin fuentes (`day-add`, `edit-action`,
`search-button`) comparten una clase; `undo`/`redo` se cablean sobre la del
nav; `timegrid` pasa a `time-view` porque estaba a un guion de la parte
`time-grid` que vive dentro; `peek-panel` y `day-peek` quedan como partes de
display puro con marcador inline. Cada una renderiza un `<button>` real
cuando el morfo lo declara: una `Region` que cae a `<div>` pondria el `type`
declarado sobre un no-boton.
La lupa deja de ser un adorno sin `onclick`: abre una paleta `Command.Dialog`
sobre TODOS los eventos, no un filtro de la vista, con `goToDate` nuevo en el
provider — la generalizacion de la que `goToday` era un caso particular. El
texto buscable viaja en `keywords` porque el scorer de Command lee value +
keywords y NUNCA los hijos renderizados, asi que se busca por titulo, por tipo
y por fecha.
Y tres etiquetas que mentian en silencio: `translationRef` / `commonRef`
normalizan la clave LITERAL y no consultan ningun mapa, asi que
`prevMonth` / `nextMonth` / `moreEvents` y `action.undo` / `action.redo` caian
al fallback ingles en una pagina `es`. Corregidas contra el catalogo, con
`common.buttons.undo` / `redo` anadidas.
Verificado: check 74 = base (delta cero) · 458 vitest · docs:check 0/621 ·
rtl:check 0/177 · eidos-lint chronos morfo-backed 32 -> 44, invalid 0 ·
navegador limpio en las tres vistas, con el A/B del undo medido
(18 jun -> drag -> 25 jun -> undo -> 18 jun -> redo -> 25 jun) y el salto de
la paleta (noviembre -> elegir Retro -> junio).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
#### C4 — hecho (2026-08-10)
Los 6 marcadores `data-chronos-*` que el morfo no conocía, resueltos así:
| Marcador | Verdicto | Cómo |
| --- | --- | --- |
| `day-add` | **PARTE pública** | Registrada: `ChronosDayAddProvider` + componente `<DayAdd>` ; el `<IconButton>` de la vista se compone por `child` . El `aria-label` a mano DESAPARECE — sale del morfo. 42 instancias medidas |
| `timegrid` → ** `time-view` ** | **PARTE privada, RENOMBRADA** | Era el shell de semana/día, a UN guion de la parte `time-grid` que vive dentro: trampa. Marcador inline (parte de display puro), 4 reglas CSS renombradas |
| `peek-panel` · `day-peek` | **PARTES privadas** | Sin role ni aria: marcador inline, que es la forma sancionada para una región estructural que no debe ser class-hook |
| `edit-action` | **PARTE pública, registrada** | `ChronosActionProvider` + `<EditAction>` sobre el `IconButton` del editor. Ver decisión 1 |
| `search-button` | **PARTE pública, registrada** | Abre una paleta `Command.Dialog` sobre TODOS los eventos. Ver decisión 2 |
⚠️ **La premisa del plan era falsa en dos puntos, medidos** :
1. Decía «las 5 partes reales **que el CSS de eidos estiliza** ». `search-button`
**no tiene ni una regla CSS ni un `onclick`** en todo el repo: una sola
aparición, el atributo. No es una parte por declarar, es una lupa que no
hace nada.
2. Suponía que estos marcadores estaban en rojo. `eidos-lint chronos` medido
antes y después (restaurando morfo+css de HEAD): ** `invalid: 0` en ambos**.
Los clasificaba como *eidos-only* , no como inválidos. Lo que cambia es
**morfo-backed 32 → 43** (y eidos-only 53 → 42): el contrato pasa de señal
DOM sin declarar a declarado. Ese es el valor, no un guard en rojo.
⚠️ ** `class-hooks: 115` ** en chronos, sin cambio. La norma S1 de eidos-lint los
prohíbe (`component:audit` exige 0). Es la familia `.chronos-tg-*` /
`.chronos-peek-*` entera: deuda grande, PREEXISTENTE, y un eje propio.
**Y de paso, el arreglo de i18n medido** (§C3): con las claves del catálogo,
`prev` /`next`/`more-link` pasan de fallback inglés a ** «Mes anterior» ·
«Mes siguiente» · «Ver más eventos»** en una página `es` .
Verificado: `check` **74 = base, DELTA CERO** · **458 ✓** · `contracts.test.ts`
los MISMOS 6 fallos ajenos (ninguna fila de chronos) · `docs:check` 0/621 ·
`rtl:check` 0/177 · navegador en las TRES vistas + el peek abierto, **consola
limpia en pestaña nueva**.
⚠️ **Dos trampas de verificación en este pase** :
- `prettier --check` sobre una copia del fichero FUERA del proyecto no valida
nada: no resuelve el plugin de svelte y **falla parseando** , lo que se lee
como «pasa» si sólo miras la lista de warns. `chronos-view.svelte` YA estaba
sucio en HEAD; lo descubrí sólo al comprobarlo in situ.
- `prettier --write` sobre él lo pasó a LF y produjo un diff de **719 líneas**
([[reference-git-crlf-wholefile-diff]]). Revertido y reaplicados los tres
cambios a mano: el diff real es **34 líneas** . No formatear ficheros que ya
estaban sucios.
#### C4 — las 3 decisiones, resueltas por el autor (2026-08-10)
**1 · `edit-action` → DECLARADA.** El plan decía retirarla; el autor decidió lo
contrario y es lo coherente: retirarla no era ejecutable (su única regla CSS la
posiciona en la esquina del diálogo, y cambiar el gancho por una clase es justo
lo que la norma S1 prohíbe), y el precedente del mismo día lo contradecía —
`more-link` dentro de un Popover compuesto y `day-add` dentro de un IconButton
compuesto SON partes. Alcanzar un componente compuesto con **tu propio attr con
namespace** es el patrón sancionado. Medido: `role` · `type` ·
`aria-label="Editar evento"` del morfo, la caja sigue a 20/20 px de la esquina
del diálogo (medido contra el rect real), y el click sigue abriendo la edición
(19 campos `data-readonly` → 0).
**2 · `search-button` → PARTE + PALETA sobre TODOS los eventos.** No había spec
en ninguna parte (ni `SPEC.md` ni `CONTINUE.md` ; el README sólo lo mencionaba
como composición) y en soma/engine no había una línea de búsqueda, así que la
conducta la eligió el autor: paleta que busca en todo el almacén, no filtro de
la vista. Construido con ** `Command.Dialog` del propio framework** — que YA
existe (paleta modal sobre el Dialog de eidos, con `mod+k` de serie) y que
estuve a punto de recomponer a mano: leer el barrel antes de escribir markup.
- Provider: ** `goToDate(date)` **, la generalización de la que `goToday` era un
caso particular (mismo ancla rovinga, anuncio y emisión `shift-navigate` ).
- El texto buscable viaja en ** `keywords` **: el scorer de Command lee
`value` + `keywords` y **NUNCA** los hijos renderizados, y el `value` es el id
del evento. Por eso se busca por título, por tipo Y por fecha.
- Una serie recurrente sale UNA vez, igual que `openEdit` , que edita la serie.
Medido: `ctrl+k` ✓ · `meta+k` ✓ · el botón ✓ · 18 eventos listados con
«Título — 8 jun 2026 · Todo el día» · filtro por título («trip» → 1) y **por
fecha** («18 jun» → Retro) · vacío traducido · y el salto de marco:
**noviembre de 2026 → elegir «Retro» → junio de 2026** con el editor abierto.
⚠️ **Lo que NO se pudo verificar** : que la paleta se DESMONTE al elegir. Su
`data-state` pasa a `closed` (correcto), pero el nodo permanece porque **el
panel oculto congela las animaciones de salida** — medido: `document.hidden =
true` y `scale-out` /`fade-out` en `running` con `currentTime: 0` . Control: el
diálogo del editor tampoco cierra con Escape en ese estado (22 animaciones
congeladas). Es el entorno, no el código — pero queda por confirmar con el
panel visible.
**3 · `undo-button` / `redo-button` → CABLEADOS.** `ChronosNavPart` pasa de 3 a
5 partes (misma clase, el patrón de paginación de Calendar) y la barra los
renderiza en su propio `chronos-nav-group` . Medido de punta a punta:
```
Retro, 18 jun 2026 → drag → 25 jun → undo → 18 jun → redo → 25 jun
undo deshabilitado al arrancar (aria-disabled=true + data-disabled), habilitado tras el move
```
⚠️ **Y destapó la MISMA clase de fallo de traducción, tercera vez** : los dos
declaraban `v.commonRef('action.undo')` y ** `common.action` no existe** — el
grupo es `common.buttons` . Añadidos `buttons.undo` / `buttons.redo`
(«Deshacer» / «Rehacer») y repuntadas las declaraciones. `docs:vocabularies`
regenerado (38 → 40 shared strings), que es lo que puso `docs:check` en rojo
hasta regenerarlo.
⚠️ **La abstracción llegó a sus dos consumidores** : `ChronosDayAddProvider` pasa
a `ChronosActionProvider` con parte `'day-add' | 'edit-action'` — mismo
contrato (botón real, role + type + aria-label traducido, CERO fuentes), el
patrón de `NavButton` . Los componentes siguen siendo dos con nombre propio
(`DayAdd`, `EditAction` ), que es lo que C5 necesita para la API compuesta.
**Después: C5 — la composición pública**, que es el motivo por el que se anuló
la decisión de `exports.ts` : exponer las partes al consumidor con la forma de
Table/Calendar y recomponer `<Chronos {events} />` desde ellas.
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
Verificación al cierre: `check` **74 = base, DELTA CERO** sostenido en cada
lote · **458 ✓** (chronos + morfo + sema) · navegador limpio en las tres vistas.
Censo global de `targetOverride` : **148 → 89** .
feat(morfo,soma,eidos): chronos deja de tener partes que el morfo no conoce
C3 cierra con `more-link` y C4 con los seis marcadores que eidos estilizaba
sin declaracion. Los tres botones sin fuentes (`day-add`, `edit-action`,
`search-button`) comparten una clase; `undo`/`redo` se cablean sobre la del
nav; `timegrid` pasa a `time-view` porque estaba a un guion de la parte
`time-grid` que vive dentro; `peek-panel` y `day-peek` quedan como partes de
display puro con marcador inline. Cada una renderiza un `<button>` real
cuando el morfo lo declara: una `Region` que cae a `<div>` pondria el `type`
declarado sobre un no-boton.
La lupa deja de ser un adorno sin `onclick`: abre una paleta `Command.Dialog`
sobre TODOS los eventos, no un filtro de la vista, con `goToDate` nuevo en el
provider — la generalizacion de la que `goToday` era un caso particular. El
texto buscable viaja en `keywords` porque el scorer de Command lee value +
keywords y NUNCA los hijos renderizados, asi que se busca por titulo, por tipo
y por fecha.
Y tres etiquetas que mentian en silencio: `translationRef` / `commonRef`
normalizan la clave LITERAL y no consultan ningun mapa, asi que
`prevMonth` / `nextMonth` / `moreEvents` y `action.undo` / `action.redo` caian
al fallback ingles en una pagina `es`. Corregidas contra el catalogo, con
`common.buttons.undo` / `redo` anadidas.
Verificado: check 74 = base (delta cero) · 458 vitest · docs:check 0/621 ·
rtl:check 0/177 · eidos-lint chronos morfo-backed 32 -> 44, invalid 0 ·
navegador limpio en las tres vistas, con el A/B del undo medido
(18 jun -> drag -> 25 jun -> undo -> 18 jun -> redo -> 25 jun) y el salto de
la paleta (noviembre -> elegir Retro -> junio).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
#### ⚠️ TRES defectos de conducta medidos en el navegador (2026-08-10) — eje NUEVO
Salieron al responder dos preguntas del autor sobre la vista de mes. Ninguno es
de superficie perceptual; se anotan aquí para que no se pierdan.
1. **La 6ª fila fantasma.** `fixedWeeks` vale ** `true` por defecto en chronos**
(`components/chronos.svelte:20`), al revés que Calendar / DatePicker /
RangeCalendar / DateRangePicker, que lo traen `false` . `buildMonthGrid`
rellena entonces hasta 42 celdas (`engine/grid.ts:67`), así que un mes que
cabe en 5 filas (junio de 2026: lun 1 jun → dom 5 jul) se inventa una 6ª
fila **entera del mes siguiente** . Decisión: alinear el defecto con la
familia, o mantener la altura estable creciendo las filas en vez de añadir
una semana.
2. **Una ocurrencia de evento recurrente NO se puede mover.** Su id es
sintético (`${serie}::${YYYY-MM-DD}`, `engine/recurrence.ts:145` ) y
`moveEventToDay` la busca en `calendar.events` , que sólo guarda la SERIE →
`find` devuelve `undefined` y **sale sin hacer nada, en silencio** . Medido:
el destino se ilumina (`data-dragover`) y al soltar el chip sigue donde
estaba. Afecta igual a un día de este mes (2026-06-04) que a uno de fuera,
así que no es un problema de mes. Mismo agujero previsible en
`moveEventToDayTime` / resize / delete de una ocurrencia.
**Los eventos normales SÍ caen en las celdas de fuera de mes** — medido:
`Retro` → 2026-07-08 y `Launch` → 2026-07-09 aterrizan y se repintan allí.
3. **El chip pierde `data-event-id` tras un intento de arrastre.** El atributo
—`propRef` + `emit: 'value'` , parte del contrato C1— desaparece del nodo
mientras `data-color` y `aria-label` , del MISMO bag, siguen. El
`data-value` de DragDrop conserva el id. Tres chips distintos, reproducible.
Medido con eventos de puntero sintéticos: confirmar con un arrastre real
antes de diagnosticar.
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
#### ⚠️ Hallazgo — chronos estampa fuera del morfo
`commit-save` y `commit-delete` declaran `event-chip` y aterrizan en los botones
Guardar / Eliminar **del editor** , que no son partes de chronos (el morfo no
declara ninguna parte de editor). `commit-move` aterriza en la celda del drop.
`shift-navigate` (declara `provider` ) aterriza en `prev-button` / `next-button` /
`today-button` / `day-cell` . Los dos primeros no se pueden declarar —
`allowedTargets` sólo admite partes de ESTE morfo— así que la decisión es tuya:
retirar la redirección, o que el editor tenga partes. `commit-restore` /
`commit-apply` (undo/redo) NO redirigen hoy: ningún llamante pasa elemento y la
vista de eidos no cablea los botones que el morfo declara.
#### La cola de la campaña
Clase C, pendientes de declarar (el censo da la lista de partes por evento):
`context-menu` · `dropdown-menu` · `editable` · `file-upload` · `float-panel`
(drag/resize → `header` / `resize-handle` / `resize-grip` ) · `grid-list`
(`selection-checkbox`) · `month-grid` y `year-grid` (`shift-navigate-step` →
`cell` ) · `palabras` (8 eventos) · `range-calendar` (`shift-navigate` → `day` ) ·
`search-field` · `stepper` · `table` (`commit-set-sort` → `sort-trigger` ) ·
`tag-group` · `tags-input` · `command` (`link-item`) · `tree-view`
(`commit-select` ya declarado, revisar `provider` ).
Sin resolver por el censo (los mira una persona): `menu-dial` , `onion-menu` ,
`metrics` (emiten desde el template de eidos) · `path-trace` , `rotate-align` ,
`drag-drop` , `virtual-list` , `virtual-grid` , `timeline` , `menubar` ,
`navigation-menu` , `css-field` , `number-field` (`commit-set`).
fix(sema,morfo,soma): una superficie no admite dos ocurrencias, y el nombre ya no miente
`data-event-*` es UNA RANURA por elemento. Tres auditorias independientes
encontraron el mismo defecto y ninguna lo cerro: fable S1 (2026-07-01, con el
README diciendo «findings executed»), sema S-17 (2026-08-05, midio el estampado
del knob muriendo a los 24ms de un hold de 240) y blocks A-36/A-65 (reproducido
en navegador, dos estampados a 1,1ms). Lo que faltaba no era diagnostico: nadie
podia distinguir una redireccion legitima de una deriva, porque la opcion que
redirigia se llamaba `fallbackTarget` y hacia lo contrario de lo que decia.
EL NOMBRE PRIMERO. `fallbackTarget` -> `targetOverride` (202 apariciones, 64
ficheros, dirigido con `git ls-files`). Siempre GANO sobre el ref registrado; el
nombre honesto es la precondicion para auditarlo. Las cronicas conservan el viejo
por diseño.
LA PROPIEDAD. `unstampEventAttrs` recibia el signal y lo DESCARTABA, asi que la
ocurrencia que terminaba antes borraba a la que tenia la ranura. Ahora comprueba
`data-event-id`. Con eso, `replace` deja de ser el comportamiento roto y pasa a
ser el suelo correcto.
A-36, SIN `regime`. Los cuatro overlays cedian el estampado del `open` al
trigger por un workaround de la era `sequence: 'pre'`; con `post` el content ya
esta montado. Retirado el override (float-panel migrado `pre`->`post`, ultimo
fuera de la doctrina que sema.md escribe). Medido en /blocks/site-header/preview
a 375px: contact-activate en el TRIGGER +18,7ms y open en el CONTENT +20,6ms,
con press-squeeze y slide-from-right-full corriendo. Antes: los dos en el
trigger, press-squeeze jamas.
`regime` DEFINIDO — y son dos valores, no cuatro. `replace` (default) y `queue`,
este solo para las 4 parejas irreducibles: los tres toggles (el provider ES el
boton) y el knob. `collapse` retirado (su caso murio con la propiedad: medido, 0
huecos en 5 emisiones a 72ms) y `lock` retirado (significaria «un cierre que no
se anuncia»), con sus 4 declaraciones muertas.
El guard de `queue` PASO EN VERDE SIENDO INCORRECTO: con timers falsos no hay
animaciones, asi que awaitExpression volvia al instante. El navegador midio el
commit encolado llegando 1,6s tarde. `queue` espera el HOLD, no la expresion.
Medir la envolvente no es medir la salida.
`renderAttrs` — los attrs que no son partes. Vaciar ACTIVE_DEV_TRACK (palabras y
chronos ya no estan excluidos) destapo 23 `data-palabras-*` que ningun morfo
declaraba y que palabras.css SI estiliza: aterrizan en el arbol que el usuario
escribio, asi que no pueden ser partes. Campo nuevo con la linea afilada —si el
consumidor puede componerlo, es una PARTE— y 3 consumidores (palabras, waveform,
aura). Declarado en types.ts Y en el esquema sium: la leccion de MorfoElement.
Ademas: 5 selectores muertos en el predicado del focus-scope de palabras
(barrido de todo el repo: aparecian solo ahi) · RTL-1 real en palabras-chrome
(inset logico + translate fisico = doble volteo; rtl:check 1->0) · el union
inline al `Direction` canonico, que destapo que el censo de direccion grepea el
NOMBRE del tipo · deuda de chronos (scope, barrel, README).
Y de la cola: S-36 (barrel 68->71 + guard), S-31 (vibrate(NaN)), S5 (la
preferencia de reduced-motion del usuario ya gana a los channels del morfo), SO2
(el warn que el JSDoc prometia), SO4 (validar el target antes del prewrite).
Todos los guards nuevos vistos fallar antes de arreglar nada.
Base: check 74 (= base) · sema+morfo+contracts 454 verdes / 6 ajenos
preexistentes · soma navegador 1258/1259 (el rojo es un timeout ajeno,
reproducido con los cambios en stash) · docs:check 0/618 · rtl:check 0/177.
Handoff: docs/process/CONTINUE-perceptual-surface.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 3.2 Restitución de persistentes — **eje NUEVO, ninguno de `regime` lo cubre**
Medido con el engine real:
```
afterWarn: "signal-warn-invalid" ← el aviso untilFix proyectado
duringCommit: "commit-submit" ← el commit lo pisa
afterCommit: null ← al limpiar se lleva el aviso
engineStillHoldsWarning: true ← el engine lo cree vivo
```
Una persistente **no compite por la ranura: es un estado al que la ranura debe
VOLVER**. Propuesta: tras el hold de una transitoria, el engine re-proyecta la
persistente viva de ese nodo. Afecta a `form` , `file-upload` , `textarea` ,
`media-player` , `announce` , `aura` .
### 3.3 Eje B — ordenación y emisión
- **`sequence: 'coincident'` sigue sin implementar** (idéntico a `pre` ); 21
eventos lo declaran. Implementarlo (emit sin await + handler inmediato) o
marcarlo reservado.
- **4 continuos sin throttle** contra la doctrina de `sema.md` :
`gradient-builder` , `color-picker` (área), `number-field` , `css-field`
emiten en CADA `pointermove` . Cinco compañeros sí throttlean
(`DRAG_SIGNAL_MS`). ⚠️ `rotate-align` NO es infractor: throttlea por detente.
- **Las 68 demos emiten sin `verb` ** → desde el rediseño del 2026-08-06 el verbo
elige el sonido, así que un play de `emerge-close` **suena `open`** y
`commit-delete` suena `tick` en vez de `tick.loss` . El bloque está copiado 68
veces; extraer un helper a `$demo` es refactor aparte.
- **`knob.handle-drop` es `pre` ** con handler → `data-dragging` sobrevive 240 ms
al soltar. ** `button.contact-activate` ** retrasa `onPress` 120 ms.
### 3.4 Eje C — cola de `AUDIT-sema-2026-08-05`, re-verificada abierta
`S-19` (`AnnounceFn` no compila contra `uix.announce` ; ninguna raíz cablea el
canal) · `S-27/S-41` (`prepare()` abre AudioContext con `sound:'off'` ) · `S-10`
(rama visual sin `??` → revienta el boot) · `S-33` (`'forbidden'` no prohíbe) ·
`S-34` (gemelo del schema copiado a mano) · `S-35` (la etiqueta esquiva la
política) · `S-30/S-38` (5 waivers, ya desbloqueados por S-31).
### 3.5 Eje D — fable, lo que sigue abierto
`M1 emission` (D.2, aceptada y nunca implementada — cerraría los waivers de
tooltip y gradient-picker) · `M4` (`enumPair` frágil con 3+ valores) · `M5`
(tipar el matcher `state` ) · `M6` (`IntentExpectedFamily` deprecado, × 2) ·
`E2` (`globalThis.matchMedia` crudo en eidos) · `E3` (catch mudo en
`#renderSchemeCss` ).
### 3.6 Eje E — canon del ledger de blocks
`A-47` (anillo de foco 1.43:1 sobre panel sólido) · `A-69` (`count-up`:
`duration` no significa segundos) · ** `A-85` confirmado**: el morfo del drawer
declara el `aria-label` del trigger **sin condición** (a diferencia de `button` ,
que usa `prop-truthy` ), así que pisa el del consumidor → arreglar **y censar la
clase** en los 166 morfos.
---
## 4 · Decisiones del autor, pendientes
- **`mode` y `scope` **: 0 declaraciones, 0 consumidores, 0 JSDoc — la misma
situación que `collapse` y `lock` , ya retirados. ¿Se retiran también?
- **`regime` es por EVENTO, pero la colisión es de la SUPERFICIE.** Para la clase
intra-morfo el evento vale; en A-36 chocaban dos morfos distintos y ninguno
podía saber del otro. Si vuelve a aparecer una colisión cruzada que no se
resuelva quitando la redirección, hay que decidir quién manda.
---
## 5 · Cómo verificar en este hilo
**Línea base ANTES de tocar** — hay 6 fallos AJENOS y preexistentes en
`contracts.test.ts` (waveform barrel · provider filenames · MOR-4 · DOM writes de
menubar · data-attrs · namespaces de aura), y 1 en el proyecto de navegador de
soma (un timeout de 5 s en `migrated providers re-declare no morfo attrs` ,
reproducido con los cambios en stash). Clasificar míos vs ajenos **por contenido
del diff**, nunca por confianza.
```
npx vitest run src/uix/sema src/uix/morfo src/uix/contracts.test.ts --project=server
npx vitest run src/uix/soma # proyecto navegador
npm run check # 74 errores = base
npm run docs:check # 0 / 618
npm run rtl:check # 0 / 177
```
**El navegador NO es opcional en este eje.** El A/B que aísla la causalidad:
`MutationObserver` ligado **al nodo** (no a un selector) sobre `data-event*` , más
`getAnimations()` . Dos trampas medidas hoy:
- **El panel oculto suspende rAF y no compone**: un bucle sobre
`requestAnimationFrame` se cuelga, `animationstart` no dispara y el
`screenshot` falla. Muestrear con temporizador y leer `getAnimations()` /
`getComputedStyle` , que sí son fiables ahí.
- **`awaitExpression` llega a su tope de 1500 ms** con el panel oculto, porque
las transiciones nunca terminan. No confundirlo con un defecto del código.
**Audio**: render offline replicando el grafo (`OfflineAudioContext`) y medir
pico / RMS. Contar osciladores responde «¿llegó algo?», nunca «¿se oye?».