39 KiB
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
- Se escribe por
id.D1…D14no se renumeran nunca. Un defecto que resulta ser dos se parte enD3a/D3b, no desplaza a los siguientes. - Un
ARREGLADOlleva su commit. Sin sha no es un arreglo, es una opinión. - 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.
- «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 —D10nació 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í.
- «
mode='modal'promete bloquear Escape» — la promesa existía sólo enpicker-shell-handle.svelte.ts:32y en un comentario dedate-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): Escape descarta en AMBOS modos porque el morfo declara el patrón APGcomboboxy 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. - «No hay buffer» — sí lo hay:
date-picker-provider.svelte.ts:153capturavalueOnOpenen la arista de apertura ycancel()(: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)
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 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 F-2 |
«Sin Home/End (APG spinbutton) — pass de familia de segmentos (con date/color)» |
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 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,
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, «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 esdiv+role='button'. Lo que faltaba era el puente: el «tabindexpropiedad de soma» que ese comentario promete no existía. Ahora el provider emitetabindex: 0y unonkeydownque convierte Enter / Espacio en un clic (conpreventDefaultsobre el espacio, que si no desplaza la página), y el componente puede ser eldivque su contrato declara.tooltip.trigger— probé el morfo y me equivoqué; la medición lo corrigió. Declarababutton, 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; declaradiv, y la a11y la dan eltabindex: 0del provider y elaria-describedbydel morfo, que es lo que el patrón APG pide.stepper.item— undivsin rol dentro de unrole='tablist'. El patrón eslist (tablist) > item > trigger (tab), y un elemento genérico entre un tablist y sus tabs rompe la relación de posesión. Esrole='presentation'ahora — el envoltorio sale del árbol, igual que un<li>dentro de unrole='menu'; el idioma ya estaba en 8 componentes del catálogo.color-picker.channel-input— declarabainputmientras componeColorField.Input, que esdiv+role='group'(el contenedor de segmentos, la misma forma que elinputde date-field). Ninguna composición podía satisfacer esa declaración: las piezas editables son las partes hermanaschannel-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 deslider(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/.branchdeclaranliy rindendiv,tree-view.branch-contentdeclaraul,stepper.listdeclaraol,file-upload.file-listdeclaraul,feed.articledeclaraarticle,tooltip.triggerdeclarabutton. Rendirlos comodivpierde 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
y link-preview
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) 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). 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 |
«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 |
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 |
§4, los tres en la columna «sema conserva» de la LÍNEA DE CORTE |
PLAN-audio-player.md:367 |
D-AP.7, ✅ CONFIRMADO: el slider adjunta el resolver en cada handle-drag |
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 — 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.mdestá CERRADO (2026-07-30) y su columna «sema conserva» miente en 4 de 5 celdas:SOUND_LIBRARYno existe,SOUND_TUNINGSse 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.mdestá ⛔ 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, 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 -Ssobresounds.tsdevuelve 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 sin mirar que
arts/sound/types.ts:44 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 §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
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).