|
|
---
|
|
|
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.
|