|
|
---
|
|
|
title: Registro de desviaciones entre implementación y canon editorial
|
|
|
type: decision-log
|
|
|
audience: human + agent
|
|
|
authority: authoritative registry — where the implementation deviates from the book canon, with per-entry status
|
|
|
status: current
|
|
|
source: migrated verbatim from src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md (2026-07-02, docs-book F7.6; kept in Spanish — it is a logbook of literal author decisions and proposed doctrinal text for the Spanish book)
|
|
|
---
|
|
|
|
|
|
# Registro de desviaciones entre implementación y canon editorial
|
|
|
|
|
|
> **Este documento no forma parte del libro.** Es un registro interno de decisiones de implementación. Su objetivo es separar qué pertenece al canon editorial («Diseñando lo que ocurre»), qué pertenece al canon del proyecto UIX, y qué queda como extensión local o candidato pendiente.
|
|
|
>
|
|
|
> **Principio rector**: solo entra al libro lo que mejora la teoría general. Lo demás puede vivir como extensión del proyecto. El libro debe conservar un núcleo estable; la implementación puede tener vocabulario más rico.
|
|
|
>
|
|
|
> **No todo lo que aparece en implementación debe volver al libro.**
|
|
|
|
|
|
---
|
|
|
|
|
|
## Clasificación de status
|
|
|
|
|
|
| Status | Significado |
|
|
|
| --------------------------- | -------------------------------------------------------------------------- |
|
|
|
| **BOOK_CANON** | Ya debería pasar al libro como ampliación canónica. Aceptado por el autor. |
|
|
|
| **PROJECT_CANON** | Canon válido del proyecto, pero no necesariamente del libro. |
|
|
|
| **CANDIDATE** | Caso plausible, pero falta doctrina o uso real para canonizar. |
|
|
|
| **LOCAL_EXTENSION** | Necesario para este proyecto, pero no escalable al libro. |
|
|
|
| **DEPRECATED** | Se mantiene temporalmente, pero debería eliminarse. |
|
|
|
| **ALIAS** | Nombre aceptado como comodidad técnica, mapea a otro verbo doctrinal. |
|
|
|
| **IMPLEMENTATION_CONTRACT** | Existe en morfos/runtime, pero no necesariamente como doctrina editorial. |
|
|
|
|
|
|
Cada entrada lleva su status. Las entradas con `BOOK_CANON` incluyen el texto doctrinal recomendado para el libro, en bloques citados.
|
|
|
|
|
|
---
|
|
|
|
|
|
## A. Verbos añadidos al canon de implementación
|
|
|
|
|
|
Estos verbos están en `src/uix/sema/verbs.ts:SEMA_VERBS`. Su status doctrinal para el libro varía.
|
|
|
|
|
|
### A.1 `handle.scroll`
|
|
|
|
|
|
- **Status**: **BOOK_CANON**
|
|
|
- **Libro Cap 25 §1** lista 7 verbos: pick, carry, drop, drag, resize, rotate, reorder. `scroll` no aparece literalmente.
|
|
|
- **Decisión del autor**: aceptar como ampliación de `handle`. Cap 25 §1 ("manipulación directa de un objeto") cubre la lectura: el usuario desplaza directamente un viewport mediante gesto continuo.
|
|
|
- **Distinción doctrinal**:
|
|
|
- Scroll gestual del usuario → `handle.scroll`
|
|
|
- Scroll programático del sistema (scrollToIndex / scrollToCell / scrollIntoView) → `shift.navigate`
|
|
|
- **Texto doctrinal para el libro**:
|
|
|
|
|
|
> `handle.scroll` cubre los casos en los que el usuario desplaza directamente un viewport, lista, panel o superficie desplazable. No equivale a navegación programática: cuando el sistema mueve al usuario a una posición concreta sin control directo, el evento pertenece mejor a `shift.navigate`.
|
|
|
|
|
|
### A.2 `commit.acknowledge`
|
|
|
|
|
|
- **Status**: **CANDIDATE**
|
|
|
- **Libro Cap 23 §5** lista 12 verbos de commit. `acknowledge` no aparece.
|
|
|
- **Decisión del autor**: NO pasar al libro todavía. Mantener en canon de implementación pendiente de casos fuertes.
|
|
|
- **Solapamiento problemático**: con `commit.confirm`, `commit.cancel`, `commit.submit`, `signal.dismiss`. Si el usuario solo cierra un aviso, quizá no hay commit; quizá solo hay retirada de señal (`signal.notify + neutral → emerge.close`).
|
|
|
- **Cuándo sí cambiar a BOOK_CANON**: si hay obligación explícita de reconocimiento (registrado en el sistema):
|
|
|
- "He leído y entiendo esta advertencia"
|
|
|
- "Entiendo que esta acción no se puede deshacer"
|
|
|
- "Acepto las condiciones"
|
|
|
|
|
|
### A.3 `commit.remove` vs `commit.delete` vs `commit.unselect`
|
|
|
|
|
|
- **Status de `remove`**: **BOOK_CANON**
|
|
|
- **Libro Cap 23 §5** lista `delete` (destrucción consumada). `remove` no aparece.
|
|
|
- **Decisión del autor**: distinción de tres verbos sobre operaciones de retirada/eliminación.
|
|
|
- **Texto doctrinal para el libro**:
|
|
|
|
|
|
> `remove` no significa destruir. Significa retirar un elemento de una colección, relación o conjunto operativo. Si el elemento deja de existir o deja de estar disponible, corresponde `delete`. Si solo deja de estar seleccionado, corresponde `unselect`.
|
|
|
|
|
|
### A.4 `commit.confirm`
|
|
|
|
|
|
- **Status**: **CANDIDATE**
|
|
|
- **Libro Cap 23 §5** lista `submit`, `complete`. `confirm` no aparece.
|
|
|
- **Decisión del autor**: NO canonizar todavía.
|
|
|
- **Problema**: "confirmar" muchas veces no es el resultado final, sino un paso previo. El botón dice "Confirmar" pero el evento real puede ser `delete`, `submit`, `apply`, `authorize` o `acknowledge`.
|
|
|
- **Cuándo sí cambiar a BOOK_CANON**: si hay casos donde el resultado aplicado sea literalmente "confirmación registrada", no acción posterior (confirmar asistencia, confirmar lectura, confirmar email).
|
|
|
|
|
|
### A.5 `commit.set`
|
|
|
|
|
|
- **Status**: **BOOK_CANON**
|
|
|
- **Libro**: Cap 25 ejemplo slider menciona `commit.set + affirm` pero no figura en lista canónica de Cap 23.
|
|
|
- **Decisión del autor**: formalizar en Cap 23 §5.
|
|
|
- **Casos canónicos**: slider, sort, criterio de filtro, valor de un picker, tamaño de página, zoom, preferencia local, criterio de ordenación, valor numérico.
|
|
|
- **Texto doctrinal para el libro**:
|
|
|
|
|
|
> `commit.set` comunica que un valor, criterio o parámetro ha quedado aplicado. Diferente de `save` (persistir globalmente), `submit` (enviar formulario), `complete` (culminar flujo), `select` (elegir un item).
|
|
|
|
|
|
### A.6 `commit.apply`
|
|
|
|
|
|
- **Status**: **BOOK_CANON**
|
|
|
- **Libro Cap 29** (delegate) usa `commit.apply` en ejemplos. Cap 23 §5 no lo lista.
|
|
|
- **Decisión del autor**: formalizar en Cap 23 §5.
|
|
|
- **Texto doctrinal para el libro**:
|
|
|
|
|
|
> `commit.apply` comunica que un conjunto de cambios o una operación se ha aplicado. Diferente de `save` (guardar) — apply puede aplicar sin persistir; save persiste.
|
|
|
|
|
|
### A.7 `commit.move`
|
|
|
|
|
|
- **Status**: **BOOK_CANON** (con distinción de `reorder`)
|
|
|
- **Decisión del autor**: aceptar si se distingue:
|
|
|
- `commit.move` → un elemento cambia de lugar
|
|
|
- `commit.reorder` → una colección cambia de orden
|
|
|
- **Caso típico**: `handle.drop → commit.move + affirm` o `handle.drop → commit.reorder + affirm` según si lo que cambió fue la posición de UN item o el orden de la colección.
|
|
|
|
|
|
### A.8 `commit.upload`
|
|
|
|
|
|
- **Status**: **CANDIDATE**
|
|
|
- **Decisión del autor**: dudoso como commit. Subir un archivo es proceso (`sustain.uploading`) que termina en `commit.complete` (éxito) o `commit.fail` (error). Como verbo de resultado quizá es redundante con `commit.complete`.
|
|
|
- **Mantener** como candidato hasta que haya caso donde "upload" sea el resultado final no reducible a complete/attach/add/apply.
|
|
|
|
|
|
### A.9 `commit.partial`
|
|
|
|
|
|
- **Status**: **CANDIDATE / revisar**
|
|
|
- **Decisión del autor**: probablemente NO es verbo. "Partial" suena a estado o resultado incompleto, no a acción.
|
|
|
- **Alternativas mejores**:
|
|
|
- `commit.complete` + variant `partial`
|
|
|
- `commit.fail + risk`
|
|
|
- `sustain.partial` (estado, no acción)
|
|
|
- **A revisar antes de canonizar**.
|
|
|
|
|
|
### A.10 `commit.block`
|
|
|
|
|
|
- **Status**: **CANDIDATE / revisar**
|
|
|
- **Decisión del autor**: probablemente NO es verbo de commit. "Blocked" suele ser estado, no resultado aplicado por el usuario.
|
|
|
- **Alternativas mejores**:
|
|
|
- `signal.alert + threat`
|
|
|
- `commit.fail + risk`
|
|
|
- `sustain.blocked + risk` (estado)
|
|
|
- **A revisar antes de canonizar**.
|
|
|
|
|
|
### A.11 `commit.unselect`
|
|
|
|
|
|
- **Status**: **BOOK_CANON**
|
|
|
- **Libro Cap 23 §5** lista `select` pero no `unselect`. Pasa el par natural al canon.
|
|
|
- **Decisión del autor (transcrita literal)**:
|
|
|
|
|
|
> Seleccionar y deseleccionar son resultados aplicados sobre el estado de selección de un elemento. Eso es `commit`, porque el resultado queda aplicado.
|
|
|
>
|
|
|
> - No es `remove`: no estás eliminando el item ni sacándolo de una colección funcional; solo estás cambiando su estado de selección.
|
|
|
> - No es necesariamente `toggle`: `toggle` describe mejor el mecanismo binario o el control, pero no expresa tan bien el resultado semántico concreto.
|
|
|
>
|
|
|
> Regla final:
|
|
|
>
|
|
|
> - `select` / `unselect` → resultados sobre estado de selección
|
|
|
> - `toggle` → inversión binaria genérica
|
|
|
> - `remove` → retirada, eliminación o salida de colección
|
|
|
|
|
|
- **Aplicado en**: calendar, combobox, grid-list, listbox, select, tag-group.
|
|
|
|
|
|
---
|
|
|
|
|
|
## B. Adopciones interpretativas (no literales en el libro)
|
|
|
|
|
|
### B.1 Cancelación de drag = `commit.cancel`
|
|
|
|
|
|
- **Status**: **BOOK_CANON** (composición canónica)
|
|
|
- **Libro Cap 25 §4** cubre fases pick → carry → drop. No aborda explícitamente Escape durante carry.
|
|
|
- **Decisión del autor**: la cancelación de un drag es composición `handle + commit.cancel`. No hay drop, no hay resultado aplicado, la manipulación se aborta.
|
|
|
- **Para el libro**: añadir al capítulo de handle como composición canónica.
|
|
|
|
|
|
### B.2 Scroll programático = `shift.navigate`
|
|
|
|
|
|
- **Status**: **BOOK_CANON** (con matiz)
|
|
|
- **Libro Cap 27 §5** lista `shift.navigate` con ejemplos a escala "Lista → detalle. Página A → página B".
|
|
|
- **Decisión del autor**: aclarar que `shift.navigate` puede operar a varias escalas:
|
|
|
- entre páginas
|
|
|
- entre vistas
|
|
|
- entre pasos
|
|
|
- dentro de una lista (programáticamente)
|
|
|
- hasta una celda
|
|
|
- hasta un índice
|
|
|
- **Distinción**: usuario desplaza manualmente → `handle.scroll`; sistema mueve viewport → `shift.navigate`. **No** crear `shift.scrollto` todavía.
|
|
|
|
|
|
### B.3 Cambio de tamaño del modelo de datos
|
|
|
|
|
|
- **Status**: **LOCAL_EXTENSION / CANDIDATE**
|
|
|
- **Decisión del autor**: NO regla general del libro todavía. Caso por caso.
|
|
|
- **Regla doctrinal aplicable** (ya en el libro Cap 4 §1, recordatorio):
|
|
|
|
|
|
> Un cambio interno de datos solo se convierte en evento cuando se vuelve perceptible o cambia lo que el usuario puede hacer, debe atender o necesita interpretar.
|
|
|
|
|
|
- **Si el cambio merece evento**, el verbo correcto depende del caso:
|
|
|
- `signal.notify + neutral` — si avisa de nuevos items
|
|
|
- `emerge.reveal` — si aparecen elementos
|
|
|
- `commit.set + neutral` — si el sistema aplica un nuevo valor operativo visible (tamaño de dataset, criterio)
|
|
|
|
|
|
### B.4 Sort de tabla = `commit.set`
|
|
|
|
|
|
- **Status**: **BOOK_CANON** (como ejemplo en commit.set)
|
|
|
- **Decisión del autor**: ordenar por columna no es reordenar manualmente items; es fijar un criterio.
|
|
|
- **Distinción**:
|
|
|
- sort by criterion → `commit.set`
|
|
|
- manual reorder → `handle.reorder` → `commit.reorder`
|
|
|
- **Para el libro**: añadir como ejemplo de `commit.set` cuando se introduzca ese verbo.
|
|
|
|
|
|
### B.5 Eventos no perceptibles no son eventos
|
|
|
|
|
|
- **Status**: **BOOK_CANON** (regla doctrinal)
|
|
|
- **Libro Cap 4 §1** ya tiene la regla pero conviene reforzar.
|
|
|
- **Texto doctrinal para el libro** (añadir como nota explícita en Cap 4):
|
|
|
|
|
|
> No todo cambio de estado necesita evento. Un estado puede volver automáticamente a su forma base sin producir un evento semántico si el usuario no necesita interpretarlo como cambio relevante.
|
|
|
|
|
|
- **Ejemplo de aplicación**: timer interno que revierte `copied=false` tras N ms en clipboard. Es estado (`data-copied`), no evento sema.
|
|
|
|
|
|
### B.6 Toggle con dos eventos direccionales
|
|
|
|
|
|
- **Status**: **BOOK_CANON** (posibilidad direccional, NO canonizar intents)
|
|
|
- **Libro Cap 22 §10** ejemplo: "Toggle: contact.press → commit.toggle + affirm". UN evento.
|
|
|
- **Decisión del autor**: aceptar la POSIBILIDAD de dos eventos direccionales, pero NO fijar intents por defecto.
|
|
|
- **Texto doctrinal para el libro**:
|
|
|
|
|
|
> Un toggle puede modelarse como un solo evento de inversión o como dos eventos direccionales si la diferencia entre activar y desactivar importa para el usuario. El intent de cada dirección depende del contexto.
|
|
|
|
|
|
- **Ejemplos del autor**:
|
|
|
- Activar notificaciones: check → affirm, uncheck → neutral
|
|
|
- Desactivar tracking: uncheck → affirm
|
|
|
- Desmarcar consentimiento obligatorio: uncheck → risk
|
|
|
- **Nota para la implementación**: el checkbox actual del proyecto fija `check=affirm` / `uncheck=neutral` como defaults razonables; los consumidores pueden override per-instance.
|
|
|
|
|
|
### B.7 Intent en `contact` como anticipación visual únicamente
|
|
|
|
|
|
- **Status**: **BOOK_CANON** (regla de buena práctica)
|
|
|
- **Libro Cap 22 §11** ya tiene la doctrina ("el intent fuerte no debería vivir en el contacto").
|
|
|
- **Decisión del autor**: el evento `contact.*` no debe cargar intent fuerte; el `intent` prop del componente puede afectar visualmente (data-color, forma) pero el evento sema se mantiene neutro.
|
|
|
- **Aplicado en**: Button (`contact.activate` sin intent en el evento; `intent` prop drives data-color).
|
|
|
- **Conecta con D.3**: la family policy actual `'allowed'` no comunica "discouraged" — requiere refinamiento.
|
|
|
|
|
|
---
|
|
|
|
|
|
## C. Cluster decisions
|
|
|
|
|
|
### C.1 `clear` field — RESUELTO
|
|
|
|
|
|
- **Status**: **BOOK_CANON** (mapeo a verbo existente)
|
|
|
- **Decisión del autor**: `commit.reset`. 10 morfos actualizados.
|
|
|
- **Para el libro**: añadir como ejemplo de `commit.reset` cuando se introduzca `commit.set`/`commit.reset` al libro.
|
|
|
|
|
|
### C.2 `unselect` — RESUELTO
|
|
|
|
|
|
- Ver A.11 — promovido a verbo BOOK_CANON.
|
|
|
|
|
|
### C.3.a Renames de forma (mecánicos)
|
|
|
|
|
|
- **Status**: PROJECT_CANON (no requieren cambio en el libro)
|
|
|
- 11 events renombrados a `{family}-{verb}-{variant}` para alinearse con la convención de naming:
|
|
|
- `carousel.shift-slide` → `shift-navigate-slide`
|
|
|
- `feed.shift-focus-item` → `shift-navigate-focus-item`
|
|
|
- `feed.commit-load-more` → `commit-submit-load-more`
|
|
|
- `file-upload.commit-add` → `commit-set-add`
|
|
|
- `file-upload.signal-reject` → `signal-warn-reject`
|
|
|
- `form.signal-invalid` → `signal-warn-invalid`
|
|
|
- `number-field.handle-scrub` → `handle-drag-scrub`
|
|
|
- `range-calendar.commit-start` → `commit-select-start`
|
|
|
- `range-calendar.commit-range` → `commit-select-range`
|
|
|
- `tags-input.commit-add` → `commit-set-add`
|
|
|
- `tags-input.signal-reject` → `signal-warn-reject`
|
|
|
|
|
|
### C.3.b Correcciones doctrinales
|
|
|
|
|
|
- **Status**: BOOK_CANON (precedent Button)
|
|
|
- `command.commit-invoke` (declared `submit + fulfill`) → `commit-submit-invoke + submit + affirm`. Cambio de intent (`fulfill` → `affirm`) por Cap 22 §8 (celebrate-before-time es antipatrón). Mismo precedent que Button.
|
|
|
|
|
|
---
|
|
|
|
|
|
## D. Decisiones arquitecturales
|
|
|
|
|
|
### D.1 Cada actor declara sus eventos
|
|
|
|
|
|
- **Status**: **BOOK_CANON**
|
|
|
- **Texto doctrinal para el libro** (capítulo de composición o apéndice técnico):
|
|
|
|
|
|
> Un componente no debe declarar eventos que no produce. En una composición, cada actor declara su parte del evento.
|
|
|
|
|
|
- **Ejemplo**: un Button no declara `commit.delete + loss` si solo registra el click; declara `contact.activate`. El flujo, diálogo o acción que realmente elimina declara `commit.delete + loss`. Esto evita sobrecargar el botón y explica por qué la gramática necesita composición.
|
|
|
|
|
|
### D.2 Eventos del contrato que no se disparan
|
|
|
|
|
|
- **Status**: **IMPLEMENTATION_CONTRACT** (no doctrina del libro)
|
|
|
- **Decisión del autor**: doctrinalmente peligroso si se presenta mal (parece que el sistema promete eventos inexistentes). NO pasarlo al cuerpo del libro.
|
|
|
- **Tratamiento**: apéndice técnico con metadata explícita:
|
|
|
|
|
|
```ts
|
|
|
emission: 'runtime' | 'host' | 'external' | 'declared-only';
|
|
|
// o:
|
|
|
implemented: true | false;
|
|
|
```
|
|
|
|
|
|
- **Acción del proyecto**: añadir flag `emission` al MorfoEvent type en una iteración futura para hacer explícito qué eventos se emiten en runtime vs cuáles son declarados sin emisor (contract surface para testing, analytics, accesibilidad externa).
|
|
|
|
|
|
### D.4 Campo `expression` en el morfo: cómo se rellena la firma sema
|
|
|
|
|
|
- **Status**: **IMPLEMENTATION_CONTRACT** (no doctrina del libro)
|
|
|
- **Decisión del autor (Lectura C)**: un morfo con eventos doctrinales declara su modo de expresión perceptual:
|
|
|
|
|
|
```ts
|
|
|
type SemaExpressionMode = 'pack' | 'family-default' | 'delegated' | 'none';
|
|
|
```
|
|
|
|
|
|
- **`pack`**: el morfo tiene un archivo `src/uix/sema/components/{kebab}.ts` con cascade rules específicas (sonido, háptica, prioridad).
|
|
|
- **`family-default`**: el morfo descansa en `SEMA_MAP.families[family].base` sin tuning per-componente. Apropiado cuando el componente no necesita firma diferenciada.
|
|
|
- **`delegated`**: el componente compone otros morfos que sí emiten (p. ej. picker emite via calendar/time-field/color-area que tienen sus propios eventos).
|
|
|
- **`none`**: el morfo declara contrato pero no participa en sema runtime (reservado para utilidades no perceptuales).
|
|
|
|
|
|
- **Lint**: `npm run morfo:vocabulary` valida que todo morfo con `events.length > 0` cumpla:
|
|
|
1. `scope` incluye `'sema'` (FAIL).
|
|
|
2. Existe un pack en `src/uix/sema/components/{kebab}.ts` **o** `expression !== undefined` (WARN si falla).
|
|
|
|
|
|
- **Cuándo crear pack vs `family-default`** (criterios para D.4):
|
|
|
- **Crear pack** si: eventos de alta frecuencia + riesgo de fatiga (toggles, form controls), o el componente necesita una firma sobria distinta del family base, o introduce cascades con prioridad (a11y override).
|
|
|
- **`family-default`** si: el family base ya es la firma correcta y el componente no compite con otros del mismo family por intensidad perceptual.
|
|
|
|
|
|
### D.5 Packs para componentes de alta frecuencia (toggles)
|
|
|
|
|
|
- **Status**: **PROJECT_CANON** (decisión arquitectural sin necesidad de pasar al libro)
|
|
|
- **Caso**: `switch`, `toggle`, `toggle-group` emiten `commit-toggle` en bucle (settings panels, toolbars, segmented controls). Sin tuning, heredan el `family.commit.base.gain = 0.3` que es demasiado para una sesión sostenida.
|
|
|
- **Solución (2026-05-26, DEROGADA — ver la nota del 2026-08-06 al final)**: tuning `form.toggle.silent = { gain: { op: 'add', value: -0.3 } }` en `src/uix/sema/sounds.ts`. Cancelaba exactamente el `gain` del family base, dejando el default en 0 (silencio). Los intent.deltas que añaden gain (`threat: +0.1`, `fulfill: +0.05`) seguían surgiendo, así un toggle destructivo sí emitía señal audible.
|
|
|
- **Haptic**: tap leve (`intensity: 0.3, duration: 12, delay: 0`) sustituye el tap medio del family. Replaza el `kind` para que la háptica no oscile con el intent — la carga evaluativa de un toggle se lee en `data-color` + (selectivamente) sonido, no en háptica fluctuante.
|
|
|
- **Doctrina**: el libro habla de "componentes de baja intensidad" (cap. 22) — esta es la materialización runtime. NO ir al libro: es decisión de tuning, no de gramática.
|
|
|
- **Packs concretos**: `src/uix/sema/components/{switch,toggle,toggle-group}.ts` — los tres comparten la misma firma porque comparten rol UX (un press → un flip).
|
|
|
- **REVERTIDO 2026-08-05 — los toggles recuperan voz (directiva de autor)**: el
|
|
|
silencio de arriba se retira para `switch`, `toggle` y `toggle-group`, que pasan a
|
|
|
**`commit.medium` (gain 0.1)** — audible, y un tercio de una pulsación de botón
|
|
|
(`contact` base 0.25) para que un flip nunca pese más que una activación. Los deltas
|
|
|
de intent siguen montando encima (`threat` +0.1, `fulfill` +0.05). Razón: un silencio
|
|
|
por defecto se percibe como componente roto, no como sobriedad; la fatiga se combate
|
|
|
bajando el nivel, no anulándolo. El háptico ligero de abajo NO cambia.
|
|
|
Se conserva el silencio, con su justificación intacta, sólo donde el silencio ES la
|
|
|
firma: `tooltip` (revela al pasar el ratón — sonaría en cada cruce) y `media-player`
|
|
|
(su propia salida ES audio, D-AP2.7).
|
|
|
Además, **emitir silencio dejó de costar**: `SoundChannel.handle` corta antes de
|
|
|
sintetizar cuando la ganancia resuelta es ≤ 0 (`chans/sound.ts`), así que un
|
|
|
`{family}.silent` ya no levanta dos osciladores, un filtro y una envolvente para no
|
|
|
sonar. Fijado por test, verificado en rojo.
|
|
|
- **Renombrado 2026-08-05 — el prefijo `form.` muere**: este tuning nació como
|
|
|
`form.toggle.silent` y pasó a llamarse `commit.silent` — una clave que vivió UN
|
|
|
DÍA: la nota siguiente la retira del catálogo entera. El prefijo `form.` entró el
|
|
|
2026-05-19 (`09e261878`) nombrando con verdad a siete controles de formulario, pero
|
|
|
ESTE MISMO commit (`2d562f378`) lo extendió a menús y árboles vía `form.commit.subtle`
|
|
|
— el D.6 de abajo llegó a escribir el nombre correcto (`commit.select`) en la misma
|
|
|
línea que usaba la clave incorrecta. Llegó a 50 packs, de los que exactamente uno era
|
|
|
el componente Form. La ley queda escrita en
|
|
|
[`sema.md` § Tuning naming shapes](../architecture/sema.md#tuning-naming-shapes) —
|
|
|
**el primer segmento es la familia cuya base modifica el tuning, nunca un
|
|
|
componente** — y la vigila `src/uix/sema/sounds-grammar.test.ts`. Renombres del
|
|
|
mismo barrido: `form.commit.soft` → `commit.soft`, `form.commit.subtle` →
|
|
|
`commit.subtle`, `tooltip.silent` → `emerge.silent` (ambas retiradas al día
|
|
|
siguiente), `tabs.select.soft` → `commit.select.soft`. Sin cambio de
|
|
|
comportamiento: mismos deltas, mismas ganancias. **Claves vivas hoy: sólo
|
|
|
`commit.soft`, `commit.subtle`, `commit.select.soft`, `commit.medium` y las
|
|
|
`emerge.*`** — el enumerado generado está en
|
|
|
[`canon/vocabularies.md`](../canon/vocabularies.md#sound-tunings).
|
|
|
- **EL SILENCIO DEJA DE SER ARITMÉTICA — 2026-08-06 (directiva de autor)**: «el silencio
|
|
|
no admite verbos ni intents, es universal, por lo tanto es un valor canónico que cuando
|
|
|
se le envía al canal semántico éste simplemente lo ignora y no envía nada al
|
|
|
soundengine». Las tres claves silenciadoras (`commit.silent`, `emerge.silent`,
|
|
|
`contact.silent`) **desaparecen del catálogo**; en su lugar hay **`SILENT`**, un único
|
|
|
valor exportado por `$uix/sema` que se declara en el slice del canal y que el resolver
|
|
|
honra **retirando ese canal** de la firma — la forma que el propio motor ya prescribía
|
|
|
(«muting DROPS the channel rather than scaling to zero, because a `0` still buzzes»).
|
|
|
Un delta de intent ya no puede resucitarlo, porque no queda ningún número que mover.
|
|
|
MOTIVO MEDIDO, y desmiente la promesa de la «Solución» de arriba: el cero no era
|
|
|
silencio. Los deltas de intent suman **rugosidad** (`risk` +0.2, `threat` +0.4), lo que
|
|
|
cruza el umbral del AM del motor, y como el modulador se conecta al `AudioParam` de la
|
|
|
envolvente —y conectar a un `AudioParam` SUMA, no multiplica— el trémolo pasaba a ser
|
|
|
la señal entera. Renderizado offline con el grafo real: `risk` **−13,9 dBFS** y `threat`
|
|
|
**−6,7 dBFS** desde una firma declarada a `gain 0`, frente a **−9,3 dBFS** de un botón
|
|
|
normal. El «silencio» era lo más fuerte de la interfaz, y lo que sonaba era un zumbido
|
|
|
sin ataque ni caída cortado en seco. Fijado por tests en `resolver.test.ts` (universal
|
|
|
para toda familia e intent, y liftable sólo por REEMPLAZO) y por
|
|
|
`sounds-grammar.test.ts`, que ahora prohíbe tanto una clave `*.silent` como cualquier
|
|
|
afinado que deje su familia en `gain <= 0`.
|
|
|
|
|
|
### D.6 Packs para superficies de menú y árboles
|
|
|
|
|
|
- **Status**: **PROJECT_CANON**
|
|
|
- **Caso**: `menubar`, `navigation-menu`, `context-menu`, `dropdown-menu`, `tree-view`, `tree-grid` — superficies navegacionales de alta frecuencia. El usuario abre/cierra menús y expande/contrae nodos decenas de veces por sesión. Sin tuning, heredan `family.emerge.base.gain = 0.2` y `family.commit.base.gain = 0.3` — demasiado prominente.
|
|
|
- **Solución**: packs por componente que combinan tuning emerge soft + commit subtle:
|
|
|
- **emerge.open (menús, expand)**: `emerge.soft` (gain 0.08) — el contenido se revela sin competir con la superficie que lo invoca.
|
|
|
- **emerge.close (menús, collapse)**: `emerge.exit.soft` (gain 0.05, descending) — disciplina de dirección compartida con dialog/drawer/popover.
|
|
|
- **commit.select (item de menú, nodo de árbol)**: `commit.subtle` (gain 0.03) + `tap` leve. Más sobrio que radio-group porque la cascada típica es "menú cierra + item commit + nueva superficie aparece" — tres señales en milisegundos, hay que repartir intensidad.
|
|
|
- **Coherencia**: dropdown-menu y context-menu comparten firma idéntica (el usuario no debe aprender dos "sonidos de menú"). Tree-view y tree-grid idem.
|
|
|
- **ENMIENDA 2026-08-19 — `navigation-menu` sale del alcance de esta entrada.** Su `Link` ya no emite `commit-select`: navegar no fija nada, y un `commit` ahí es el antipatrón «Success de navegación» del libro (TABLA 9.3), reforzado por cap. 27 §2 («shift no es commit») y por la apertura de cap. 10 («una navegación no es exitosa por cambiar de pantalla»). El gesto habla ahora el par de cap. 22 §9 — `contact-activate` en el enlace y `shift-navigate` en el `<nav>` —, y **ninguna de las dos familias se afina**: traen su carácter del mapa (`touch` + `tick` háptico · `slide`), que es el mismo silencio que guarda el pack del `Sidebar` para el mismo acto. La preocupación que originó D.6 sigue siendo válida y se satisface por otra vía: el par nuevo es más sobrio que el `commit` que sustituye.
|
|
|
- **Dos cláusulas de esta entrada estaban caducadas cuando se enmendó**, y quedan corregidas aquí: (1) «menubar y navigation-menu no tienen evento de open/close declarado en el morfo» dejó de ser cierto el 2026-08-05, cuando `navigation-menu` declaró `emerge-open` / `emerge-close` sobre su `trigger`; (2) el `commit.subtle` (0.03) que esta entrada prescribía para `navigation-menu` **llevaba tiempo sin estar implementado** — murió en `86fa9a1b7` (2026-08-06, «el sonido se ELIGE, no se modula»), nadie nombró un sonido más quedo en su lugar y el evento sonaba a la base de familia. (3) Y el `emerge.soft` (0.08) de esta entrada lo absorbió el catálogo: la base `open` es 0.09, por eso las reglas `emerge` de los menús de clic están vacías y siguen siendo correctas. Para `navigation-menu`, en cambio, la respuesta NO es «soft» sino **`SILENT`** (firmado 2026-08-19): su megamenú se abre por HOVER, y medido con el default de familia un barrido por dos triggers y salir de la barra sonaba tres veces sin un clic — cap. 17 §3, cap. 26 §7, cap. 32 §10; la misma postura que `tooltip` y que el flyout del `sidebar` (D-SB.3). Una desviación PROJECT_CANON que describe una afinación que el código no aplica es una fuente que miente: el registro se actualiza en el mismo pase que el cableado.
|
|
|
- **`menubar` queda pendiente y NO se toca aquí**: su `commit-select` vive en el `trigger` de primer nivel, o sea en el gesto que DESPLIEGA un menú, que por cap. 26 §5 es una aparición. Misma clase de defecto, otro componente, otra sesión (regla `no-cascade-changes`).
|
|
|
- **Caveat (`tree-view` / `tree-grid`) — RESUELTO (verificado 2026-07-12, SEM-4)**: la
|
|
|
emisión aterriza vía `targetOverride` 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: `commit-select`; nav-menu: `emerge-open`/`emerge-close` desde 2026-08-05 y
|
|
|
`contact-activate`/`shift-navigate` desde 2026-08-19, ver la ENMIENDA de arriba;
|
|
|
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 — **RETIRADA el 2026-08-06, por disolución**
|
|
|
|
|
|
- **Status**: **RETIRADA**. No derogada por cambio de opinión: **disuelta por un
|
|
|
cambio de arquitectura que hace imposible el defecto que prohibía.** La
|
|
|
doctrina del autor que la originó sigue siendo cierta, pero ahora la sostiene
|
|
|
el orden de resolución en vez de la disciplina de quien escribe un pack.
|
|
|
- **Qué prohibía y por qué.** «Los samples no deben sustituir la firma semántica
|
|
|
base cuando esa sustitución impide la modulación por intent.» El mecanismo era
|
|
|
real: `sound()` devolvía una firma COMPLETA y la cascada la aplicaba en modo
|
|
|
`replace` DESPUÉS de los deltas de intent, así que canonizar un sample sobre un
|
|
|
evento `commit` borraba el perfil evaluativo — un `fulfill` perdía su +300 Hz
|
|
|
ascendente. La regla se enunció como lista de familias prohibidas, se le
|
|
|
encontró un infractor (`proof-of-human`), se enmendó para sacar a `handle` /
|
|
|
`sustain` / `delegate` (familias sin `base.sound`: donde no hay base no hay
|
|
|
nada que aplastar) y se le añadió un guard.
|
|
|
- **Por qué ya no hace falta.** Desde el 2026-08-06 el sonido no se resuelve por
|
|
|
capas: se ELIGE, en dos búsquedas.
|
|
|
|
|
|
```
|
|
|
nombre = per-emit ?? cascada ?? pack ?? morfo ?? familia[verbo] ?? familia.default
|
|
|
sonido = pack[`${nombre}.${intent}`] ?? pack[nombre] ?? nada
|
|
|
```
|
|
|
|
|
|
El intent **selecciona un sonido entero** (`tick.threat` no es un `tick`
|
|
|
deformado) y, si el pack no trae variante para ese intent, se desprecia el
|
|
|
intent y suena la base. No queda nada que aplastar porque no queda aritmética:
|
|
|
un nombre sustituye a un nombre. El tipo de una regla acepta un `SoundName` o
|
|
|
`SILENT`, y ni el producto ni un tema ni la app pueden escribir un parámetro.
|
|
|
|
|
|
- **Lo que se conserva de la doctrina, y dónde vive ahora:**
|
|
|
- «El silencio es una firma válida» → `SILENT`, valor canónico honrado por el
|
|
|
resolver retirando el canal (D.5 + `sounds-grammar.test.ts`).
|
|
|
- «Los packs sólo se crean cuando corrigen una diferencia perceptiva real» →
|
|
|
sigue vigente como criterio de autor, y hoy es barato cumplirlo: un pack que
|
|
|
no tiene nada que decir no tiene nada que escribir.
|
|
|
- «Los samples son recursos de producto, no canon por defecto» → el catálogo
|
|
|
los admite con nombre (`ping`, `error`) y una app puede autorar los suyos por
|
|
|
`overrides.cascade`, que sigue abierto a propósito.
|
|
|
|
|
|
- **⚠️ EL LÍMITE FÍSICO, medido — lo único de esta entrada que hay que seguir
|
|
|
sabiendo.** `playSample` lee **exactamente dos campos** de la firma:
|
|
|
`sampleUrl` y `gain`. No hay `playbackRate` ni `detune`. Luego **sobre una
|
|
|
grabación el intent sólo puede mover el volumen**. Un nombre con fichero que
|
|
|
deba llevar carga evaluativa **necesita un fichero por intent** (`tick.mp3` Y
|
|
|
`tick.threat.mp3`), no uno solo modificado. Con síntesis el problema no existe
|
|
|
porque cada variante se diseña entera. Es física del audio, no política: no se
|
|
|
diseña alrededor de ella.
|
|
|
|
|
|
Este límite dejó de ser una excepción y pasó a ser **la forma general del
|
|
|
sistema**: un intent siempre selecciona una entrada distinta, venga de un
|
|
|
oscilador o de un fichero. Lo que antes era el caso raro de `signal` es hoy
|
|
|
cómo funciona todo.
|
|
|
|
|
|
- **Lo que sigue descartado permanentemente**: `sampleOverlay` (sample como capa
|
|
|
adicional sobre el synth). Sobreingeniería — añade mixing en WebAudio, capa de
|
|
|
resolver y mandos extra al diseñador, para casos raros.
|
|
|
|
|
|
### S-07 — **DISUELTA junto con D.7**
|
|
|
|
|
|
El hallazgo S-07 de [`AUDIT-sema-2026-08-05`](../process/AUDIT-sema-2026-08-05.md)
|
|
|
decía que las 14 claves de escalera de `SOUND_TUNINGS` fijaban `gain` como número
|
|
|
DESNUDO, y un número desnudo se aplica en `replace` igual que un sample: cambiar
|
|
|
un sample por `soundTuning('commit.soft')` dejaba el intent igual de aplastado.
|
|
|
Era el mismo defecto que D.7 una capa más abajo, y quedó ABIERTO por decisión del
|
|
|
autor («se trata más detenidamente»).
|
|
|
|
|
|
Ya no existe: **`SOUND_TUNINGS` fue retirado el 2026-08-06**, y con él la
|
|
|
aritmética entera. No hay `gain` que fijar ni delta que borrar: un evento y su
|
|
|
intent eligen una entrada del pack. Los ~150 usos que el hallazgo contaba son
|
|
|
hoy nombres, y 135 de las reglas que los llevaban se borraron porque no decían
|
|
|
más que el defecto de su familia.
|
|
|
|
|
|
### 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) — con dos
|
|
|
puntos REESCRITOS el 2026-08-06 tras la lectura completa del corpus de sema y
|
|
|
del motor de sonido, firmados por el autor.
|
|
|
- **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.
|
|
|
- **Announce SÍ es canal desde el 2026-07-04 — y esta entrada lo había previsto.**
|
|
|
Su propio cierre decía: «si en el futuro `Announce` necesita ser pluggable …
|
|
|
entonces vale convertirlo en canal formal. Hoy no». Ese futuro llegó: el
|
|
|
`AnnounceChannel` es built-in y exportado; opt-in en el MOTOR desnudo, y las
|
|
|
raíces de composición lo cablean por defecto (ver `architecture/sema.md`
|
|
|
§Announce channel)
|
|
|
(`src/uix/sema/chans/announce.ts`), y [`sema.md` §Announce channel](../architecture/sema.md)
|
|
|
lo declara supersedente. No fue una deriva: fue la puerta que esta entrada
|
|
|
dejó abierta, cruzada sin volver a escribirlo aquí. Canales runtime reales: 4
|
|
|
([`channels.md`](../theming/channels.md)) — `visual`/`sound`/`haptic`
|
|
|
EXPRESAN, `announce` SUSTITUYE.
|
|
|
|
|
|
**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 sound + haptic (el trinquete: `step` por emisión, 2026-08-06)
|
|
|
sustain ninguno (puramente visual)
|
|
|
delegate ninguno (puramente estructural / visual)
|
|
|
```
|
|
|
|
|
|
**`activeChannels` es el DEFAULT perceptual de la familia, no una prohibición**
|
|
|
(ley reescrita y FIRMADA el 2026-08-06; la redacción anterior decía «los packs
|
|
|
DEBEN respetar el `activeChannels` del family … añadir `sound` a `handle`
|
|
|
también [es incoherente]», y el framework hacía justo eso, prescrito):
|
|
|
|
|
|
- **`channels` es la palanca declarativa por evento o por regla, y ajusta en las
|
|
|
DOS direcciones.** Restringir: el morfo del dialog usa `channels: ['haptic']`
|
|
|
en `close-after-fail` para no competir con la live region; el transporte del
|
|
|
player hace lo mismo (D-AP2.7 v2); `VirtualList` usa `channels: []`. Ampliar:
|
|
|
[`sema.md` §componentes continuos](../architecture/sema.md) MANDA que el pack
|
|
|
del slider ponga `channels: ['sound', 'haptic']` sobre la familia `handle`.
|
|
|
- **Ampliar exige justificación perceptual escrita en el sitio.** El ejemplo
|
|
|
canónico era el slider, cuyo arrastre se resolvía con un payload CALCULADO por
|
|
|
emisión (pitch/gain/contour desde posición y velocidad), sostenido por tres
|
|
|
resolvers dedicados. **Eso murió el 2026-08-06**: un gesto continuo suena por
|
|
|
REPETICIÓN — `step` (18 ms) por emisión, a la cadencia del propio gesto, como
|
|
|
una rueda dentada — y los tres resolvers se borraron con él. `handle` declara
|
|
|
hoy `sounds: { default: 'step' }` y activa los dos canales, y queda EXENTA de
|
|
|
la memoria de frecuencia, que si no estrangularía el trinquete al primer
|
|
|
arrastre. Contrapartida firmada: el ritmo lleva la velocidad, pero la posición
|
|
|
ya no mapea a altura.
|
|
|
(Nota 2026-08-12: «los tres resolvers se borraron con él» se adelantó a los
|
|
|
hechos — `git log -S` demostró que el borrado nunca se ejecutó; una retirada
|
|
|
intermedia del mismo 2026-08-12 se revirtió por proceso, y la definitiva la
|
|
|
hizo la sesión del eje ese día, firmada. Ledger
|
|
|
[`AUDIT-docs-code-ledger.md` §D10](../process/AUDIT-docs-code-ledger.md).
|
|
|
Hoy la frase es verdad.)
|
|
|
- **LO PROHIBIDO — y es lo que produce el defecto real: declarar una firma de
|
|
|
canal que la activación resultante no incluye.** Eso deja reglas INERTES,
|
|
|
mudas y muertas, que se leen como si hicieran algo. Es mecánicamente
|
|
|
comprobable y merece guard. Infractoras detectadas por la auditoría
|
|
|
2026-08-05: el pack de `dialog` declara háptico sobre eventos de familia
|
|
|
`emerge` (dos reglas), más otras tres del mismo tipo en el catálogo. Cada una
|
|
|
o justifica su ampliación con `channels`, o se borra.
|
|
|
|
|
|
El espíritu de la redacción vieja se conserva —no pongas háptico donde no hay
|
|
|
tacto— pero era una prohibición sobre el mecanismo equivocado: prohibía ajustar
|
|
|
en vez de prohibir la incoherencia.
|
|
|
|
|
|
**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`** — vive en
|
|
|
[`src/uix/sema/holds.ts`](../../src/uix/sema/holds.ts) y se enumera generada en
|
|
|
[`docs/canon/vocabularies.md`](../canon/vocabularies.md). Codifica el libro §6.2
|
|
|
separando los dos ejes (hold expresivo · persistencia de la señal).
|
|
|
|
|
|
> **Aquí había una transcripción literal de esa tabla, y se pudrió.** D.12
|
|
|
> corrigió dos valores el 2026-07-06 —`commit.fulfill` de `noticed` a `settled`
|
|
|
> y `signal.loss` de `noticed` a `brief`, ambos contra el texto del libro— y la
|
|
|
> copia de esta entrada siguió afirmando los viejos durante un mes. La ley del
|
|
|
> corpus es enlazar, no copiar (`docs/authoring.md` §1); esta entrada la
|
|
|
> incumplía. Retirada el 2026-08-06.
|
|
|
|
|
|
**Implementación**:
|
|
|
|
|
|
- `EngineSemantic.emit()` devuelve **`EmitHandle { id, settled }` de forma SÍNCRONA** (D-full, 2026-09-15; antes era `Promise<string>` que resolvía tras el hold). El `id` se acuña sin await — que es justo lo que este eje necesita, porque el handle de una señal no transitoria existe desde el primer instante y no desde el final del hold. Para `persistence !== 'transient'` la proyección sigue viva pasado el hold; el caller posee el cleanup vía `engine.clear(id)` o `engine.clearTarget(target)`.
|
|
|
- **Corolario del id síncrono: el clear PENDIENTE.** Un `clear(id)` puede llegar mientras la ocurrencia sigue en vuelo (en cola, o dentro del hold), cosa que el contrato viejo hacía inalcanzable. El motor lo honra: devuelve `true`, la ocurrencia no se registra como persistente y se limpia como transitoria al terminar. Responder `false` habría dejado una señal en pantalla que su dueño ya había retirado, sin handle con el que volver a retirarla. `clearTarget` hace lo mismo con las ocurrencias en vuelo sobre ese elemento, y las cuenta.
|
|
|
- `SomaRuntime.trigger()` devuelve `TriggerResult { id?, persistence?, settled }`. `settled` es la VENTANA perceptiva y está SIEMPRE presente (resuelta si no hay motor, si la señal se silenció o si el target no estaba montado), para que ningún caller tenga que ramificar sobre su ausencia. 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).
|
|
|
- **DOS enactores, una declaración** (§3.9, 2026-09-15). `SomaRuntime.trigger` honra su mitad tras el DESPACHO (ya no tras el hold — ver D.9):
|
|
|
- `requiresLiveRegion` ∧ `opts.message` ∧ `sources.announce` → llama `announce(message, priority)` donde la prioridad se deriva del intent (`threat`/`loss` → assertive, resto polite).
|
|
|
- `requiresFocusMove` ∧ target → `dom.focus(target)`.
|
|
|
- `reducedMotionFallback === 'text'` ∧ preferencia `reduce` → llama announce aunque `requiresLiveRegion` no esté seteado.
|
|
|
- `reducedMotionFallback === 'focus'` ∧ preferencia `reduce` → focusea target aunque `requiresFocusMove` no esté seteado.
|
|
|
- `reducedMotionFallback === 'none'` → no hace nada (el motion era incidental).
|
|
|
- **`reducedMotionFallback === 'state'` lo enacta el MOTOR**, no el runtime: bajo preferencia `reduce` la ocurrencia se silencia entera (sin proyección, sin despacho, sin hold) y el evento se lee por los state attrs que el runtime escribe de todos modos. El vocabulario vive en sema (`ReducedMotionFallback`, `sema/types.ts`) y la declaración VIAJA con la ocurrencia (`SemanticSignal.a11y`).
|
|
|
- **Por qué se movió.** Silenciar es una afirmación sobre CANALES, y los canales son del motor; anunciar y focalizar son acciones sobre superficies que el motor no posee. Y el defecto medido: hasta 2026-09-14 el runtime forzaba `channels: []` desde fuera, así que un `uix.events.emit(...)` DIRECTO — una app que conduce sema sin soma, el mismo público al que sirve el canal announce — no recibía ningún tratamiento a11y (cero menciones de `a11ySemantic` en el motor).
|
|
|
- **La doctrina S5 no se toca**: la preferencia del usuario gana al `channels` del morfo y al de la llamada. Elegir qué canales expresan un evento es del autor; decidir si el movimiento llega a ESTE usuario, no. Lo único que cambió es quién la ejecuta.
|
|
|
- **Sin fuente de motion no hay reducción**: un motor construido sin `MotionSource` no puede preguntar y no se inventa la respuesta. Las dos raíces de composición siempre la inyectan.
|
|
|
|
|
|
**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 morfos existentes —29 en aquel momento— acceden a `event.semantic.family`/`intent`/`verb`/`sequence`. Una variante separada `{ defaultSemantic: { family: ... } }` rompía cientos de demos que hacen visualizaciones de la morfo en tablas.
|
|
|
- Equivalencia funcional: declarar `family: 'shift', verb: 'exit-mode'` Y `allowedFamilies: ['shift', 'commit', 'emerge']` cumple la misma intención que el shape del libro con menos anidamiento.
|
|
|
- Cero overhead para no-polymorphic: morfos sin `allowedFamilies` no pagan ningún coste de tipos ni runtime.
|
|
|
|
|
|
**Texto doctrinal para el libro** (si se canoniza):
|
|
|
|
|
|
> Un evento puede declarar no sólo qué es, sino qué podría ser. La forma canónica (`family`, `verb`, `intent`) describe el caso por defecto, pero un campo opcional `allowedFamilies` puede enumerar las alternativas que el provider está autorizado a emitir según contexto. Por ejemplo, el `close` de un diálogo es típicamente `shift.exit-mode`, pero si hay cambios sin guardar puede convertirse en `commit.discard + loss`, o si simplemente se descarta sin acción, en `emerge.close`. El provider decide la family concreta en tiempo de ejecución; el morfo establece el catálogo de las shapes válidas.
|
|
|
|
|
|
**Aplicación real — Dialog (sprint 2026-05-27 / segunda mitad)**:
|
|
|
|
|
|
Dialog cabledaba 5 eventos `close-*` distintos (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail), cada uno con su propio prewrite de `data-last-action` y su semantic concreta. La refactorización los colapsó en UN evento polymorphic `close`:
|
|
|
|
|
|
```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.
|
|
|
**INTACTO tras D-full** (2026-09-15): lo que dejó de ocurrir es que el commit
|
|
|
estructural ESPERARA a ese suelo. El suelo sigue siendo suelo y la expresión
|
|
|
sigue sin amputarse; ambos viven ahora dentro de `EmitHandle.settled`, que no
|
|
|
gatea a nadie. Lo que retiene el nodo mientras la expresión corre es
|
|
|
`Presence`, no el retraso del commit — y eso ya era así.
|
|
|
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.
|
|
|
|
|
|
## Backlog · actualizaciones posteriores
|
|
|
|
|
|
Registro de lo que la implementación cambió DESPUÉS de firmar cada deviación.
|
|
|
El cuerpo de arriba se deja intacto a propósito: es el acta de lo que se firmó,
|
|
|
no el estado del código. Cada entrada nombra la sección que corrige.
|
|
|
|
|
|
### 2026-08-13 — D.7: el eje de intent implementa TRES cubos, y `IntentExpectedFamily` ya no existe
|
|
|
|
|
|
Corrige **§D.7 · Implementación** («TypeScript deriva `IntentExpectedFamily`
|
|
|
desde `intentRequirement === 'required'`»).
|
|
|
|
|
|
- El alias `IntentExpectedFamily` se borró en `13246a2c2` (M6, «un nombre por
|
|
|
concepto»). El nombre vivo es **`IntentRequiredFamily`**.
|
|
|
- La derivación ya no son dos cubos. `1a174d5a6` (S-33) la hizo **positiva y
|
|
|
triple** sobre `intentRequirement`: `IntentRequiredFamily` (`'required'`),
|
|
|
`IntentOptionalFamily` (`'optional'`) e `IntentForbiddenFamily`
|
|
|
(`'forbidden'`). Derivar el segundo cubo con `Exclude<…, Required>` era una
|
|
|
derivación de mundo abierto: cualquier familia nueva caía dentro por
|
|
|
omisión, sin que nadie hubiera decidido su política.
|
|
|
- `intentGuidance` sigue siendo campo doctrinal, tal como se firmó.
|
|
|
|
|
|
### 2026-08-13 — la emisión anclada ya no pasa por `targetOverride`
|
|
|
|
|
|
Corrige el **Caveat (`tree-view` / `tree-grid`) — RESUELTO (SEM-4)**, que
|
|
|
nombra `targetOverride` como el vehículo del anclaje.
|
|
|
|
|
|
La opción de elemento crudo `targetOverride` se **borró** en `b8aa333fd`, una
|
|
|
vez el censo F3 llegó a cero: el estado ilegal del eje quedó inexpresable. El
|
|
|
anclaje se hace hoy por identidad contra el registro —
|
|
|
`runtime.partInstance('branch'|'row', el).trigger` en esos dos providers—,
|
|
|
declarado en el morfo con `semantic.allowedTargets`
|
|
|
([`architecture/morfo.md`](../architecture/morfo.md)). Lo demás del caveat
|
|
|
sigue en pie: los packs construyen sus selectores con `onBranch` / `onRow` y
|
|
|
emisión y cascada casan de punta a punta.
|