# 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 `
` 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 `
` — `ARREGLADO` (2026-08-12)
El morfo declara `defaultElement: 'label'` y el componente renderizaba `
`.
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: `