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)
- **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:
- **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.
- **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.
- **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:
-`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:
- **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).
- **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. Menubar y navigation-menu se quedan en solo `commit.subtle` porque no tienen evento de open/close declarado en el morfo (la apertura es de un dropdown adyacente).
> "El sonido canónico de la gramática debe ser modulable por familia, intent, frecuencia e intensidad.
>
> Los samples no deben sustituir la firma semántica base cuando esa sustitución impide la modulación por intent.
>
> Los samples pueden existir como recursos de producto, tema o branding, pero no forman parte del canon semántico por defecto.
>
> En eventos frecuentes, la prioridad es evitar fatiga. El silencio es una firma válida.
>
> Los packs solo deben crearse cuando corrigen una diferencia perceptiva real: frecuencia, fatiga, incongruencia, accesibilidad, patrón recurrente o necesidad de diferenciación."
- **Regla práctica**: si un pack solo selecciona un tuning existente y no evita un problema real, no se crea.
- **Decisión arquitectural inmediata**:
-`SOUND_LIBRARY` (samples + synth concretos) = **recursos**. NO se usa en packs canónicos.
-`SOUND_TUNINGS` (deltas paramétricas sobre family base) = **canon semántico**. Esta es la única capa que se usa en packs por defecto.
- Los packs componen tunings, nunca samples directos. Esto preserva `intent.deltas` (capa 2) que es lo que da diferenciación perceptual al sistema.
- **Excepción aceptable**: family `signal` (alarm / notify / announce) admite samples como replacement porque (a) tienen marca cultural prescriptiva (error wav, ping, ding), (b) la intent-variability es efectivamente nula en ese family. Si emerge un caso, se documenta explícitamente.
- **Lo que NO se hace**:
- No hay `sampleOverlay` (sample como capa adicional sobre synth). Sobreingeniería: añade mixing en WebAudio, layer de resolver, knobs extra al diseñador, y los casos donde aportaría son raros. Descartado permanentemente, no como pendiente.
- No se canonizan samples en packs de `commit`, `emerge`, `contact`, `handle`, `shift`, `sustain`, `delegate`. Los packs viven de tunings.
### D.3 Family policy: separar requirement de guidance
- **Status**: **BOOK_CANON** (concepto) + cambio inmediato en el proyecto
- **Decisión del autor**: la policy actual `'allowed' | 'expected' | 'optional'` mezcla dos cosas: requisito de tipo y guía doctrinal. Separar:
- **Implementación**: aplicar en `src/uix/sema/types.ts:SEMA_FAMILY_POLICY`. TypeScript deriva `IntentExpectedFamily` desde `intentRequirement === 'required'`. `intentGuidance` queda como campo doctrinal de documentación + posible lint en el futuro.
- **Origen**: al revisar la sección D.7 emergió la tentación de promocionar ARIA a "canal a11y". Análisis honesto: era confusión categorial. Esta sección fija los límites para que nadie reincida.
**Canales runtime declarables en sema (built-in del framework):**
```
sound — SoundSignature (synth/sample, modulable por intent.deltas)
haptic — HapticSignature (vibration, modulable por intent.deltas)
```
**Lo que NO es canal (y no debe convertirse en canal):**
| Cosa | Dónde vive | Razón |
|---|---|---|
| ARIA estructural (`aria-label`, `aria-expanded`, `role`, ...) | `Morfo.parts[].aria` + `.role` | Declarativo. Resuelto desde props/states. Promocionarlo a canal sería convertir lo declarativo en post-hoc DOM manipulation. |
| ARIA dinámico (live regions `aria-live`) | Soma escribe directo en el live region DOM | Sólo un morfo lo necesita (`Announce`). Hacer canal añadiría engine surface sin caso plural. |
| Visual — motion (animaciones, transiciones) | Eidos CSS `@keyframes` reaccionando a `data-event-*` stampeado por `VisualChannel` | El stamp es la única responsabilidad de sema; el output visual es CSS. |
**Regla operativa**: algo es canal sólo si cumple las tres:
```
(a) recibe SemanticSignal y emite output perceptual,
(b) acepta modulación por intent.deltas (perceptual loading),
(c) tiene signature paramétrica análoga a SoundSignature / HapticSignature.
Si falla (b), no es canal — es declarativo o ad-hoc.
```
ARIA dinámico falla (b): `signal-announce + threat` produce el MISMO texto + el MISMO ARIA. La carga evaluativa del intent vive en sound + visual, no en el texto del anuncio. Por eso no es canal.
**Channels extensibles (no built-in, opt-in por la app):**
El registry `SemaChannelSignatures` es OPEN vía declaration merging. Una app puede:
a11y: { ariaPayload: string; politeness: 'polite' | 'assertive' }; // si la app lo quiere formal
}
}
```
E implementar un `Channel` con `prepare(signal, target)` + `play(effective)`. El framework no envía ninguno de éstos — son extensión de producto.
**Pendiente sin urgencia**: si en el futuro `Announce` necesita ser pluggable (apps que quieran enviar a logger, telemetría, voice UI), entonces vale convertirlo en canal formal. Hoy no.
---
### D.9 Persistence: separar hold expresivo de lifecycle de la señal
- **Status**: **PROJECT_CANON** (codifica libro Cap 24 §6.1 — primera implementación operativa)
- **Origen**: libro Cap 24 §6.1 distingue `hold` (duración mínima perceptible) de `persistence` (cuánto tiempo dura realmente la señal). La implementación trataba todo como transient con un hold numérico — regresión: un `signal.warn + risk` de validación desaparecía a los 240 ms aunque el formulario siguiera inválido.
-`EngineSemantic.emit()` ahora devuelve el `id` del signal. Para `persistence !== 'transient'` mantiene la proyección viva pasado el hold; el caller posee el cleanup vía `engine.clear(id)` o `engine.clearTarget(target)`.
-`SomaRuntime.trigger()` devuelve `TriggerResult { id?, persistence? }`. Expone `runtime.clearSignal(id)` y `runtime.clearTarget(target)` para que providers cierren el ciclo.
- Default conservador: cuando un morfo NO declara `persistence`, el runtime asume `'transient'`. La tabla canónica es REFERENCIA — los autores la declaran explícitamente en cada morfo. No se aplica de oficio para no introducir regresiones silenciosas.
**Morfos actualizados** (consumidores reales):
| Morfo / evento | persistence | Por qué |
|---|---|---|
| `announce.signal-alert` | `untilAction` | El banner crítico debe esperar gesto del usuario |
| `dialog.close-after-fail` | `transient` (explícito) | El dialog se desmonta; la persistencia del fallo vive en Toast/Announce externo |
| `drawer.close-after-fail` | `transient` (explícito) | Misma razón |
| `file-upload.signal-warn-reject` | `untilFix` | El archivo rechazado sigue presente hasta que el usuario lo quita |
| `form.signal-warn-invalid` | `untilFix` | El warning persiste hasta que la validación pase |
| `password-field.signal-notify-caps-state` | `stateBound` | El indicator vive mientras caps lock esté on |
**Providers cabledados** (form / file-upload / password-field): cada uno llama `runtime.clearTarget(provider)` antes de re-emitir, o sobre la transición a estado "fix aplicado". Ver providers de los tres componentes.
**Texto doctrinal para el libro**:
> Cada señal perceptiva tiene dos duraciones independientes: un `hold` que es la duración mínima necesaria para que el usuario la registre como evento, y una `persistence` que dice cuánto tiempo permanece visible una vez registrada. Para la mayoría de eventos coinciden: la señal aparece, dura `hold` ms, y desaparece. Pero para `signal.warn + risk` y `signal.alert + threat`, la duración real depende del estado del sistema o de la acción del usuario, no de un cronómetro: una advertencia de validación debe seguir visible mientras el problema exista, y una alerta crítica debe seguir visible hasta que el usuario reconozca la situación.
---
### D.10 Accesibilidad semántica por evento (`a11ySemantic`)
- **Status**: **PROJECT_CANON** (codifica libro Cap 24 §9 — primera implementación operativa)
- **Origen**: el libro §9 define un contrato a11y por evento (live region, focus move, persistent trace, reduced-motion fallback). La implementación lo respetaba ad-hoc en cada provider. Ahora se declara en el morfo y el runtime lo honra centralizadamente.
-`ActiveDom.prefersReducedMotion`: tracker reactivo del media query, vive en `src/arts/adom/reduced-motion.svelte.ts`. SSR-safe (devuelve `false` cuando no hay `matchMedia`).
-`ActiveUix.announce(message, priority?, timeout?)`: live region compartida lazy-creada en document body. NO depende de soma — usa `dom.writeNode` directo. Lower-level que `<Announce>` soma (que ofrece snippet props y A/B alternation para repetidos).
-`SomaRuntime.trigger` honra `a11ySemantic` después del emit:
-`requiresLiveRegion` ∧ `opts.message` ∧ `sources.announce` → llama `announce(message, priority)` donde la prioridad se deriva de family/intent (`signal + threat/loss → assertive`, resto polite).
**Mensaje del live region**: el caller pasa `opts.message` en `runtime.trigger`. El runtime NO infiere texto del nombre del evento — los nombres son técnicos (`signal-warn-invalid`), no user-facing. Esto deja el control de fraseo en el provider (que sabe en qué idioma y con qué contexto).
**`requiresPersistentTrace` no es ejecutable por el runtime**: declara un contrato que el provider/app debe cumplir mostrando un afford persistente (banner, inline error, undo toast). Es una nota declarativa que lint/docs/audit pueden chequear, pero no algo que el runtime pueda forzar — el "trace" vive en código de aplicación.
---
### D.11 Eventos polimórficos (`allowedFamilies`)
- **Status**: **PROJECT_CANON** (codifica libro Cap 5 §3 — implementación operativa diferida)
- **Origen**: libro §5.3 documenta que un morfo puede declarar CAPACIDAD para varios shapes semánticos en lugar de comprometerse a uno. Hasta ahora la implementación sólo soportaba shape fijo. No había consumidor concreto, pero el tipo + runtime quedan disponibles para futuras decisiones (Dialog `close` con/sin cambios sin guardar, etc.).
**Shape**: ADITIVO sobre el shape concreto, no variante separada. El morfo declara su `family` + `intent` + `verb` como default; añade `allowedFamilies` para autorizar overrides:
- Backwards-compat: los morfos existentes —29 en aquel momento— acceden a `event.semantic.family`/`intent`/`verb`/`sequence`. Una variante separada `{ defaultSemantic: { family: ... } }` rompía cientos de demos que hacen visualizaciones de la morfo en tablas.
- Equivalencia funcional: declarar `family: 'shift', verb: 'exit-mode'` Y `allowedFamilies: ['shift', 'commit', 'emerge']` cumple la misma intención que el shape del libro con menos anidamiento.
- Cero overhead para no-polymorphic: morfos sin `allowedFamilies` no pagan ningún coste de tipos ni runtime.
**Texto doctrinal para el libro** (si se canoniza):
> Un evento puede declarar no sólo qué es, sino qué podría ser. La forma canónica (`family`, `verb`, `intent`) describe el caso por defecto, pero un campo opcional `allowedFamilies` puede enumerar las alternativas que el provider está autorizado a emitir según contexto. Por ejemplo, el `close` de un diálogo es típicamente `shift.exit-mode`, pero si hay cambios sin guardar puede convertirse en `commit.discard + loss`, o si simplemente se descarta sin acción, en `emerge.close`. El provider decide la family concreta en tiempo de ejecución; el morfo establece el catálogo de las shapes válidas.
**Aplicación real — Dialog (sprint 2026-05-27 / segunda mitad)**:
Dialog cabledaba 5 eventos `close-*` distintos (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail), cada uno con su propio prewrite de `data-last-action` y su semantic concreta. La refactorización los colapsó en UN evento polymorphic `close`:
El provider hace `dom.apply({ target, attrs: { 'data-last-action': cause.lastAction } })` antes de `runtime.trigger('close', { semantic: cause.semantic, ... })`.
**Cambios colaterales necesarios**:
- **Validador del morfo** (`src/uix/morfo/schema.ts`): se relajó el invariante "cada `data-last-action.values[]` debe ser prewritten por algún event". El otro sentido sigue estricto (un prewrite con valor fuera de `values[]` falla). Razón: con polymorphism, el provider escribe imperativamente — la sincronía bidireccional dejaba de tener sentido.
- **Cascade sema de Dialog** (`src/uix/sema/components/dialog.ts`): los selectores que matcheaban `eventName: 'close-dismiss-outside'` o `eventNamePrefix: 'close-'` se reescribieron para usar `eventName: 'close'` + matchers adicionales (`state: { attr: 'data-last-action', value: 'dismissed-outside' }` y `eventFamily: 'emerge'`). El helper `semaSelector` soporta el matcher `state` nativamente.
- **Eidos CSS** (`dialog.css`): NO requirió cambios — los selectores ya leen `data-last-action` para tintar la animación de salida, no los nombres de evento.
- **Tests del morfo/runtime**: actualizados para esperar `close` en lugar de `close-cancel`/etc. El test de prewrite del compiler se movió a `drawerMorfo` (que mantiene su shape per-event).
**Drawer y Popover**: refactorizados con el mismo patrón en sprint 2026-05-27 #3. Cada uno:
- 5 close-* events → 1 polymorphic `close` event en su morfo
- DISMISS_CAUSES + dismissWith adaptado en su provider
- Internal callsites (escape, outside-click, close button) migrados a `dismissWith`
- Tests actualizados
- Eidos CSS sin tocar (ya leía `data-last-action`)
**Picker family** (color-picker, date-picker, date-range-picker, time-picker, time-range-picker): refactorizados al patrón polymorphic close en sprint 2026-05-27 #4.
Detalle del refactor:
- Cada picker tenía 4–5 eventos `close-*` (close-commit / close-cancel / close-dismiss / close-dismiss-outside, + variantes como `close-range-commit` en date-range-picker), todos con prewrite individual de `data-last-action`.
- Colapsados a 1 evento `close` polymorphic con `family: 'emerge', allowedFamilies: ['emerge', 'commit', 'signal']`, sin prewrite.
- **Hallazgo**: los providers de los pickers NO disparan los close events vía `runtime.trigger`. Sólo togglean `opts.open = false`. Los eventos estaban declarados pero **inertes** — su único consumidor era el schema validator y el compiler tests. El refactor es alineación doctrinal, no de comportamiento.
- Sema cascade: solo `color-picker` tiene un sema pack y NO referenciaba close-* (sólo handle-*). Nada que actualizar.
**Test fixtures decoupling**: antes de tocar los pickers, los tests `compile.test.ts` + `runtime.svelte.test.ts` se migraron a un fixture sintético `prewriteFixtureMorfo` (en `src/uix/morfo/test-fixtures.ts`). Esto desacopla los tests de las decisiones del catálogo de componentes — los tests validan el contrato del compiler / runtime, no qué morfos lo usan.
**Resultado del rollout polymorphic completo**:
- **3 overlays** (Dialog / Drawer / Popover): polymorphic close cabledado al runtime (providers disparan vía `dismissWith`).
-`commit.confirm` (A.4) — solapa con submit/apply/acknowledge
-`commit.upload` (A.8) — probable redundancia con complete
-`commit.partial` (A.9) — probable estado, no verbo
-`commit.block` (A.10) — probable estado/señal, no commit
- Data-size auto-change (B.3) — caso por caso
### IMPLEMENTATION_CONTRACT (no doctrina)
- Eventos declarados pero no emitidos (D.2)
- Campo `expression` en el morfo (D.4)
- Packs sema soft-tuned para alta frecuencia (D.5, toggles)
- Packs sema para superficies de menú y árboles (D.6)
- Doctrina sonido canónico vs samples (D.7) + packs tooltip / collapsible
- Channel scope: qué es canal y qué no (D.8)
---
## F. Cómo proceder
Para cada entrada el autor decidió un status. La implementación:
1.**BOOK_CANON aceptados**: ya están en `SEMA_VERBS` del proyecto. La próxima edición del libro puede formalizarlos. Los textos doctrinales recomendados están en bloques citados arriba.
2.**CANDIDATE**: mantener en `SEMA_VERBS` pero no promover al libro hasta acumular casos.
3.**IMPLEMENTATION_CONTRACT**: requiere cambios en types (flag `emission`) en una iteración futura.
4.**Cambio inmediato pendiente en proyecto**: split de `intentPolicy` en `intentRequirement` + `intentGuidance` (D.3) — commit separado.
Hasta que el libro se actualice, este documento es la fuente de verdad sobre dónde el proyecto se ha desviado del libro literal y con qué status.
---
## G. Anti-mezclas
Este documento mezclaría planos peligrosamente si no se mantiene la disciplina de:
- **No copiar el contenido de este documento al libro tal cual.** El libro necesita gramática estable; este documento es bitácora.
- **No promover automáticamente `PROJECT_CANON` a `BOOK_CANON`.** Solo entra al libro lo que mejora la teoría general.
- **No usar lenguaje interno del proyecto** ("Capa 2", "Cluster 6", "Plan B commit X") en texto editorial.
- **Versionar el documento** cuando cambie un veredicto del autor.
Las familias son el núcleo estable. Los verbos son extensibles bajo criterios. La implementación puede tener aliases y extensiones locales. Solo las extensiones que revelan una diferencia recurrente y general deben pasar al libro.