You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/decisions/book-deviations.md

852 lines
52 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
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**: nuevo tuning `form.toggle.silent = { gain: { op: 'add', value: -0.3 } }` en `src/uix/sema/sounds.ts`. Cancela 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`) siguen surgiendo, así un toggle destructivo sí emite 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).
### 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)**: `form.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).
- **Caveat (`tree-view` / `tree-grid`) — RESUELTO (verificado 2026-07-12, SEM-4)**: la
emisión aterriza vía `fallbackTarget` en el elemento real (`branchEl` / `rowEl`) y los
packs construyen sus selectores con `onBranch` / `onRow` (`semaSelector(morfo,
'branch'|'row')`) — emisión y cascada casan de punta a punta.
- **Caveat (emisión soma) — RESUELTO (verificado 2026-07-12, SEM-4)**: los 6 providers
emiten vía `runtime.trigger` (dropdown/context: `open`/`close`/`commit-select`;
menubar/nav-menu: `commit-select`; trees: `emerge-expand`/`emerge-collapse` +
`commit-select`). Verificado en vivo: stamps `open · emerge · active` y
`commit-select · commit · affirm` en el navegador. Este párrafo quedó STALE varias
semanas y una auditoría clean-room (2026-07-10) lo citó como evidencia de dormancia —
lección: el registro se actualiza EN EL MISMO PASE que el cableado.
### D.7 Sonido canónico y samples
- **Status**: **PROJECT_CANON** (regla doctrinal sin necesidad de pasar al libro)
- **Doctrina (verbatim del autor)**:
> "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:
```ts
intentRequirement: 'required' | 'optional' | 'forbidden'
intentGuidance: 'expected' | 'contextual' | 'discouraged'
```
- **Policy propuesta**:
| Family | intentRequirement | intentGuidance |
|---|---|---|
| `contact` | optional | discouraged |
| `commit` | required | expected |
| `signal` | required | expected |
| `handle` | optional | contextual |
| `emerge` | optional | contextual |
| `shift` | optional | contextual |
| `sustain` | optional | contextual |
| `delegate` | optional | contextual |
- **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.
### D.8 Channels: qué es canal y qué no
- **Status**: **PROJECT_CANON** (regla arquitectural de límites)
- **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. |
| Visual — color/intent (data-color, data-event-intent overlays) | Eidos recipes + design tokens | Idem. |
| Visual — presence (z-index, opacity, layout) | Eidos CSS | Idem. |
**Channel activation por familia** (en `SEMA_MAP`):
```
contact sound + haptic
commit sound + haptic
signal sound + haptic
emerge sound (sin haptic — apariciones no son táctiles)
shift sound (sin haptic — cambio de marco)
handle haptic (sin sound — feedback gestual es táctil)
sustain ninguno (puramente visual)
delegate ninguno (puramente estructural / visual)
```
Los packs DEBEN respetar el `activeChannels` del family. Añadir `haptic` a un pack que opera sobre family `emerge` es incoherente; añadir `sound` a `handle` también. El resolver no lo bloquea, pero la doctrina sí.
**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:
```ts
declare module '$uix/sema' {
interface SemaChannelSignatures {
voice: { phrase: string; rate?: number; ... }; // TTS
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.
**Tipos** (`src/uix/sema/types.ts`):
```ts
export type SignalPersistence = 'transient' | 'untilAction' | 'untilFix' | 'stateBound';
```
| Valor | Lifecycle | Caso típico |
|---|---|---|
| `transient` | Engine auto-limpia tras `hold`. Default. | contact.press, commit.save, emerge.open |
| `untilAction` | Persiste hasta acción del usuario. | signal.alert + threat (banner crítico) |
| `untilFix` | Persiste hasta corrección. | signal.warn + risk (validación de campo) |
| `stateBound` | Lifecycle = duración del estado. | sustain.progress, caps-lock indicator |
**Tabla canónica `SEMA_HOLDS_BY_INTENT`** (`src/uix/sema/holds.ts`) — codifica el libro §6.2 separando los dos ejes:
```ts
{
contact: { _default: { hold: 'glimpse', persistence: 'transient' } },
emerge: { _default: { hold: 'brief', persistence: 'transient' } },
shift: { _default: { hold: 'noticed', persistence: 'transient' } },
commit: {
_default: { hold: 'brief', persistence: 'transient' },
fulfill: { hold: 'noticed', persistence: 'transient' }
},
signal: {
_default: { hold: 'brief', persistence: 'transient' },
risk: { hold: 'brief', persistence: 'untilFix' },
threat: { hold: 'brief', persistence: 'untilAction' },
loss: { hold: 'noticed', persistence: 'transient' }
},
handle: { _default: { hold: 'brief', persistence: 'transient' } },
sustain: { _default: { hold: 'noticed', persistence: 'stateBound' } },
delegate: { _default: { hold: 'noticed', persistence: 'transient' } }
}
```
**Implementación**:
- `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.
**Tipo** (`src/uix/morfo/types.ts`):
```ts
export interface MorfoA11ySemantic {
requiresPersistentTrace?: boolean; // app debe mostrar trace externo
requiresLiveRegion?: boolean; // runtime pushes message a aria-live
requiresFocusMove?: boolean; // runtime mueve foco al target
keyboardEquivalent?: boolean; // contrato — handle.* drag etc.
reducedMotionFallback?: 'state' | 'text' | 'focus' | 'none';
}
```
Y en `MorfoEvent`:
```ts
{ name: 'signal-warn-invalid', semantic: {...}, a11ySemantic: { requiresPersistentTrace: true, requiresFocusMove: true, reducedMotionFallback: 'text' } }
```
**Infraestructura nueva**:
- `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).
- `requiresFocusMove` ∧ target → `dom.focus(target)`.
- `reducedMotionFallback === 'state'` ∧ user prefers reduced → fuerza `channels: []` (silencia toda la señal perceptiva; sólo state attrs).
- `reducedMotionFallback === 'text'` ∧ user prefers reduced → llama announce aunque `requiresLiveRegion` no esté seteado.
- `reducedMotionFallback === 'focus'` ∧ user prefers reduced → focusea target aunque `requiresFocusMove` no esté seteado.
- `reducedMotionFallback === 'none'` → no hace nada (el motion era incidental).
**Morfos anotados** (mismos 6 consumidores que persistence):
| Morfo / evento | `a11ySemantic` |
|---|---|
| `announce.signal-alert` | `{ requiresPersistentTrace, requiresLiveRegion }` |
| `dialog.close-after-fail` | `{ requiresPersistentTrace, requiresLiveRegion, reducedMotionFallback: 'text' }` |
| `drawer.close-after-fail` | idem dialog |
| `form.signal-warn-invalid` | `{ requiresPersistentTrace, requiresFocusMove, reducedMotionFallback: 'text' }` |
| `file-upload.signal-warn-reject` | `{ requiresPersistentTrace, reducedMotionFallback: 'text' }` |
| `password-field.signal-notify-caps-state` | `{ requiresLiveRegion, reducedMotionFallback: 'text' }` |
**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:
```ts
{
name: 'close',
semantic: {
family: 'shift', // default
verb: 'exit-mode',
target: v.partRef('content'),
allowedFamilies: ['shift', 'commit', 'emerge'] // polymorphic capacity
}
}
```
Trigger:
```ts
runtime.trigger('close'); // emite { family: 'shift', verb: 'exit-mode' }
runtime.trigger('close', { semantic: { family: 'commit', verb: 'discard', intent: 'loss' } });
runtime.trigger('close', { semantic: { family: 'signal', verb: 'alert' } }); // → throws (signal no en allowedFamilies)
```
**Reglas**:
- El `family` declarado en el morfo es IMPLÍCITAMENTE allowed; no hace falta repetirlo en `allowedFamilies` (que enumera SOLO las alternativas).
- El override falla con `SomaRuntimePolymorphicError` si la family no está en allowedFamilies y no es el default.
- `TriggerOptions.semantic` opcional. Sin él, la trigger usa el shape canónico del morfo (comportamiento original).
**Por qué no la forma del libro literal** (`{ allowedFamilies, defaultSemantic: { family, verb, intent } }`):
- Backwards-compat: los 29 morfos existentes 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`:
```ts
{
name: 'close',
semantic: {
family: 'emerge', // default
verb: 'close',
target: v.partRef('content'),
sequence: 'pre',
persistence: 'transient',
allowedFamilies: ['emerge', 'commit', 'signal']
},
regime: 'lock',
commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' }
// NO prewrite — el provider escribe data-last-action imperativamente
}
```
El `DialogProvider.dismissWith(action, opts?)` traduce la acción al shape correcto:
```ts
const DISMISS_CAUSES = {
save: { lastAction: 'saved', semantic: { family: 'commit', verb: 'save', intent: 'fulfill' } },
cancel: { lastAction: 'cancelled', semantic: { family: 'emerge', verb: 'close' } },
dismiss: { lastAction: 'dismissed', semantic: { family: 'emerge', verb: 'dismiss' } },
'dismiss-outside': { lastAction: 'dismissed-outside', semantic: { family: 'emerge', verb: 'dismiss' } },
fail: { lastAction: 'failed', semantic: { family: 'signal', verb: 'alert', intent: 'threat' } }
};
```
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
- Cascade sema reescrito (`eventName: 'close'` + `state` matchers en `data-last-action`)
- 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`).
- **5 pickers** (color / date / date-range / time / time-range) — **CERRADO 2026-07-12
(SEM-4, `b55ca6ee`)**: tras la reconciliación de-dialoged (2026-06-27) sus morfos ya no
declaran `close` propio (`expression: 'delegated'` — la firma pertenece al Popover
compuesto); el cierre programático (commit/cancel/select-close) ahora enruta por causa
vía `PickerShellHandle.setPopoverDismiss` → `popover.dismissWith('save'|'cancel')`
(delegado inyectado por el eidos PickerShell root; fallback raw para composiciones
headless). Verificado en vivo: Done → `close · commit · fulfill` · Cancel →
`close · emerge`.
- Único morfo con shape pre-polymorphic restante: el fixture sintético `prewriteFixtureMorfo` — vivo sólo para tests.
**API pública preservada** en todos: ningún cambio observable para el consumidor de los componentes.
### D.12 Holds/duraciones: materialización numérica de las regiones cualitativas del libro (+ "el hold es suelo, no tijera")
- **Status**: **PROJECT_CANON** (materialización propia; el libro rehúsa dar números a propósito)
- **Origen**: auditoría clean-room 2026-07-06. La cabecera de `holds.ts` citaba un
"cap. 24 §6.2 (Holds por familia e intent)" **que no existe** (el cap. 24 §6 real
es "Canales, texto e intent") y reclamaba tablas "verbatim" con números
("commit.fulfill: 280") **que el libro jamás da**. La doctrina temporal real del
libro es CUALITATIVA: cap. 4 §13 (duración expresiva · estado · resultado ·
huella — "un evento no termina siempre cuando acaba su animación") · cap. 12 §4-§9
(duración/persistencia/huella/caducidad; "si un evento importante solo existe
durante un instante, muchos usuarios no lo recibirán") · cap. 32 TABLAS 32.1/32.2
("regiones de diseño, no números sagrados": affirm "breve" · fulfill "breve-media,
más resolutivo" · risk "hasta corrección" · threat "entrada rápida + persistencia
hasta acción" · loss "breve + huella").
**Decisiones (usuario, 2026-07-06):**
1. **Los ms son autoría del framework** (materialización de las regiones sobre
`SEMA_DURATIONS`), nunca "transcripción del libro". Procedencia corregida en
`holds.ts`/`durations.ts`.
2. **Peldaño nuevo `settled` (400 ms)** — la escala no tenía paso entre `brief`
(240) y `noticed` (600) y la región "breve-media" lo exigía. `commit.fulfill`
600→400 (`settled`); `signal.loss` 600→240 (`brief` — la huella es del caller:
undo/estado, no señal más larga).
3. **Tabla única**: `SEMA_HOLDS_BY_INTENT` (familia+intent) es LA fuente de holds;
el `hold` por-familia duplicado de `SEMA_MAP` se eliminó (había derivado:
signal 600 contra la región "breve o contextual" → vuelve a 240). El resolver
compone `signal.hold ?? resolveHoldsByIntent(family, intent)`.
4. **"El hold es suelo, no tijera"** — el des-estampado ya no amputa la expresión:
tras el hold (mínimo de registro), el canal visual espera el `finished` de las
animaciones activas del target (`VisualChannel.awaitExpression`), con tope
ABSOLUTO `MAX_EXPRESSION_WAIT_MS = 1500` (constante de ingeniería — derivarlo
del hold re-acoplaría los presupuestos que el cap. 4 §13 separa; el corte era
el antipatrón del cap. 32 §1: "una señal necesaria, por desaparecer demasiado
pronto"). La persistencia sigue siendo declaración del morfo (la huella SE
DECLARA), nunca default del runtime.
5. **Firmas re-materializadas a las regiones**: announce neutral/affirm
`deliberate(600)`→`moderate(240)` ("breve"); fulfill →`slower(400)`
("breve-media"); risk `emphatic(800)` / threat `sustained(1000)` conservan la
escalación que el libro sí quiere saliente/persistente.
6. **Triple guarda**: suelo (attrs viven ≥ hold), espera de expresión (test del
canal), y lint de diseño (ninguna firma transitoria > tope; subirla exige subir
la constante conscientemente). `vocabularies.md` (generado de `holds.ts`) queda
veraz sin tocarlo.
**Candidato editorial** (si el autor lo quiere para el ApD, "Lo que la práctica
corrigió"): esta es la historia inversa a las demás — aquí **el libro corrigió a la
práctica**: el runtime había convertido el suelo en tijera y la tabla derivada en
canon; releer la fuente restauró ambos.
---
### D.13 El diálogo declara `emerge.open`, no `shift.enter-mode` (la desviación emerge/shift del diálogo)
- **Status**: **DESVIACIÓN REGISTRADA Y BENDECIDA POR EL LIBRO** (edición FINAL,
Apéndice D, ancla **BK-D11**; pass de verbos C2, checkpoint 2026-07-07)
- **Origen**: el libro doctrina el modal bloqueante como `shift.enter-mode`
(cap. 8 §5 lo usa así; cap. 26 §8: "el dropdown es emerge.open y el modal es
shift.enter-mode"; cap. 27 §1: "abrir un modal… todo eso es shift"). El
contrato real de `dialog` declara `emerge-open`/`emerge-close`, y su cierre
polimórfico admite emerge/commit/signal — shift no está en la lista. Esta
entrada es el registro que el propio Apéndice D exige ("registrada en el
cuaderno de desviaciones del proyecto, con su razón").
**La razón (del propio Apéndice D del libro):**
> "En el componente genérico pesó más la aparición que el cruce de marco. Y el
> cruce quedó reservado a los usos que de verdad bloquean el fondo y capturan
> el foco — el mismo componente puede ser una cosa u otra según cómo se use.
> La doctrina del libro no cambia: el modal pesado sigue siendo shift. Lo que
> la práctica enseñó es que **la frontera emerge/shift no pasa entre
> componentes, sino por dentro de ellos**."
Y la regla de convivencia (pág. 411): *"Ambas lecturas son defendibles… La
gramática no exige que todas las implementaciones lean igual el caso frontera;
exige que cada una **elija, declare y sea consecuente**. El desacuerdo,
mientras esté declarado, es información."*
**Consecuencias operativas:**
1. `dialog` (y `alert-dialog`, que delega en sus eventos) CONSERVA
`emerge-open`/`emerge-close`. No hay migración a shift.
2. Un uso que de verdad cambie el régimen (bloquea el fondo, captura el foco,
exige reorientación como MODO) puede componer `shift.enter-mode` a nivel de
aplicación — la frontera se decide POR USO, no por componente.
3. El caso queda como "lo que sigue abierto" nº1 del propio libro: señala dónde
la frontera emerge/shift necesita más trabajo teórico. Si el libro la
redefine en una edición futura, esta entrada se revisa.
---
## E. Resumen ejecutivo
**Familias** (libro Cap 8): 8 — sin cambios. (`contact, commit, signal, handle, emerge, shift, sustain, delegate`)
**Intents** (libro Cap 10): 6 — sin cambios. (`neutral, affirm, fulfill, risk, threat, loss`)
### Verbos / casos que pasan al libro (BOOK_CANON)
- `handle.scroll` (A.1)
- `commit.remove` (A.3, con distinción remove/delete/unselect)
- `commit.set` (A.5)
- `commit.apply` (A.6)
- `commit.move` (A.7, distinto de reorder)
- `commit.unselect` (A.11)
- Cancelación de drag = `commit.cancel` (B.1)
- Scroll programático = `shift.navigate` (B.2)
- Sort = `commit.set` (B.4)
- Eventos no perceptibles no son eventos (B.5)
- Toggle con dos eventos direccionales (B.6, sin canonizar intents)
- Intent en contact visual-only (B.7)
- Cada actor declara sus eventos (D.1)
- Family policy `intentRequirement` + `intentGuidance` (D.3)
- `clear` = `commit.reset` (C.1)
### Pendientes — CANDIDATE (mantener en canon de implementación, NO al libro todavía)
- `commit.acknowledge` (A.2) — necesita casos fuertes
- `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.

Powered by TurnKey Linux.