You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/AUDIT-docs-code-ledger.md

571 lines
39 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# AUDIT — contradicciones docs↔código: registro de defectos (ledger)
> **Fuente única del estado de los defectos que salieron del barrido
> docs↔morfo del 2026-08-11.** Hasta hoy vivían sólo en la memoria de la
> sesión que los encontró, que es una superficie de recuerdo del agente y no
> del proyecto: un handoff que no se carga los pierde. Este fichero es la casa.
## Por qué existe este fichero
El 2026-08-11 un barrido de alta precisión sacó **101 contradicciones** entre la
documentación y el morfo. La instrucción inicial fue «borra toda la basura», y
fue corregida en el acto: _«puede ser que la documentación esté desfasada o que
el código esté obsoleto, hay que evaluar primero cuál es el caso»_. Sin esa
corrección los 101 hallazgos se habrían aplanado contra el morfo y **habría
desaparecido la única huella de nueve averías reales**.
De ahí el veredicto por sitio, con evidencia (`git log -S`, docs de decisión, el
componente vivo):
- **DOC-DESFASADA** — la documentación miente sobre código correcto → se arregla.
- **CÓDIGO-OBSOLETO** — el código miente sobre documentación correcta → **se
reporta, no se toca**. Son las filas de abajo.
- **FALSO-POSITIVO** — el script se equivocó.
⚠️ Aun con la instrucción explícita, **dos agentes borraron filas de «Gaps» que
documentaban un defecto abierto**. Repuestas a mano. La lección es la regla 3.
## Las reglas de este registro
1. **Se escribe por `id`.** `D1`…`D14` no se renumeran nunca. Un defecto que
resulta ser dos se parte en `D3a`/`D3b`, no desplaza a los siguientes.
2. **Un `ARREGLADO` lleva su commit.** Sin sha no es un arreglo, es una opinión.
3. **Una fila de gap o de known issue NUNCA es basura**, aunque cite nombres
muertos: es la huella de una avería. Borrarla es borrar el defecto, no
arreglarlo.
4. **«No lo referencia nadie» no es un veredicto.** Es una observación. Antes de
proponer retirar algo hay que abrir TODOS los hits de `docs/` que lo nombren
— `D10` nació precisamente de saltarse esto.
Estados: `CONFIRMADO` (medido, abierto) · `ARREGLADO` (con sha) ·
`PENDIENTE` (sin re-medir) · `DIFERIDO` (confirmado, disposición firmada).
## La tabla
| id | Componente | Estado | Prioridad | Una línea |
| --- | --------------------------- | -------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------- |
| D1 | AlertDialog | `ARREGLADO` `f4e414e2e` | P0 | Action y Cancel cerraban sin causa: mudos |
| D2 | Splitter (eidos) | `ARREGLADO` `6f42eebfe` | P0 | regla CSS enganchada a un evento renombrado en mayo |
| D3 | 5 pickers | `ARREGLADO` `f4e414e2e` | P0 | `commit-reset` declarado y jamás disparado |
| D4 | ColorPicker | `ARREGLADO` `f4e414e2e` | P0 | swatch y eyedropper commitean en silencio |
| D5 | los 5 pickers | `ARREGLADO` | P1 | en `modal`, un cierre que no declara causa es un descarte, y un descarte no commitea |
| D6 | date/time/color-field | `ARREGLADO` | P1 | `Home`/`End` en la familia de segmentos, con una sola acción declarada |
| D7 | 5 componentes de campo | `ARREGLADO` | P1 | el Label renderizaba `<div>` y el morfo declara `label` |
| D8 | los 7 `arrow` | `ARREGLADO` | P2 | `defaultElement` falso en el `arrow` — sistémico, confirmado |
| D9 | chronos | `ARREGLADO` | P2 | el acarreo habla: `handle-drag` y `handle-resize`, anclados y a la cadencia del gesto |
| D10 | sema ↔ `$sound` | `ARREGLADO` (2026-08-12, desde el eje) | P1 | los 3 resolvers de gesto RETIRADOS por la sesión del eje de sonido, con los 8 sitios rancios adjudicados |
| D11 | catálogo | `ARREGLADO` | P1 | `defaultElement` mentía en 28 partes; las 28 dicen ya la verdad y el guard no tiene excepciones |
| D12 | context-menu · link-preview | `ARREGLADO` | P2 | sus flechas componen ya la primitiva compartida: una flecha, una forma |
| D13 | 4 componentes | `CONFIRMADO` | P1 | 7 eventos declarados que nadie emite — enumerados y guardados |
| D14 | chronos | `CONFIRMADO` | P1 | `restoreChipFocus` pierde el foco: el movimiento por teclado muere al primer paso |
---
## Fichas
### D1 · AlertDialog mudo — `ARREGLADO` (`f4e414e2e`)
Action y Cancel llamaban `dialog.handleClose()`, que sólo voltea `open`: nunca
pasaban por `dismissWith` ni disparaban `emerge-close`. Medido: sólo sellaba
`contact-activate`. Escape sí funcionaba, así que **el teclado tenía firma y los
botones no**.
Arreglo: Action llama `dismissWith('save', { intent })` y Cancel
`dismissWith('cancel')`. `dismissWith` gana un `opts.intent` porque la causa es
vocabulario cerrado pero el PESO es por instancia — el mismo eje por el que
`emerge-open` liga con `fromProp`. Medido: Action → commit/risk/saved, Cancel →
emerge/cancelled.
### D2 · Regla CSS muerta del splitter — `ARREGLADO` (`6f42eebfe`)
`splitter.css` enganchaba `data-event='commit-resize'`, evento renombrado a
`commit-set` en `bd2e40366` (2026-05-22). No se arreglaba renombrando: el
`commit-set` de hoy apunta al `provider`, no al trigger.
Arreglo: la regla engancha por familia sobre el provider
(`[data-event-family='commit'][data-event-phase='active']`) y pinta el trigger
con **selector descendente**, más un opt-out del anillo global de commit. La
firma que pinta otro nodo es un selector descendente — no una razón para mover
el sello (doctrina en `docs/architecture/eidos.md` §«descendant-selector
corollary»).
### D3 · `commit-reset` mudo en 5 pickers — `ARREGLADO` (`f4e414e2e`)
Declarado en date / time / date-range / time-range / color y jamás disparado:
`clear()` sólo asignaba.
Al arreglarlo salió **una segunda avería debajo**: date-picker y
date-range-picker apuntaban `commit-reset` a la parte `calendar`, que nadie
registra en el runtime — destino inalcanzable, emit rechazado en silencio.
Retargeteados al `provider`.
⚠️ **Una parte declarada sin registro en runtime es un destino inalcanzable, y
no chilla.** Vale para todo el catálogo, no sólo para estos dos.
### D4 · ColorPicker commitea en silencio — `ARREGLADO` (`f4e414e2e`)
Swatch y eyedropper aplican el color sin disparar nada, pese a que el morfo **y**
el pack de sema declaran que disparan `commit-set`.
### D5 · Escape en `modal` cierra sin revertir — `ARREGLADO` (2026-08-12)
⚠️ **Dos afirmaciones de esta fila eran falsas y se corrigen aquí.**
1. _«`mode='modal'` promete bloquear Escape»_ — la promesa existía sólo en
[`picker-shell-handle.svelte.ts:32`](../../src/uix/soma/components/picker-shell/picker-shell-handle.svelte.ts)
y en un comentario de `date-picker.svelte`. **Era doc rancia**: el 2026-08-11
se adjudicó lo contrario en tres pickers, con razón escrita
([`time-picker/types.ts:42`](../../src/uix/soma/components/time-picker/types.ts)):
Escape descarta en AMBOS modos porque el morfo declara el patrón APG
`combobox` y el framework bloquea Escape en exactamente un sitio,
`alertdialog`; tragárselo dejaría a un usuario de teclado sin salida salvo
que el consumidor recordara componer un Cancel. La adjudicación no se propagó
al handle compartido ni a date-picker / date-range-picker. **Propagada el
2026-08-12.**
2. _«No hay buffer»_ — sí lo hay:
[`date-picker-provider.svelte.ts:153`](../../src/uix/soma/components/date-picker/date-picker-provider.svelte.ts)
captura `valueOnOpen` en la arista de apertura y `cancel()` (:262) lo
restaura.
**Lo que sí es un defecto, medido en navegador el 2026-08-12** (date-picker en
`mode='modal'`): el popover no autocierra al elegir ✓, pero Escape **cierra y se
queda la edición** (05/20 → 05/10). Es un tercer camino de salida que se comporta
como Save, y el contrato documentado del modo sólo tiene dos: Save confirma,
Cancel revierte.
La causa está a la vista: `PickerCloseCause` es `'save' | 'cancel' | 'dismiss'`
([`picker-shell-handle.svelte.ts:28`](../../src/uix/soma/components/picker-shell/picker-shell-handle.svelte.ts))
y **`'dismiss'` no lo produce nadie** — misma clase que D1/D3/D4. Escape cierra
por la vía del Popover sin pasar por `closeWith`, así que ni sella causa ni
revierte. Ningún picker cablea `escapeKeydownBehavior`: los cinco se comportan
igual.
**ARREGLADO 2026-08-12** — y con una tercera corrección de la fila: _«`'dismiss'`
no lo produce nadie»_ también era falso. Lo produce el **Popover**
(`dismissWith('dismiss')` en su manejador de Escape), no el picker; por eso el
picker nunca se enteraba. Grepear sólo el directorio del picker lo escondió.
El arreglo **no intercepta la tecla**, que sería frágil y estaría en la capa
equivocada (el `Content` es el del Popover, y su gancho de Escape es prop del
consumidor). Vigila la **arista de cierre**: un cierre que no declara causa es un
descarte, y en `modal` un descarte revierte al valor con el que se abrió. Eso
cubre además a un consumidor que baje `open` a mano, que tampoco es un Save. En
`inline` no revierte nada — allí la selección se aplicó según se hacía y el
snapshot sólo sirve para `cancel()`.
Vive una vez, en `watchPickerDismiss` (`picker-shell-handle.svelte.ts`), y los
cinco pickers lo consumen: eran cinco copias del mismo `watch` sobre `open`. El
provider declara su causa desde `closeWith`, así que `commit()` y `cancel()`
pasan intactos.
Medido en navegador (date / time / color-picker en `modal`): se edita, Escape
cierra el popover Y el valor vuelve al de apertura. ⚠️ La primera medición dijo
«Escape BLOQUEADO» — era la sonda leyendo a 400 ms, en plena animación de salida;
la traza temporal muestra el popover aún presente a +200 ms y ya cerrado, con el
valor revertido, a +600 ms.
Cinco tests sobre el guard compartido (`picker-shell-handle.svelte.test.ts`),
**vistos fallar** desactivando la reversión. ⚠️ Los tres primeros pasaban _en
vacío_ hasta que se les añadió `flushSync`: `watch` no ve una transición si las
tres escrituras caen en el mismo tick, así que no observaba ninguna arista.
### D6 · `Home` / `End` en la familia de segmentos — `CONFIRMADO` · P1
⚠️ **Otra fila con dos afirmaciones falsas.** Decía «`Home`/`End`/`PageUp`/
`PageDown` documentados en 4 sitios» y «las tres páginas se contradicen sobre qué
harían». `PageUp`/`PageDown` **no aparecen en ninguna parte**, y las tres
auditorías **coinciden** — no se contradicen:
| Documento | Qué especifica |
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`audit/components/date-field.md`](../audit/components/date-field.md) F-1 | `{ key: 'Home', action: 'set-min' }` / `{ key: 'End', action: 'set-max' }` en el morfo + implementación en provider, «extensivo a time-field/color-field (misma familia de segmentos)» |
| [`audit/components/time-field.md`](../audit/components/time-field.md) F-2 | «Sin Home/End (APG spinbutton) — pass de familia de segmentos (con date/color)» |
| [`audit/components/color-field.md`](../audit/components/color-field.md) F-2 | «especialmente útil aquí: canales 0-255/0-360 — pass de familia» |
No era una decisión sin tomar: era una **especificación acordada sin construir**,
con la declaración del morfo escrita literalmente en F-1.
**Hecho el 2026-08-12** — y con una tercera corrección de la fila: **color-field
YA lo tenía implementado** (`handleHomeEnd`, desde que se construyó). Su F-2
estaba desfasada. Así que el pase no inventó la conducta: copió la referencia que
ya vivía en el árbol, a date-field y time-field, incluidos sus segmentos
`dayPeriod` (donde `aria-valuemin` es AM y `aria-valuemax` es PM, así que Home /
End son absolutos donde las flechas ALTERNAN).
**Una sola acción declarada.** F-1 proponía `set-min` / `set-max`; color-field
declaraba `first-item` / `last-item`. El tipo obliga a elegir —
_«Semantic action identifier. Keep consistent across components for the same
intent»_— y el catálogo tiene dos vocabularios: el de LISTA (`first-item`, 25
usos, y `menu-dial` lo consume por el mapa de acciones del runtime) y el de VALOR
(`set-min`, que usa `knob`). Un segmento es un control de valor y no tiene ítems,
así que los tres declaran `set-min` / `set-max` y color-field se alineó. Sin
riesgo: los segmentos despachan por tecla, no por el mapa del runtime.
Medido con teclado real en el navegador, y los valores caen exactamente en el
aria que cada segmento publica: date-field mes `End→12 / Home→01` (min/max 1/12),
time-field hora `End→23 / Home→00` (0/23), color-field hex `End→ffffff /
Home→000000` (0/16777215). Home dos veces no envuelve.
Seis tests nuevos, **los seis vistos fallar** contra la conducta vieja
(inyectando el cierre del gate compartido y la retirada de cada rama). Incluido
uno para color-field, que era la referencia y **no tenía ninguno**.
### D7 · El Label de los campos renderizaba `<div>` — `ARREGLADO` (2026-08-12)
El morfo declara `defaultElement: 'label'` y el componente renderizaba `<div>`.
No eran dos componentes sino **cinco**: date-field, date-range-field, time-field,
time-range-field y color-picker.
⚠️ **La otra mitad de esta fila era falsa.** Decía «la doc promete un par
`for`/`id` que nadie emite»; no existe tal promesa en date-field —
[`types.ts:158`](../../src/uix/soma/components/date-field/types.ts) dice lo
contrario y bien: _«Prefer `aria-labelledby` with a `DateField.Label`»_. La fila
había mezclado el hallazgo del **`Field` genérico** (`field.md:19`, que sí emite
`for` condicional) con el date-field. El nombre accesible ya estaba cableado por
`aria-labelledby` desde el `role="group"` — que es el mecanismo correcto, porque
`for=` no puede apuntar a un grupo — y el clic-a-enfocar ya lo implementaba
`DateFieldLabelProvider.onclick` (enfoca el primer segmento).
Riesgo comprobado antes de tocar, no supuesto: `<label>` es `inline` por defecto
y `<div>` es `block`. Dos de los cinco reciben `display: inline-flex` de
`field.css` vía `fieldRole('label')`; los otros tres son ítems de flex o grid, y
el navegador los blockifica. Medido después en los cinco demos: ninguno queda en
`inline`, ninguno arrastra un `for` que no pueda honrar, y la maquetación no se
mueve.
### D8 · `defaultElement` falso en los `arrow` — `ARREGLADO` (2026-08-12)
La sospecha de deuda sistémica era cierta y peor de lo escrito: **los siete
`arrow` del catálogo declaraban `div` y ninguno renderiza un `div`.** Cinco
(combobox, dropdown-menu, popover, select, tooltip) componen
[`components/internal/arrow.svelte`](../../src/uix/soma/components/internal/arrow.svelte),
cuyo elemento de parte es un `<span>`; dos (context-menu, link-preview) rinden un
`<svg>` propio. Cada morfo declara ahora lo que su componente renderiza.
### D11 · `defaultElement` miente en 24 partes más — `CONFIRMADO` · P1
El censo que cerró D8 destapó la clase entera: **nada verificaba
`defaultElement`**, y es un contrato — `assertContract`, las recetas y quien lee
el morfo lo toman como el tag que aterriza en el DOM.
Hay un guard desde el 2026-08-12
([`contracts.test.ts`](../../src/uix/contracts.test.ts), «guards defaultElement
against the tag the soma part actually renders»), con las 24 restantes
enumeradas en `DEFAULT_ELEMENT_DEBT` para que la deuda sea visible y no crezca.
El guard lleva además un detector anti-podredumbre: si una entrada de la lista
deja de estar rota, falla pidiendo que se retire.
**Cerradas 24 de 28 el 2026-08-12** (eran 28, no 24: mi cifra anterior estaba
mal). Dieciséis eran el morfo yendo detrás de un componente que ya renderiza lo
más correcto — edición de declaración, el DOM no se mueve. Ocho más parecían del
grupo contrario y no lo eran: declaran un `role` explícito que carga la semántica
que daría el elemento nativo (`role='list'`, `listitem'`, `treeitem'`, `group'`,
`article'`, `tablist'`) o son decorativas (`aria-hidden`). Ahí la a11y **no está
rota** y la declaración era aspiracional; cambiar el DOM a listas nativas es una
mejora deliberada aparte, no algo que colar en un pase de verdad-del-contrato.
**Las 4 últimas, cerradas el mismo día.** Cada una pedía un cambio con
consecuencias, y la lección es que la pregunta «¿qué lado está mal?» sólo se
contesta mirando:
- `chronos.event-chip` — **el morfo tenía razón**, con el motivo escrito desde el
2026-08-10: el chip CONTIENE el tirador de redimensionado y el modelo de
contenido de un `<button>` no admite un control anidado, así que es `div` +
`role='button'`. Lo que faltaba era el puente: el «`tabindex` propiedad de soma»
que ese comentario promete no existía. Ahora el provider emite `tabindex: 0` y
un `onkeydown` que convierte Enter / Espacio en un clic (con `preventDefault`
sobre el espacio, que si no desplaza la página), y el componente puede ser el
`div` que su contrato declara.
- `tooltip.trigger` — **probé el morfo y me equivoqué; la medición lo corrigió.**
Declaraba `button`, así que rendericé un `<button>`… y la captura mostró el
cromado nativo alrededor del disparador. La receta llama a esta parte _«a
focusable shell»_ y difiere el anillo de foco _«to the wrapped element»_: es un
ENVOLTORIO, el control real del consumidor va dentro, y un `<button>` que
envuelve un `<button>` es el mismo anidamiento inválido del chip. El morfo era
el lado equivocado; declara `div`, y la a11y la dan el `tabindex: 0` del
provider y el `aria-describedby` del morfo, que es lo que el patrón APG pide.
- `stepper.item` — un `div` **sin rol** dentro de un `role='tablist'`. El patrón
es `list (tablist) > item > trigger (tab)`, y un elemento genérico entre un
tablist y sus tabs rompe la relación de posesión. Es `role='presentation'`
ahora — el envoltorio sale del árbol, igual que un `<li>` dentro de un
`role='menu'`; el idioma ya estaba en 8 componentes del catálogo.
- `color-picker.channel-input` — declaraba `input` mientras compone
`ColorField.Input`, que es `div` + `role='group'` (el contenedor de segmentos,
la misma forma que el `input` de date-field). Ninguna composición podía
satisfacer esa declaración: las piezas editables son las partes hermanas
`channel-segment`.
`DEFAULT_ELEMENT_DEBT` está **vacío**, y su comentario explica que una entrada
nueva es la decisión de publicar un contrato que miente.
Las 24 se partieron en dos, y por eso no se arreglaron en bloque:
- **El componente renderiza lo más correcto y el morfo va detrás** —
`calendar.header` / `range-calendar.header` (`header`), `field.helper-text` /
`field.error-text` (`p`), los cuatro de `slider` (`span`), `chronos.event-chip`
(`button`)… Arreglo mecánico: poner al día el morfo.
- **El morfo declara lo más correcto y el componente va detrás, con peso de
a11y** — `tree-view.item` / `.branch` declaran `li` y rinden `div`,
`tree-view.branch-content` declara `ul`, `stepper.list` declara `ol`,
`file-upload.file-list` declara `ul`, `feed.article` declara `article`,
`tooltip.trigger` declara `button`. Rendirlos como `div` pierde la semántica
de lista / landmark / control que APG espera. **Son decisiones, no ediciones.**
⚠️ Notas de método, por si alguien reescribe el guard: (1) el elemento de la
parte es **el que difunde los props**, no «el tag del último `{:else}`» — el
primer borrador usó lo segundo y se fue en FALSO VERDE, porque el `<svg>`
decorativo de `internal/arrow.svelte` no lleva spread; se descubrió inyectando
una regresión antes de fiarse. (2) Hay partes **polimórficas por diseño**
(`media-player.media` rinde `audio` o `video`, `breadcrumb.link` `span` o `a`):
el guard lee TODAS las ramas y da el contrato por bueno si el declarado está
entre ellas.
### D12 · Dos flechas se dibujan distinto que las otras cinco — `CONFIRMADO` · P2
Salió al censar D8. `internal/arrow.svelte` dibuja `<polygon>` **más** un
`<path>` de contorno trazado; las flechas propias de
[`context-menu`](../../src/uix/soma/components/context-menu/components/context-menu-arrow.svelte)
y [`link-preview`](../../src/uix/soma/components/link-preview/components/link-preview-arrow.svelte)
dibujan sólo el polígono. Dos de siete flechas del sistema no tienen contorno.
**ARREGLADO 2026-08-12**: las dos componen ya la primitiva compartida, así que
las siete flechas del sistema son el mismo `<span>` con el mismo `<svg>`, y las
siete declaran `span`. Las recetas pasan a direccionar las formas por dentro
(`polygon { fill }`, `path { stroke }` con `vector-effect: non-scaling-stroke`),
porque un `fill` sobre el envoltorio no puede pisar los atributos de presentación
que polígono y path llevan encima.
⚠️ **Y el arreglo destapó otro defecto por el camino.** Al componer la primitiva,
la flecha desapareció: `<span>` de 0×0 y sin `<svg>` dentro. Causa: los wrappers
de eidos de estos dos pasaban `{@render children?.()}` **incondicionalmente**, y
la primitiva lee CUALQUIER snippet de children como «el consumidor trae su propio
glifo» y se salta el suyo. popover y tooltip no pasan children en absoluto, por
eso nunca les pasó. Los dos wrappers son condicionales ahora — es la regla
«eidos wrappers need conditional children», reaprendida midiendo.
Medido tras el cambio: context-menu y link-preview dan `<span>` 10×5 con `<svg>`
10×5, polígono relleno y `<path>` con trazo — idénticos a tooltip, que es el
control. Antes link-preview no tenía `<path>` **ninguno**. Consola limpia.
### D9 · Eventos de chronos sin emisor — `CONFIRMADO` · P2
El morfo de chronos declara `handle-drag`
([`chronos.ts:131`](../../src/uix/morfo/components/chronos.ts)) y `handle-resize`
(:141), y **ningún provider los dispara**: el barrido de `trigger(` en
`soma/components/chronos/` da `shift-navigate`, `commit-restore`, `commit-apply`
y el trío `commit-save`/`commit-move`/`commit-delete` de `emitOnChip`. El
arrastre y el redimensionado emiten su `commit-move`, nunca la familia `handle`
que declaran. Ya declarado como S-37 en el README de chronos.
⚠️ Corrección de método, 2026-08-12: la primera lectura de esta fila dio «los
tres `commit-*` tampoco se disparan». Era falsa — se despachan **por variable**
en `emitOnChip` (`chronos-provider.svelte.ts:1240`), invisible a un grep de
`trigger('literal')`.
⚠️ Los dos documentos de chronos discrepaban: el README de soma decía que el pack
«ships rules» para ambos, y el SPEC que **se retiraron el 2026-08-07** por código
muerto. El SPEC tenía razón (el pack sólo lleva los tres `commit-*`); el README
quedó al día el 2026-08-12.
**ARREGLADO 2026-08-12 — se cablean.** `handle` pregunta _«¿estoy manipulando
directamente este objeto?»_, y una familia que no responde su propia pregunta ha
fallado en lo único para lo que existe ([CANON §2](../CANON.md)). Chronos es el
componente más manipulable del catálogo: hablaba el suelte (`commit-move`) y el
acarreo entero estaba mudo. El _handle invisible_.
No se autora nada: `handle` ya declara `sounds: { default: 'step' }` y los dos
canales en el mapa, así que el ritmo ES el sonido — un diente por emisión, a la
cadencia del gesto. Por eso las dos reglas del pack se retiraron el 2026-08-07
como código muerto: sobraba la regla, no el evento.
Estrangulado a ~14 Hz como slider y splitter, y **anclado**: los destinos son
`event-chip` y `event-resize-handle`, partes REPETIDAS, así que es una aplicación
directa de la ley de `e5811e422` (una ocurrencia con un sujeto entre muchos se
ancla). Chronos ya tenía el mecanismo en `emitOnChip`.
**Dónde el acarreo NO es de chronos.** Arrastrar un chip con el PUNTERO pasa por
el `DragDrop` compuesto, que posee ese bucle y lo expresa en `handle-pick` y
`handle-drop` — declara tres eventos y emite tres, sin acarreo continuo (CANON
regla 6: la evaluación se concentra en el drop). Añadir una tercera narración
encima serían dos componentes contando el mismo gesto. Así que `handle-drag` es
aquí el acarreo de TECLADO y `handle-resize` cubre las dos vías.
Medido: el arrastre del tirador con puntero da **6 `handle-resize` en 6 pasos**,
todos sobre `event-resize-handle`. El `handle-drag` sella `event-chip` a la
primera flecha y no más — no por el cableado, sino por **D14**.
### D10 · Los 3 resolvers de gesto: migración a medias — `CONFIRMADO` · P1
El 2026-08-06 se reescribió `dragSignalOverrides` (slider y splitter) a
`sound: 'step'` — el gesto pasa a sonar por REPETICIÓN — y eso dejó
`resolveHandleDragSound` / `resolveSliderDragSound` / `resolveSplitterDragSound`
sin un solo llamador. **No se adjudicó ninguno de los cinco sitios que los
describen como vigentes:**
| Sitio | Qué afirma |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`splitter-provider.svelte.ts:435`](../../src/uix/soma/components/splitter/splitter-provider.svelte.ts) | «Pitch/centroid/gain come from… `resolveSplitterDragSound`» — y `dragSignalOverrides`, 30 líneas más abajo **en el mismo fichero**, dice que eso se sustituyó |
| [`sema/components/splitter.ts:15`](../../src/uix/sema/components/splitter.ts) | medio cierto: soma **sí** calcula overrides por emisión, pero no por ese resolver, y «la curva dinámica sobrevive» ya sólo vale para el háptico |
| [`PLAN-sound-engine.md:95`](./PLAN-sound-engine.md) | §4, los tres en la columna **«sema conserva»** de la LÍNEA DE CORTE |
| [`PLAN-audio-player.md:367`](./PLAN-audio-player.md) | D-AP.7, **✅ CONFIRMADO**: el slider adjunta el resolver en cada `handle-drag` |
| [`AUDIT-sema-2026-08-05.md`](./AUDIT-sema-2026-08-05.md) hallazgo 7 | la regla del pack del slider está muda **a propósito** porque las primitivas vienen del resolver — razón hoy sin base |
El slider **no** arrastra el comentario rancio: el suyo describe bien la
sustitución. La contradicción interna es sólo del splitter.
**Decisión abierta del autor:** ¿se retiran los resolvers de gesto, o se
recablean? La tabla de corte de `PLAN-sound-engine.md` §4 hay que actualizarla
en los dos casos. Nada se toca hasta entonces: reescribir los comentarios para
decir «se sustituyó» contesta la pregunta y borra la huella (regla 3).
**RETIRADA EJECUTADA Y REVERTIDA el mismo día (2026-08-12).** El borrado se hizo
en `5571f3bf7` y el autor lo revirtió en el acto, con razón: **la sesión era de
normalizar nombres de evento, no de modificar la librería de sonido**, y la
retirada se ejecutó sin leer primero el handoff vivo del eje
([`CONTINUE-sound-engine.md`](./CONTINUE-sound-engine.md) — la memoria del
proyecto lo marca «PRIMERO») ni `PLAN-audio-player-v2.md` (gate firmado), que ni
siquiera se sabía que existía. La verificación posterior salió limpia — ninguno
de los seis documentos no leídos nombra los resolvers — pero salió limpia por
suerte, no por método. `sounds.ts` está restaurado byte a byte; **no se toca sin
una sesión propia del eje de sonido, que empiece por su handoff.**
**DECISIÓN FIRMADA (2026-08-12): SE RETIRAN.** La ejecuta la sesión del eje de
sonido — la condición procedimental de arriba queda intacta (su handoff
PRIMERO). Con la retirada: actualizar la tabla de corte de
`PLAN-sound-engine.md` §4, adjudicar los cinco sitios de la tabla, y un
**SEXTO** que el re-análisis del 2026-08-12 destapó:
`docs/architecture/sema.md` §«The per-emit payload owns the primitives» aún
describe el payload sonoro de la era resolver («pitch / gain / contour derived
from pointer position and velocity»), cuando lo que soma adjunta hoy es
`sound: 'step'` + curva háptica (`dragSignalOverrides` en
`slider-provider.svelte.ts:233` y `knob-provider.svelte.ts:242`, con el
trade-off del 2026-08-06 citado en el propio comentario). El homónimo
`SoundContour` se disuelve con ellos — nada que renombrar.
**EJECUTADA (2026-08-12, esta vez desde el eje).** Sesión abierta leyendo
ENTEROS `CONTINUE-sound-engine.md` + `PLAN-audio-player-v2.md` antes de tocar.
Retirados los 7 símbolos (3 resolvers · `DragSoundParams` · `SoundContour` de
sema · `clamp`/`lerp` huérfanos) + los 5 re-exports + los 2 tests que sólo se
probaban a sí mismos; el registro (`SEMA_SOUND_VOICE`, `sample`,
`resolveSoundDefinition`…) intacto. Adjudicados los seis sitios — y DOS MÁS que
esta ficha no listaba, hallados al leer el eje (la fila de estado de
`CONTINUE-sound-engine.md`, «conserva su resolver por mecanismo», y D-AP2.7 de
`PLAN-audio-player-v2.md`, «el resolver del drag posee las primitivas»), ambos
con nota fechada. El detalle: comentarios de `splitter-provider` y del pack del
splitter reescritos al modelo vigente · `sema.md` §per-emit payload dice ya la
verdad (nombre + curva háptica) · tabla de corte §4 anotada ·
`book-deviations.md` D.7 gana la nota de que su pasado se hizo verdad hoy ·
`AUDIT-sema-2026-08-05` NO se toca (crónica por cabecera propia). El homónimo
queda resuelto por disolución: sobrevive sólo el `SoundContour` del art (5
valores); los docblocks que contrastaban con el de sema (`sema/types.ts` ·
`arts/motion/types.ts`) apuntan ahora al del art.
El análisis de abajo se conserva porque es evidencia válida PARA esa sesión:
- [`PLAN-sound-engine.md`](./PLAN-sound-engine.md) está **CERRADO** (2026-07-30) y
su columna «sema conserva» miente en 4 de 5 celdas: `SOUND_LIBRARY` no existe,
`SOUND_TUNINGS` se retiró el 2026-08-06 (con guard que lo mantiene retirado),
el sonido salió de la cascada y «qué firma para qué familia × intent» lo
sustituyó la ley del nombre. Es un inventario, no un mandato.
- [`PLAN-audio-player.md`](./PLAN-audio-player.md) está **⛔ GATE RECHAZADO —
SUSPENDIDO**, y su cabecera dice _«nada de lo de abajo se implementa»_. Su ✅
CONFIRMADO es una medición del terreno del 2026-07-31, cierta entonces.
- La fuente que gobierna no es un plan de proceso sino
[`book-deviations.md`](../decisions/book-deviations.md), de **decisiones**, que
el 2026-08-06 escribió en pasado: _«un gesto continuo suena por REPETICIÓN … y
**los tres resolvers se borraron con él**»_. `git log -S` sobre `sounds.ts`
devuelve **un solo commit en toda su vida** (mayo): nunca se borraron. El
registro dio por ejecutado lo que no se ejecutó.
Alcance medido de la retirada (por si esa sesión la retoma): los tres resolvers,
`DragSoundParams`, el `SoundContour` de sema, sus 5 re-exports, 2 tests que sólo
se probaban a sí mismos y los helpers `clamp`/`lerp` huérfanos — 81 líneas, 7
símbolos; el registro (`SEMA_SOUND_VOICE`, `sample`, `resolveSoundDefinition`…)
no se toca. La nota fechada que se añadió al §4 del plan cerrado también se
revirtió con el resto.
**Cae con ellos** el homónimo `SoundContour`: el 2026-08-11 se renombró
`SoundDirection` → `SoundContour` en
[`sema/sounds.ts:34`](../../src/uix/sema/sounds.ts) sin mirar que
[`arts/sound/types.ts:44`](../../src/arts/sound/types.ts) ya exportaba ese nombre
con cinco valores en vez de dos. Cinco sitios a tocar; el nombre no puede volver
a `SoundDirection` (el rename se hizo porque tres cosas distintas se llamaban
`direction`). Propuesto: `GestureContour`, que es la palabra que usa la propia
tabla de corte («los 3 resolvedores de **gesto**»). Si los resolvers se retiran,
el tipo se va con ellos y no hay nada que renombrar.
⚠️ Esta fila nació de un error de método: se propuso **retirar** los tres
resolvers por «cero llamadores», con dos de esos cinco sitios en la salida del
propio grep y sin abrirlos. «No lo referencia nadie» es una observación, nunca
un veredicto (regla 4).
---
## Cola menor · comentarios fósiles
~11 comentarios en código que citan nombres muertos, sin defecto de
comportamiento detrás: color-picker, combobox, nav-tree («stays sema-free» con
dos eventos `emerge` declarados) y aura, que cita el patrón
`close-save`/`close-cancel` que Dialog ya no usa. Sin `id` propio: se limpian
cuando se toque el fichero por otra razón.
### D13 · Siete eventos declarados que nadie emite — `CONFIRMADO` · P1
Salió al cerrar D9: es la CLASE, y la clase ya produjo D1 (AlertDialog mudo), D3
(`commit-reset` en cinco pickers), D4 (ColorPicker) y el propio D9.
`book-deviations.md` tipifica el defecto hermano —declarar una firma de canal que
la activación no incluye— como _«mecánicamente comprobable y merece guard»_. Esto
es la misma forma un piso más arriba.
Hay guard desde el 2026-08-12 (`contracts.test.ts`, «guards declared events
against the ones a provider actually emits»), con los siete en `INERT_EVENT_DEBT`.
⚠️ **Corrección 2026-08-12 (misma jornada): esto NO es «la cola», y el censo no
es nuevo.** [`CONTINUE-sema-audit.md`](./CONTINUE-sema-audit.md) §3.1 ya lo había
medido — _«eventos huérfanos: 14 de 249 en 104 morfos con soma»_— y, sobre todo,
ya los había **clasificado en tres clases que yo metí en un saco**. Verificado
componente a componente, la clasificación de aquella auditoría es la correcta:
| Clase | Cuáles | Quién la cierra |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Contrato muerto** — el único defecto de la clase D1/D3/D4 | `tooltip` ×3 | libre; es el que queda accionable aquí |
| **Punto de extensión documentado** — lo emite la APP, no el componente | `virtual-list.handle-scroll` · `virtual-grid.handle-scroll-row/column` | el morfo **no tiene forma de declararlo**: la auditoría propone un `emission: 'app'`. Es vocabulario de morfo, no cableado |
| **Verbo delegado** — la transacción vive en un componente compuesto | `gradient-picker.commit-reset` (S-14, **1 de 6 casos idénticos**) | `CONTINUE-sema-audit.md`. No se arregla como D3: `GradientPickerProvider` **no tiene `clear()`** — su docblock dice que open/commit/cancel/clear viven en el `PickerProvider` genérico compuesto, y esta clase «sólo posee el runtime del morfo». No hay dónde enganchar; o se retira la declaración, o el externo re-emite |
Lo que este guard añade sobre aquel censo no es el hallazgo, es que **deja de ser
una medición de una vez**: bloquea la regresión. Eso lo demostró D9 al deshacerlo.
**El accionable: `tooltip`, 3 de 3.** Cero llamadas a `trigger(` en todo
`soma/components/tooltip` — abre y cierra **sin firma alguna**, en uno de los
componentes más ubicuos del catálogo. `CONTINUE-sema-audit.md` lo marca como
«HALLAZGO NUEVO, no está entre los 44» y lo tipifica como _contrato muerto_: o el
provider emite los tres, o las tres declaraciones se retiran. La pregunta previa
es cuál de las dos, porque el tooltip **compone Popover** y podría ser otro
`expression: 'delegated'` como los pickers — eso hay que medirlo antes de tocar,
no suponerlo.
⚠️ Nota de método: la prueba **no es** `trigger('literal')`. Chronos despacha por
variable (`emitOnChip`) y un grep literal dio dos falsos negativos durante la
auditoría. Lo que se comprueba es más débil y robusto: que el nombre aparezca
como literal en algún sitio que el provider alcance — su directorio soma **más
las librerías compartidas**. Sin `$libs/selection` en el ámbito, combobox y
select se leen como inertes y no lo son.
### D14 · El movimiento por teclado de chronos pierde el foco — `CONFIRMADO` · P1
Salió midiendo D9, y es anterior a él.
[`restoreChipFocus`](../../src/uix/soma/components/chronos/chronos-provider.svelte.ts)
reenfoca el chip en un `queueMicrotask`, que corre **antes** de que Svelte reponga
el nodo: consulta un elemento que ya no está, `dom.focus(null)`, y el foco cae al
`<body>`.
Medido: se enfoca un chip, se pulsa flecha → el agarre se activa (`data-dragging`)
y el foco pasa a `BODY`; la segunda y la tercera flecha **no llegan al
manejador**, y el Escape tampoco, así que el agarre queda colgado. La interacción
que el propio componente anuncia —_«Grabbed. Arrow keys move, Enter drops, Escape
cancels»_— muere al primer paso.
No se toca aquí: es un defecto de foco, no de firma, y merece su propia medición
(un `raf` o el `tick` de Svelte en lugar del microtask, verificado en navegador
real y no sólo headless).

Powered by TurnKey Linux.