--- title: Registro de desviaciones entre implementación y canon editorial type: decision-log audience: human + agent authority: authoritative registry — where the implementation deviates from the book canon, with per-entry status status: current source: migrated verbatim from src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md (2026-07-02, docs-book F7.6; kept in Spanish — it is a logbook of literal author decisions and proposed doctrinal text for the Spanish book) --- # Registro de desviaciones entre implementación y canon editorial > **Este documento no forma parte del libro.** Es un registro interno de decisiones de implementación. Su objetivo es separar qué pertenece al canon editorial («Diseñando lo que ocurre»), qué pertenece al canon del proyecto UIX, y qué queda como extensión local o candidato pendiente. > > **Principio rector**: solo entra al libro lo que mejora la teoría general. Lo demás puede vivir como extensión del proyecto. El libro debe conservar un núcleo estable; la implementación puede tener vocabulario más rico. > > **No todo lo que aparece en implementación debe volver al libro.** --- ## Clasificación de status | Status | Significado | | --------------------------- | -------------------------------------------------------------------------- | | **BOOK_CANON** | Ya debería pasar al libro como ampliación canónica. Aceptado por el autor. | | **PROJECT_CANON** | Canon válido del proyecto, pero no necesariamente del libro. | | **CANDIDATE** | Caso plausible, pero falta doctrina o uso real para canonizar. | | **LOCAL_EXTENSION** | Necesario para este proyecto, pero no escalable al libro. | | **DEPRECATED** | Se mantiene temporalmente, pero debería eliminarse. | | **ALIAS** | Nombre aceptado como comodidad técnica, mapea a otro verbo doctrinal. | | **IMPLEMENTATION_CONTRACT** | Existe en morfos/runtime, pero no necesariamente como doctrina editorial. | Cada entrada lleva su status. Las entradas con `BOOK_CANON` incluyen el texto doctrinal recomendado para el libro, en bloques citados. --- ## A. Verbos añadidos al canon de implementación Estos verbos están en `src/uix/sema/verbs.ts:SEMA_VERBS`. Su status doctrinal para el libro varía. ### A.1 `handle.scroll` - **Status**: **BOOK_CANON** - **Libro Cap 25 §1** lista 7 verbos: pick, carry, drop, drag, resize, rotate, reorder. `scroll` no aparece literalmente. - **Decisión del autor**: aceptar como ampliación de `handle`. Cap 25 §1 ("manipulación directa de un objeto") cubre la lectura: el usuario desplaza directamente un viewport mediante gesto continuo. - **Distinción doctrinal**: - Scroll gestual del usuario → `handle.scroll` - Scroll programático del sistema (scrollToIndex / scrollToCell / scrollIntoView) → `shift.navigate` - **Texto doctrinal para el libro**: > `handle.scroll` cubre los casos en los que el usuario desplaza directamente un viewport, lista, panel o superficie desplazable. No equivale a navegación programática: cuando el sistema mueve al usuario a una posición concreta sin control directo, el evento pertenece mejor a `shift.navigate`. ### A.2 `commit.acknowledge` - **Status**: **CANDIDATE** - **Libro Cap 23 §5** lista 12 verbos de commit. `acknowledge` no aparece. - **Decisión del autor**: NO pasar al libro todavía. Mantener en canon de implementación pendiente de casos fuertes. - **Solapamiento problemático**: con `commit.confirm`, `commit.cancel`, `commit.submit`, `signal.dismiss`. Si el usuario solo cierra un aviso, quizá no hay commit; quizá solo hay retirada de señal (`signal.notify + neutral → emerge.close`). - **Cuándo sí cambiar a BOOK_CANON**: si hay obligación explícita de reconocimiento (registrado en el sistema): - "He leído y entiendo esta advertencia" - "Entiendo que esta acción no se puede deshacer" - "Acepto las condiciones" ### A.3 `commit.remove` vs `commit.delete` vs `commit.unselect` - **Status de `remove`**: **BOOK_CANON** - **Libro Cap 23 §5** lista `delete` (destrucción consumada). `remove` no aparece. - **Decisión del autor**: distinción de tres verbos sobre operaciones de retirada/eliminación. - **Texto doctrinal para el libro**: > `remove` no significa destruir. Significa retirar un elemento de una colección, relación o conjunto operativo. Si el elemento deja de existir o deja de estar disponible, corresponde `delete`. Si solo deja de estar seleccionado, corresponde `unselect`. ### A.4 `commit.confirm` - **Status**: **CANDIDATE** - **Libro Cap 23 §5** lista `submit`, `complete`. `confirm` no aparece. - **Decisión del autor**: NO canonizar todavía. - **Problema**: "confirmar" muchas veces no es el resultado final, sino un paso previo. El botón dice "Confirmar" pero el evento real puede ser `delete`, `submit`, `apply`, `authorize` o `acknowledge`. - **Cuándo sí cambiar a BOOK_CANON**: si hay casos donde el resultado aplicado sea literalmente "confirmación registrada", no acción posterior (confirmar asistencia, confirmar lectura, confirmar email). ### A.5 `commit.set` - **Status**: **BOOK_CANON** - **Libro**: Cap 25 ejemplo slider menciona `commit.set + affirm` pero no figura en lista canónica de Cap 23. - **Decisión del autor**: formalizar en Cap 23 §5. - **Casos canónicos**: slider, sort, criterio de filtro, valor de un picker, tamaño de página, zoom, preferencia local, criterio de ordenación, valor numérico. - **Texto doctrinal para el libro**: > `commit.set` comunica que un valor, criterio o parámetro ha quedado aplicado. Diferente de `save` (persistir globalmente), `submit` (enviar formulario), `complete` (culminar flujo), `select` (elegir un item). ### A.6 `commit.apply` - **Status**: **BOOK_CANON** - **Libro Cap 29** (delegate) usa `commit.apply` en ejemplos. Cap 23 §5 no lo lista. - **Decisión del autor**: formalizar en Cap 23 §5. - **Texto doctrinal para el libro**: > `commit.apply` comunica que un conjunto de cambios o una operación se ha aplicado. Diferente de `save` (guardar) — apply puede aplicar sin persistir; save persiste. ### A.7 `commit.move` - **Status**: **BOOK_CANON** (con distinción de `reorder`) - **Decisión del autor**: aceptar si se distingue: - `commit.move` → un elemento cambia de lugar - `commit.reorder` → una colección cambia de orden - **Caso típico**: `handle.drop → commit.move + affirm` o `handle.drop → commit.reorder + affirm` según si lo que cambió fue la posición de UN item o el orden de la colección. ### A.8 `commit.upload` - **Status**: **CANDIDATE** - **Decisión del autor**: dudoso como commit. Subir un archivo es proceso (`sustain.uploading`) que termina en `commit.complete` (éxito) o `commit.fail` (error). Como verbo de resultado quizá es redundante con `commit.complete`. - **Mantener** como candidato hasta que haya caso donde "upload" sea el resultado final no reducible a complete/attach/add/apply. ### A.9 `commit.partial` - **Status**: **CANDIDATE / revisar** - **Decisión del autor**: probablemente NO es verbo. "Partial" suena a estado o resultado incompleto, no a acción. - **Alternativas mejores**: - `commit.complete` + variant `partial` - `commit.fail + risk` - `sustain.partial` (estado, no acción) - **A revisar antes de canonizar**. ### A.10 `commit.block` - **Status**: **CANDIDATE / revisar** - **Decisión del autor**: probablemente NO es verbo de commit. "Blocked" suele ser estado, no resultado aplicado por el usuario. - **Alternativas mejores**: - `signal.alert + threat` - `commit.fail + risk` - `sustain.blocked + risk` (estado) - **A revisar antes de canonizar**. ### A.11 `commit.unselect` - **Status**: **BOOK_CANON** - **Libro Cap 23 §5** lista `select` pero no `unselect`. Pasa el par natural al canon. - **Decisión del autor (transcrita literal)**: > Seleccionar y deseleccionar son resultados aplicados sobre el estado de selección de un elemento. Eso es `commit`, porque el resultado queda aplicado. > > - No es `remove`: no estás eliminando el item ni sacándolo de una colección funcional; solo estás cambiando su estado de selección. > - No es necesariamente `toggle`: `toggle` describe mejor el mecanismo binario o el control, pero no expresa tan bien el resultado semántico concreto. > > Regla final: > > - `select` / `unselect` → resultados sobre estado de selección > - `toggle` → inversión binaria genérica > - `remove` → retirada, eliminación o salida de colección - **Aplicado en**: calendar, combobox, grid-list, listbox, select, tag-group. --- ## B. Adopciones interpretativas (no literales en el libro) ### B.1 Cancelación de drag = `commit.cancel` - **Status**: **BOOK_CANON** (composición canónica) - **Libro Cap 25 §4** cubre fases pick → carry → drop. No aborda explícitamente Escape durante carry. - **Decisión del autor**: la cancelación de un drag es composición `handle + commit.cancel`. No hay drop, no hay resultado aplicado, la manipulación se aborta. - **Para el libro**: añadir al capítulo de handle como composición canónica. ### B.2 Scroll programático = `shift.navigate` - **Status**: **BOOK_CANON** (con matiz) - **Libro Cap 27 §5** lista `shift.navigate` con ejemplos a escala "Lista → detalle. Página A → página B". - **Decisión del autor**: aclarar que `shift.navigate` puede operar a varias escalas: - entre páginas - entre vistas - entre pasos - dentro de una lista (programáticamente) - hasta una celda - hasta un índice - **Distinción**: usuario desplaza manualmente → `handle.scroll`; sistema mueve viewport → `shift.navigate`. **No** crear `shift.scrollto` todavía. ### B.3 Cambio de tamaño del modelo de datos - **Status**: **LOCAL_EXTENSION / CANDIDATE** - **Decisión del autor**: NO regla general del libro todavía. Caso por caso. - **Regla doctrinal aplicable** (ya en el libro Cap 4 §1, recordatorio): > Un cambio interno de datos solo se convierte en evento cuando se vuelve perceptible o cambia lo que el usuario puede hacer, debe atender o necesita interpretar. - **Si el cambio merece evento**, el verbo correcto depende del caso: - `signal.notify + neutral` — si avisa de nuevos items - `emerge.reveal` — si aparecen elementos - `commit.set + neutral` — si el sistema aplica un nuevo valor operativo visible (tamaño de dataset, criterio) ### B.4 Sort de tabla = `commit.set` - **Status**: **BOOK_CANON** (como ejemplo en commit.set) - **Decisión del autor**: ordenar por columna no es reordenar manualmente items; es fijar un criterio. - **Distinción**: - sort by criterion → `commit.set` - manual reorder → `handle.reorder` → `commit.reorder` - **Para el libro**: añadir como ejemplo de `commit.set` cuando se introduzca ese verbo. ### B.5 Eventos no perceptibles no son eventos - **Status**: **BOOK_CANON** (regla doctrinal) - **Libro Cap 4 §1** ya tiene la regla pero conviene reforzar. - **Texto doctrinal para el libro** (añadir como nota explícita en Cap 4): > No todo cambio de estado necesita evento. Un estado puede volver automáticamente a su forma base sin producir un evento semántico si el usuario no necesita interpretarlo como cambio relevante. - **Ejemplo de aplicación**: timer interno que revierte `copied=false` tras N ms en clipboard. Es estado (`data-copied`), no evento sema. ### B.6 Toggle con dos eventos direccionales - **Status**: **BOOK_CANON** (posibilidad direccional, NO canonizar intents) - **Libro Cap 22 §10** ejemplo: "Toggle: contact.press → commit.toggle + affirm". UN evento. - **Decisión del autor**: aceptar la POSIBILIDAD de dos eventos direccionales, pero NO fijar intents por defecto. - **Texto doctrinal para el libro**: > Un toggle puede modelarse como un solo evento de inversión o como dos eventos direccionales si la diferencia entre activar y desactivar importa para el usuario. El intent de cada dirección depende del contexto. - **Ejemplos del autor**: - Activar notificaciones: check → affirm, uncheck → neutral - Desactivar tracking: uncheck → affirm - Desmarcar consentimiento obligatorio: uncheck → risk - **Nota para la implementación**: el checkbox actual del proyecto fija `check=affirm` / `uncheck=neutral` como defaults razonables; los consumidores pueden override per-instance. ### B.7 Intent en `contact` como anticipación visual únicamente - **Status**: **BOOK_CANON** (regla de buena práctica) - **Libro Cap 22 §11** ya tiene la doctrina ("el intent fuerte no debería vivir en el contacto"). - **Decisión del autor**: el evento `contact.*` no debe cargar intent fuerte; el `intent` prop del componente puede afectar visualmente (data-color, forma) pero el evento sema se mantiene neutro. - **Aplicado en**: Button (`contact.activate` sin intent en el evento; `intent` prop drives data-color). - **Conecta con D.3**: la family policy actual `'allowed'` no comunica "discouraged" — requiere refinamiento. --- ## C. Cluster decisions ### C.1 `clear` field — RESUELTO - **Status**: **BOOK_CANON** (mapeo a verbo existente) - **Decisión del autor**: `commit.reset`. 10 morfos actualizados. - **Para el libro**: añadir como ejemplo de `commit.reset` cuando se introduzca `commit.set`/`commit.reset` al libro. ### C.2 `unselect` — RESUELTO - Ver A.11 — promovido a verbo BOOK_CANON. ### C.3.a Renames de forma (mecánicos) - **Status**: PROJECT_CANON (no requieren cambio en el libro) - 11 events renombrados a `{family}-{verb}-{variant}` para alinearse con la convención de naming: - `carousel.shift-slide` → `shift-navigate-slide` - `feed.shift-focus-item` → `shift-navigate-focus-item` - `feed.commit-load-more` → `commit-submit-load-more` - `file-upload.commit-add` → `commit-set-add` - `file-upload.signal-reject` → `signal-warn-reject` - `form.signal-invalid` → `signal-warn-invalid` - `number-field.handle-scrub` → `handle-drag-scrub` - `range-calendar.commit-start` → `commit-select-start` - `range-calendar.commit-range` → `commit-select-range` - `tags-input.commit-add` → `commit-set-add` - `tags-input.signal-reject` → `signal-warn-reject` ### C.3.b Correcciones doctrinales - **Status**: BOOK_CANON (precedent Button) - `command.commit-invoke` (declared `submit + fulfill`) → `commit-submit-invoke + submit + affirm`. Cambio de intent (`fulfill` → `affirm`) por Cap 22 §8 (celebrate-before-time es antipatrón). Mismo precedent que Button. --- ## D. Decisiones arquitecturales ### D.1 Cada actor declara sus eventos - **Status**: **BOOK_CANON** - **Texto doctrinal para el libro** (capítulo de composición o apéndice técnico): > Un componente no debe declarar eventos que no produce. En una composición, cada actor declara su parte del evento. - **Ejemplo**: un Button no declara `commit.delete + loss` si solo registra el click; declara `contact.activate`. El flujo, diálogo o acción que realmente elimina declara `commit.delete + loss`. Esto evita sobrecargar el botón y explica por qué la gramática necesita composición. ### D.2 Eventos del contrato que no se disparan - **Status**: **IMPLEMENTATION_CONTRACT** (no doctrina del libro) - **Decisión del autor**: doctrinalmente peligroso si se presenta mal (parece que el sistema promete eventos inexistentes). NO pasarlo al cuerpo del libro. - **Tratamiento**: apéndice técnico con metadata explícita: ```ts emission: 'runtime' | 'host' | 'external' | 'declared-only'; // o: implemented: true | false; ``` - **Acción del proyecto**: añadir flag `emission` al MorfoEvent type en una iteración futura para hacer explícito qué eventos se emiten en runtime vs cuáles son declarados sin emisor (contract surface para testing, analytics, accesibilidad externa). ### D.4 Campo `expression` en el morfo: cómo se rellena la firma sema - **Status**: **IMPLEMENTATION_CONTRACT** (no doctrina del libro) - **Decisión del autor (Lectura C)**: un morfo con eventos doctrinales declara su modo de expresión perceptual: ```ts type SemaExpressionMode = 'pack' | 'family-default' | 'delegated' | 'none'; ``` - **`pack`**: el morfo tiene un archivo `src/uix/sema/components/{kebab}.ts` con cascade rules específicas (sonido, háptica, prioridad). - **`family-default`**: el morfo descansa en `SEMA_MAP.families[family].base` sin tuning per-componente. Apropiado cuando el componente no necesita firma diferenciada. - **`delegated`**: el componente compone otros morfos que sí emiten (p. ej. picker emite via calendar/time-field/color-area que tienen sus propios eventos). - **`none`**: el morfo declara contrato pero no participa en sema runtime (reservado para utilidades no perceptuales). - **Lint**: `npm run morfo:vocabulary` valida que todo morfo con `events.length > 0` cumpla: 1. `scope` incluye `'sema'` (FAIL). 2. Existe un pack en `src/uix/sema/components/{kebab}.ts` **o** `expression !== undefined` (WARN si falla). - **Cuándo crear pack vs `family-default`** (criterios para D.4): - **Crear pack** si: eventos de alta frecuencia + riesgo de fatiga (toggles, form controls), o el componente necesita una firma sobria distinta del family base, o introduce cascades con prioridad (a11y override). - **`family-default`** si: el family base ya es la firma correcta y el componente no compite con otros del mismo family por intensidad perceptual. ### D.5 Packs para componentes de alta frecuencia (toggles) - **Status**: **PROJECT_CANON** (decisión arquitectural sin necesidad de pasar al libro) - **Caso**: `switch`, `toggle`, `toggle-group` emiten `commit-toggle` en bucle (settings panels, toolbars, segmented controls). Sin tuning, heredan el `family.commit.base.gain = 0.3` que es demasiado para una sesión sostenida. - **Solución (2026-05-26, DEROGADA — ver la nota del 2026-08-06 al final)**: tuning `form.toggle.silent = { gain: { op: 'add', value: -0.3 } }` en `src/uix/sema/sounds.ts`. Cancelaba exactamente el `gain` del family base, dejando el default en 0 (silencio). Los intent.deltas que añaden gain (`threat: +0.1`, `fulfill: +0.05`) seguían surgiendo, así un toggle destructivo sí emitía señal audible. - **Haptic**: tap leve (`intensity: 0.3, duration: 12, delay: 0`) sustituye el tap medio del family. Replaza el `kind` para que la háptica no oscile con el intent — la carga evaluativa de un toggle se lee en `data-color` + (selectivamente) sonido, no en háptica fluctuante. - **Doctrina**: el libro habla de "componentes de baja intensidad" (cap. 22) — esta es la materialización runtime. NO ir al libro: es decisión de tuning, no de gramática. - **Packs concretos**: `src/uix/sema/components/{switch,toggle,toggle-group}.ts` — los tres comparten la misma firma porque comparten rol UX (un press → un flip). - **REVERTIDO 2026-08-05 — los toggles recuperan voz (directiva de autor)**: el silencio de arriba se retira para `switch`, `toggle` y `toggle-group`, que pasan a **`commit.medium` (gain 0.1)** — audible, y un tercio de una pulsación de botón (`contact` base 0.25) para que un flip nunca pese más que una activación. Los deltas de intent siguen montando encima (`threat` +0.1, `fulfill` +0.05). Razón: un silencio por defecto se percibe como componente roto, no como sobriedad; la fatiga se combate bajando el nivel, no anulándolo. El háptico ligero de abajo NO cambia. Se conserva el silencio, con su justificación intacta, sólo donde el silencio ES la firma: `tooltip` (revela al pasar el ratón — sonaría en cada cruce) y `media-player` (su propia salida ES audio, D-AP2.7). Además, **emitir silencio dejó de costar**: `SoundChannel.handle` corta antes de sintetizar cuando la ganancia resuelta es ≤ 0 (`chans/sound.ts`), así que un `{family}.silent` ya no levanta dos osciladores, un filtro y una envolvente para no sonar. Fijado por test, verificado en rojo. - **Renombrado 2026-08-05 — el prefijo `form.` muere**: este tuning nació como `form.toggle.silent` y pasó a llamarse `commit.silent` — una clave que vivió UN DÍA: la nota siguiente la retira del catálogo entera. El prefijo `form.` entró el 2026-05-19 (`09e261878`) nombrando con verdad a siete controles de formulario, pero ESTE MISMO commit (`2d562f378`) lo extendió a menús y árboles vía `form.commit.subtle` — el D.6 de abajo llegó a escribir el nombre correcto (`commit.select`) en la misma línea que usaba la clave incorrecta. Llegó a 50 packs, de los que exactamente uno era el componente Form. La ley queda escrita en [`sema.md` § Tuning naming shapes](../architecture/sema.md#tuning-naming-shapes) — **el primer segmento es la familia cuya base modifica el tuning, nunca un componente** — y la vigila `src/uix/sema/sounds-grammar.test.ts`. Renombres del mismo barrido: `form.commit.soft` → `commit.soft`, `form.commit.subtle` → `commit.subtle`, `tooltip.silent` → `emerge.silent` (ambas retiradas al día siguiente), `tabs.select.soft` → `commit.select.soft`. Sin cambio de comportamiento: mismos deltas, mismas ganancias. **Claves vivas hoy: sólo `commit.soft`, `commit.subtle`, `commit.select.soft`, `commit.medium` y las `emerge.*`** — el enumerado generado está en [`canon/vocabularies.md`](../canon/vocabularies.md#sound-tunings). - **EL SILENCIO DEJA DE SER ARITMÉTICA — 2026-08-06 (directiva de autor)**: «el silencio no admite verbos ni intents, es universal, por lo tanto es un valor canónico que cuando se le envía al canal semántico éste simplemente lo ignora y no envía nada al soundengine». Las tres claves silenciadoras (`commit.silent`, `emerge.silent`, `contact.silent`) **desaparecen del catálogo**; en su lugar hay **`SILENT`**, un único valor exportado por `$uix/sema` que se declara en el slice del canal y que el resolver honra **retirando ese canal** de la firma — la forma que el propio motor ya prescribía («muting DROPS the channel rather than scaling to zero, because a `0` still buzzes»). Un delta de intent ya no puede resucitarlo, porque no queda ningún número que mover. MOTIVO MEDIDO, y desmiente la promesa de la «Solución» de arriba: el cero no era silencio. Los deltas de intent suman **rugosidad** (`risk` +0.2, `threat` +0.4), lo que cruza el umbral del AM del motor, y como el modulador se conecta al `AudioParam` de la envolvente —y conectar a un `AudioParam` SUMA, no multiplica— el trémolo pasaba a ser la señal entera. Renderizado offline con el grafo real: `risk` **−13,9 dBFS** y `threat` **−6,7 dBFS** desde una firma declarada a `gain 0`, frente a **−9,3 dBFS** de un botón normal. El «silencio» era lo más fuerte de la interfaz, y lo que sonaba era un zumbido sin ataque ni caída cortado en seco. Fijado por tests en `resolver.test.ts` (universal para toda familia e intent, y liftable sólo por REEMPLAZO) y por `sounds-grammar.test.ts`, que ahora prohíbe tanto una clave `*.silent` como cualquier afinado que deje su familia en `gain <= 0`. ### D.6 Packs para superficies de menú y árboles - **Status**: **PROJECT_CANON** - **Caso**: `menubar`, `navigation-menu`, `context-menu`, `dropdown-menu`, `tree-view`, `tree-grid` — superficies navegacionales de alta frecuencia. El usuario abre/cierra menús y expande/contrae nodos decenas de veces por sesión. Sin tuning, heredan `family.emerge.base.gain = 0.2` y `family.commit.base.gain = 0.3` — demasiado prominente. - **Solución**: packs por componente que combinan tuning emerge soft + commit subtle: - **emerge.open (menús, expand)**: `emerge.soft` (gain 0.08) — el contenido se revela sin competir con la superficie que lo invoca. - **emerge.close (menús, collapse)**: `emerge.exit.soft` (gain 0.05, descending) — disciplina de dirección compartida con dialog/drawer/popover. - **commit.select (item de menú, nodo de árbol)**: `commit.subtle` (gain 0.03) + `tap` leve. Más sobrio que radio-group porque la cascada típica es "menú cierra + item commit + nueva superficie aparece" — tres señales en milisegundos, hay que repartir intensidad. - **Coherencia**: dropdown-menu y context-menu comparten firma idéntica (el usuario no debe aprender dos "sonidos de menú"). Tree-view y tree-grid idem. - **ENMIENDA 2026-08-19 — `navigation-menu` sale del alcance de esta entrada.** Su `Link` ya no emite `commit-select`: navegar no fija nada, y un `commit` ahí es el antipatrón «Success de navegación» del libro (TABLA 9.3), reforzado por cap. 27 §2 («shift no es commit») y por la apertura de cap. 10 («una navegación no es exitosa por cambiar de pantalla»). El gesto habla ahora el par de cap. 22 §9 — `contact-activate` en el enlace y `shift-navigate` en el `