# 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: `