refactor(sema/docs): apply author's evaluation — status classification + intentRequirement/intentGuidance split

Two changes in one commit:

1. **Doctrina del autor sobre el documento de variaciones**
   The author evaluated LIBRO_VARIACIONES_Y_EXTENSIONES.md and gave per-
   entry verdicts. The doc is now rebranded, restructured, and classified:
   - Renamed: "Registro de desviaciones entre implementación y canon
     editorial". Disclaimer at top: not the book, internal registry only.
   - Formal status classification (BOOK_CANON / PROJECT_CANON / CANDIDATE
     / LOCAL_EXTENSION / DEPRECATED / ALIAS / IMPLEMENTATION_CONTRACT).
   - Per-entry verdicts assigned per author's evaluation. Author's
     doctrinal texts included verbatim where given.
   - A.6 split per verb (was a single bullet for apply/partial/block/
     move/upload; now each has its own status: apply=BOOK_CANON,
     move=BOOK_CANON, upload/partial/block=CANDIDATE).
   - A.6/A.7 ordering fixed (commit.unselect now A.11 — terminal).
   - C.3 split into C.3.a (renames mecánicos) y C.3.b (correcciones
     doctrinales — command intent shift).
   - E (resumen ejecutivo) actualizado: clear/unselect movidos a
     resueltos (estaban contradiciéndose). Pendientes reales listados.
   - F + new section G ("Anti-mezclas") con disciplina sobre el doc.

2. **Family policy: split `intentPolicy` en `intentRequirement` +
   `intentGuidance`** (D.3 del doc).
   La policy actual `'allowed' | 'expected' | 'optional'` mezclaba dos
   ejes que el autor pidió separar:
   - `intentRequirement: 'required' | 'optional' | 'forbidden'` —
     compile-time type constraint.
   - `intentGuidance: 'expected' | 'contextual' | 'discouraged'` —
     guía doctrinal sin efecto en tipos.

   Policy nueva:
   | family   | requirement | guidance     |
   |----------|-------------|--------------|
   | contact  | optional    | discouraged  |  (Cap 22 §11)
   | commit   | required    | expected     |
   | signal   | required    | expected     |
   | handle   | optional    | contextual   |
   | emerge   | optional    | contextual   |
   | shift    | optional    | contextual   |
   | sustain  | optional    | contextual   |
   | delegate | optional    | contextual   |  (Cap 29 §4)

   `IntentExpectedFamily` deprecated → `IntentRequiredFamily` (alias
   mantenido). `IntentPolicy` type también deprecated.

   Consumers actualizados: event.ts (isSemaEvent usa requirement),
   validation.ts (validateSemaEvent usa requirement), event.test.ts
   (tests usan ambos campos), dialog.ts (comment doc update).

3. **Bug menor encontrado en `SEMA_TRANSITIONAL_FAMILIES`**: faltaba
   `delegate`. Añadido. (Era inconsistente con SemaTransitionalFamily
   type que sí lo incluye.)

Verification:
- vitest src/uix/sema src/uix/morfo: 195/195 pass
- morfo:vocabulary: 7 warns (todos words/*, separate dev track)
- EXIT 0

Pendiente futuro (no en este commit):
- Añadir flag `emission` a MorfoEvent type per D.2 (eventos declarados
  pero no emitidos)
- Re-evaluar verbos CANDIDATE (acknowledge/confirm/upload/partial/
  block) cuando aparezcan más casos de uso

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent bbfefdb63d
commit 00f0b740e6

@ -1,212 +1,240 @@
# Variaciones del canon UIX respecto al libro
# Registro de desviaciones entre implementación y canon editorial
> Documento vivo. Captura las decisiones del proyecto UIX que **se desvían**, **extienden**, o **interpretan** el libro «Diseñando lo que ocurre — Una gramática de eventos para interfaces de usuario» en su lectura literal.
> **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.
>
> Propósito: facilitar al autor decidir qué casos merecen pasar al libro como ampliación, qué deben quedar como extensión local del proyecto, y qué deberían replantearse en uno o en otro.
> **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.
>
> Cada sección sigue un patrón: **Status** del cambio, **Lectura literal del libro**, **Decisión tomada**, **Argumento**, **Decisión pendiente** (lo que el libro podría aclarar/incorporar).
> **No todo lo que aparece en implementación debe volver al libro.**
---
## A. Verbos añadidos al canon (extensiones de vocabulario)
## Clasificación de status
Estos verbos están en `src/uix/sema/verbs.ts:SEMA_VERBS` pero NO aparecen en las listas canónicas del libro.
| 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**: Adoptado pre-Plan B, mantenido y usado activamente en Capa 2 (virtual-list, virtual-grid).
- **Libro Cap 25 §1** lista 7 verbos para `handle`: `pick, carry, drop, drag, resize, rotate, reorder`. **`scroll` no aparece.**
- **Decisión**: añadido a canon como verbo válido de `handle`.
- **Argumento**:
- Cap 25 §1: "Handle es la familia semántica de la **manipulación directa**... una relación continua entre el usuario y un objeto."
- Cap 8 §1: "no todo debe ser familia. Algunas diferencias pertenecen al verbo".
- Scrollear un viewport (gesture continuo, control directo, respuesta inmediata) cumple las propiedades canónicas de handle (Cap 25 §3): responde inmediatamente, sigue el gesto, conserva identidad, comportamiento coherente.
- **Decisión pendiente para el libro**:
- (a) Incluir `scroll` en los ejemplos de verbos de handle (Cap 25 §1).
- (b) Dejar fuera y considerar scroll un verbo de "extensión local" sin estatuto canónico.
- (c) Crear nueva sub-familia (`handle.navigate` con propiedades scroll-específicas).
- **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**: Heredado del canon pre-Plan B. Sin uso activo en morfos hoy.
- **Libro Cap 23 §5** lista 12 verbos de commit: `select, toggle, save, submit, complete, fail, delete, restore, cancel, reset, expire, discard`. **`acknowledge` no aparece.**
- **Decisión**: mantenido en canon.
- **Argumento (especulativo)**: cerrar una decisión pendiente sin afirmar ni rechazar (ej. "got it" en onboarding, "I understand" en un disclaimer). Solapa parcialmente con `cancel` y `submit`.
- **Decisión pendiente**:
- (a) ¿Es un caso doctrinalmente distinto? ¿Tiene su propia firma perceptiva (menor activación que submit, sin la reversibilidad de cancel)?
- (b) ¿O es un sinónimo redundante? Si es así, removerlo del canon.
### A.3 `commit.remove` vs `commit.delete`
- **Status**: `remove` heredado del canon pre-libro. Tras la resolución de C.2 (unselect = `commit.unselect`, no `remove`), el verbo `remove` queda **mucho menos usado** — su nicho ahora es solamente "quitar de una colección sin destruir" (ej. removing un item de una lista de favoritos sin eliminarlo del catálogo).
- **Libro Cap 23 §5** lista `delete` (destrucción consumada). **`remove` no aparece.**
- **Decisión doctrinal del autor (2026-05-25)** — al resolver C.2:
- `remove` queda para retirada, eliminación o salida de colección — DISTINTO de unselect (cambio de estado de selección).
- `delete` queda para destrucción persistente.
- `unselect` queda para estado de selección.
- Los tres verbos son distintos doctrinalmente.
- **Decisión pendiente para el libro**:
- Formalizar `remove` como verbo distinto de `delete` y de `unselect`, con su propia firma perceptiva (retirada de colección, sin loss intent).
- **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**: Heredado, sin uso activo.
- **Libro Cap 23 §5** lista `submit, complete`. **`confirm` no aparece.**
- **Decisión**: mantenido en canon.
- **Argumento**: posible solapamiento con submit. "Confirmar una acción" puede no ser lo mismo que "submitear un formulario".
- **Decisión pendiente**:
- (a) Mantener si tiene nicho propio (confirmación de diálogo destructivo distinta del submit de formulario).
- (b) Eliminar del canon como redundante con submit/complete.
- **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**: Usado activamente en Capa 2 (table.sort, virtual-list.resize, virtual-grid.resize).
- **Libro Cap 23 §5** lista 12 verbos, `set` no entre ellos. Pero Cap 25 §7 da el ejemplo: `handle.drag → commit.set + affirm` (slider).
- **Decisión**: `set` es canónico del proyecto, usado para "aplicar un valor".
- **Argumento**: el libro lo usa implícitamente (Cap 25 ejemplo) pero no lo formaliza. Hay decenas de casos donde "el usuario fijó un valor" sin caer en save/submit/complete (sliders, sort criteria, color channels, picker selections).
- **Decisión pendiente**:
- (a) Formalizar `set` en Cap 23 §5 como verbo canónico (su nicho: aplicar un valor sin que sea persistencia/submit/completion).
- (b) Mapear set a `apply` (también en uso para delegate.act).
### A.7 `commit.unselect` (decisión doctrinal del autor, 2026-05-25)
- **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**:
- **Status**: Añadido al canon como verbo de `commit`. Resuelve C.2 (cluster unselect, 6 componentes).
- **Libro Cap 23 §5** lista `select` pero no `unselect`.
- **Decisión doctrinal del autor** (transcrita literal en C.2):
> `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).
> Seleccionar y deseleccionar son resultados aplicados sobre el estado de selección de un elemento. Cuando el usuario selecciona, **este item queda seleccionado**. Cuando deselecciona, **este item deja de estar seleccionado**. Eso es `commit`, porque el resultado queda aplicado.
>
> Diferenciación con verbos cercanos:
> - No es `remove` (no se elimina ni se saca de colección, solo cambia estado de selección).
> - No es `toggle` (toggle describe el mecanismo binario; unselect el resultado semántico concreto).
>
> El par natural sobre items seleccionables es `commit.select` / `commit.unselect`. Independiente de modo single/multi.
### A.6 `commit.apply`
- **Para el libro**: añadir `unselect` a Cap 23 §5 como verbo canónico. El par `select`/`unselect` cierra la simetría sobre estado de selección de items, distinto de `toggle` (control binario) y `remove` (colección).
- **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**:
### A.6 `commit.apply / partial / block / move / upload`
> `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.
- **Status**: añadidos en Plan B commit 1 desde el libro mismo.
- **Libro**: estos verbs aparecen en casos de estudio (Cap 29 delegate, Cap 30 composición, Apéndice C). No estaban en la lista canónica de Cap 23 §5.
- **Decisión**: incluidos en el canon como verbos commit.
- **Argumento**: el libro los usa como verbos canónicos en ejemplos; sólo faltan en la lista enumerada.
- **Decisión pendiente para el libro**:
- **Recomendado**: incluir explícitamente estos verbos en la lista de Cap 23 §5 para que sean discoverables. El libro ya los usa, sólo no los lista.
### 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.
## B. Adopciones por interpretación (no literales en el libro)
### B.1 Cancelación de drag como `commit.cancel`
- **Status**: Aplicado en Cluster 3 (drag-drop.commit-cancel).
- **Libro Cap 25 §4** cubre fases pick → carry → drop. **No aborda explícitamente qué pasa si el usuario presiona Escape durante carry.**
- **Decisión**: cancelar un drag = `commit.cancel + neutral`.
- **Argumento**: el usuario inició una operación (pick) y la revoca antes de consecuencia. Per Cap 23, `commit.cancel` cubre "el usuario revoca". El drag nunca llegó a su outcome.
- **Decisión pendiente para el libro**:
- (a) Añadir caso de estudio o párrafo en Cap 25 sobre cancelación de drag/carry.
- (b) Reconocer que `commit.cancel` puede componerse con `handle.*` cuando una manipulación se aborta.
### B.2 Scroll programático como `shift.navigate`
- **Status**: Aplicado en Cluster 2 (virtual-list.shift-navigate-to-index, virtual-grid.shift-navigate-to-cell).
- **Libro Cap 27 §5** lista `shift.navigate` con ejemplos: "Lista → detalle. Página A → página B." — escala de "lugar" o pantalla completa.
- **Decisión**: cuando el sistema mueve el viewport a una posición específica (programáticamente, no por gesto del usuario), `shift.navigate`.
- **Argumento**: la lectura perceptiva es "estoy en otro lugar de la lista" (Cap 27). No es handle (no hay control directo). No es emerge (la lista no aparece, solo cambia su offset). La escala es menor (intra-lista vs. inter-página) pero la estructura es la misma.
- **Decisión pendiente para el libro**:
- (a) Aclarar que `shift.navigate` cubre cambios de posición a CUALQUIER escala (intra-lista, inter-vista, inter-pantalla).
- (b) O introducir un sub-verbo (`shift.scrollto`, `shift.position`) para el caso intra-lista programático.
### B.3 Cambio de tamaño del modelo de datos como `commit.set`
- **Status**: Aplicado en Cluster 2 (virtual-list.commit-set-resize, virtual-grid.commit-set-resize). Fires cuando `count`/`rowCount`/`columnCount` cambia y el viewport recomputa.
- **Libro**: NO cubre eventos auto-disparados por cambio interno del modelo de datos del componente (sin acción del usuario, sin resultado de proceso explícito).
- **Decisión**: el sistema "set" un nuevo tamaño → `commit.set + neutral`.
- **Argumento**: útil como sema hook para "nuevos items disponibles" cues. Pero es un stretch — no hay un actor humano ni un proceso terminando; sólo state interno cambió.
- **Decisión pendiente para el libro**:
- (a) Reconocer que ciertos eventos auto-disparados (data changed, model recomputed) pueden necesitar estatuto sema para ser sonificables.
- (b) O establecer doctrinalmente que estos NO son eventos significativos y deben quedar como state-only (data-attr, sin sema event).
### B.4 Sort de tabla como `commit.set`
- **Status**: Aplicado en Cluster 6 (table.commit-set-sort).
- **Libro**: NO tiene caso de estudio de sort de columnas.
- **Decisión**: ordenar = fijar un criterio de orden → `commit.set + neutral`.
- **Argumento**: el usuario fija una axis/dirección de ordenamiento. El reorder visual es consecuencia. `commit.reorder` no encaja porque no es reordenamiento manual de items individuales; es aplicar un criterio.
- **Decisión pendiente para el libro**:
- (a) Añadir caso de estudio en Cap 30 o Cap 23: "sort = commit.set; reorder manual = handle.reorder + commit.reorder".
- (b) Distinguir formalmente sort-by-criterion vs manual-reorder.
### B.5 Eliminación de eventos sin perceptibilidad (clipboard timer reset)
- **Status**: Aplicado en Cluster 7 (clipboard `shift-reset` eliminado).
- **Libro Cap 4 §1**: "Un evento no es cualquier cambio perceptible. Es un cambio perceptible **con relevancia operativa**."
- **Decisión**: el timer interno que revierte `copied=false` tras N ms NO es evento sema. Removido del morfo.
- **Argumento**: el cambio visible (label "Copied" → "Copy") se comunica vía `data-copied` (atributo de estado), no necesita evento. Sin acción del usuario, sin consecuencia significativa.
- **Decisión pendiente para el libro**:
- **Recomendado**: añadir nota en Cap 4 sobre "estados que vuelven automáticamente sin necesidad de evento sema". Hoy se infiere pero no se explicita.
### B.6 Toggle con dos eventos (check vs uncheck)
- **Status**: Aplicado en Cluster 5 (checkbox), **decisión explícita del usuario** sobre dos opciones (A: un evento, B: dos eventos).
- **Libro Cap 22 §10** ejemplo literal: "Toggle: contact.press → **commit.toggle + affirm**." — UN evento.
- **Decisión**: DOS eventos `commit-toggle-check` (intent `affirm`) y `commit-toggle-uncheck` (intent `neutral`). Mismo verbo canónico, intents distintos según dirección.
- **Argumento del usuario (Plan B Capa 2)**: la diferenciación perceptiva entre check y uncheck importa operativamente — check anuncia inclusión (affirm), uncheck revoca sin carga (neutral). El verbo se mantiene canónico (`toggle`); la diferencia vive en el intent.
- **Decisión pendiente para el libro**:
- (a) Reconocer en Cap 22 §10 que un toggle puede tener dos fases con intents distintos (check=affirm, uncheck=neutral) sin perder canonicidad.
- (b) Mantener "un solo evento" como dogma; la dirección se comunica por `data-state`. (Sería contradecir la decisión del proyecto.)
### B.7 Intent en `contact` solo como anticipación visual
- **Status**: Aplicado en Plan B commit 2 (Button).
- **Libro Cap 22 §11**: "Normalmente: `contact + neutral` o incluso sin intent explícito. En acciones graves, la interfaz puede anticipar el peso mediante **forma, color o señal previa**, pero el intent fuerte no debería vivir en el contacto."
- **Decisión**: el `intent` prop del Button NO entra en el morfo event (`contact-activate` no lleva intent). Vive solo en `data-color` y la variante visual del chip.
- **Argumento**: lectura estricta del libro. La anticipación de peso es visual; el sonido/háptica del click debe ser uniforme. La diferenciación perceptual entre save y delete vive en los eventos commit/signal DOWNSTREAM, no en el click del Button.
- **Decisión pendiente para el libro**:
- **Tensión real**: el libro lista `contact: intentPolicy 'allowed'` (puede declarar) pero el spirit es "normalmente no". Nuestra implementación (Cap 22 §11 estricta) puede generar regresión perceptiva si los consumidores no cablean los downstream events. ¿El libro debe dar guía explícita sobre cómo el sistema completo (no sólo el Button) suple la diferenciació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.
## C. Decisiones doctrinales pendientes (Capa 3, no resueltas)
### A.9 `commit.partial`
Hoy ~35 events siguen como warns en `morfo:vocabulary` porque el verbo declarado es canónico pero el `name` no parsea a `{family}-{verb}`. Detrás de los renames cosméticos hay PREGUNTAS DOCTRINALES:
- **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**.
### C.1 "Clear" como concepto — RESUELTO como `commit.reset`
### A.10 `commit.block`
- **Status**: Resuelto por el autor el 2026-05-25. Los 10 componentes ahora emiten `commit-reset` con `verb: 'reset'` canónico del libro.
- **10 componentes** afectados: color-picker, date-field, date-picker, date-range-picker, file-upload, range-calendar, tag-group, tags-input, time-picker, time-range-picker.
- **Libro Cap 23 §5** lista `reset` como verbo canónico ("vuelve a estado inicial"). Encaja para todos los casos del cluster: cada uno está volviendo a estado vacío inicial (no defaults significativos que se descartan).
- **Decisión tomada**: `commit.reset` con name canónico `commit-reset` (sin variant — no hay ambigüedad de múltiples resets en ningún componente del cluster). Verbo `'remove'` mal-declarado en range-calendar también corregido a `'reset'`.
- **Decisión pendiente para el libro**: ninguna. La doctrina del libro literal cubre exactamente el caso. Se podría añadir mención explícita de "clear de un campo" como ejemplo de `commit.reset` en una próxima edición.
- **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**.
### C.2 "Unselect" como concepto — RESUELTO añadiendo `unselect` al canon
### A.11 `commit.unselect`
- **Status**: Resuelto por el autor el 2026-05-25. `unselect` añadido a `SEMA_VERBS.commit` como verbo canónico. Los 6 morfos ahora declaran `verb: 'unselect'`, `intent: 'affirm'`.
- **6 componentes** afectados: calendar, combobox, grid-list, listbox, select, tag-group.
- **Decisión doctrinal del autor** (transcrita literal):
- **Status**: **BOOK_CANON**
- **Libro Cap 23 §5** lista `select` pero no `unselect`. Pasa el par natural al canon.
- **Decisión del autor (transcrita literal)**:
> `select` y `unselect` son resultados aplicados sobre el estado de selección de un elemento. Cuando el usuario selecciona, **este item queda seleccionado**. Cuando deselecciona, **este item deja de estar seleccionado**. Eso es `commit`, porque el resultado queda aplicado.
>
> - No es `contact`, porque no solo se ha recibido el gesto.
> - No es `handle`, porque no hay manipulación continua.
> - No es `remove`, porque 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`, porque `toggle` describe mejor el mecanismo binario o el control, pero no expresa tan bien el resultado semántico concreto.
> 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 (switch, checkbox como control)
> - `toggle` → inversión binaria genérica
> - `remove` → retirada, eliminación o salida de colección
>
> Independiente de si la lista es single-select o multi-select. En ambos casos, la lectura del usuario es que el item deja de estar seleccionado.
- **Intent**: `affirm` (la acción se aplicó correctamente, sin celebración).
- **Name**: `commit-unselect` se mantiene (el name ya parsea canónico ahora que `unselect` está en `SEMA_VERBS.commit`).
- **Para el libro**: añadir `unselect` a la lista de verbos canónicos de `commit` en Cap 23 §5. El par `select`/`unselect` es el natural sobre estado de selección de items.
- **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.
### C.3 Otros nombres no canónicos — RESUELTO
- **Ejemplo de aplicación**: timer interno que revierte `copied=false` tras N ms en clipboard. Es estado (`data-copied`), no evento sema.
- **Status**: Cerrado 2026-05-25. 12 events renombrados a `{family}-{verb}-{variant}` canónico.
- **Renames mecánicos** (verbo declarado canónico, solo name):
### 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`
@ -218,90 +246,125 @@ Hoy ~35 events siguen como warns en `morfo:vocabulary` porque el verbo declarado
- `range-calendar.commit-range` → `commit-select-range`
- `tags-input.commit-add` → `commit-set-add`
- `tags-input.signal-reject` → `signal-warn-reject`
- **Una corrección doctrinal**: `command.commit-invoke` (declared `submit + fulfill`) → `commit-submit-invoke + submit + affirm`. Cambio de intent: `fulfill` → `affirm` per Cap 22 §8 — el comando aún no se ha ejecutado al click, celebrar al click es antipatrón (mismo precedent que Button). Verb `submit` mantenido (el usuario envía su elección al sistema de comandos).
- **Consumers actualizados**: sema cascades + soma providers + resolver.test.ts.
### 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 globales (relevantes para el libro)
## D. Decisiones arquitecturales
### D.1 Composición vs sobrecarga de un único morfo
### D.1 Cada actor declara sus eventos
- **Aprendizaje de Plan B commit 2 (Button)**: cada evento vive en SU morfo, no en otro. El Button no declara `commit.delete + loss` porque el Button no hace el delete.
- **Implicación**: la diferenciación perceptiva en flujos complejos requiere MÚLTIPLES morfos cada uno con sus eventos canónicos.
- **Decisión pendiente para el libro**:
- **Recomendado**: el libro Cap 30 (composición) podría dedicar una sección al patrón arquitectural "cada actor declara sus eventos". Hoy se infiere de Cap 30 pero no se nombra explícitamente.
- **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
- **Hallazgo en Capa 2**: muchos morfos declaran events que la capa soma NUNCA dispara (announce, tree-view, tree-grid, drag-drop, virtual-list, virtual-grid, clipboard tenían/tienen events morfo-only sin emisor en soma).
- **Implicación**: el morfo es **declaración de POSIBILIDAD del contrato semántico**, no requisito de que la implementación lo emita. Permite a sistemas externos (testing, analytics, accesibilidad) suscribirse a eventos que la implementación canónica puede no fire.
- **Decisión pendiente para el libro**:
- (a) Establecer que un morfo puede declarar eventos no implementados (forward compatibility, contract surface).
- (b) O exigir que todo evento declarado tenga emisor (cuidado: 7 componentes hoy declaran eventos sin emitirlos).
### D.3 Family policy "allowed" vs "normalmente"
- **Libro Cap 22 §11**: "**Normalmente**: contact + neutral o incluso sin intent explícito."
- **Código `SEMA_FAMILY_POLICY`** tiene `contact: 'allowed'` (puede llevar intent libremente).
- **Tensión**: "normalmente" del libro implica "preferiblemente no", pero `'allowed'` no comunica eso. El Button terminó NO llevando intent — alineado con el libro pero no con la policy.
- **Decisión pendiente**:
- (a) Añadir nivel intermedio en `SEMA_FAMILY_POLICY`: `'discouraged'` (puede pero no recomendado) — más fiel al libro.
- (b) El libro podría explicitar este nivel en Cap 22 (cuándo puede usarse y cuándo se considera mala práctica).
### D.4 Tabla de adopciones por morfo (resumen)
| Componente | Verbos del libro usados | Verbos extendidos | Decisiones interpretativas |
|---|---|---|---|
| Button | `contact.activate` | — | intent prop visual-only (B.7) |
| Checkbox | `commit.toggle` | — | 2 eventos vs 1 (B.6) |
| Announce | `signal.announce`, `signal.alert`, `commit.reset` | — | morfo declara, soma no emite (D.2) |
| Tree-view/grid | `emerge.expand/collapse` | — | morfo declara, soma no emite (D.2) |
| Drag-drop | `handle.pick`, `handle.drop`, `commit.cancel` | — | drag cancellation = commit.cancel (B.1) |
| Table | `emerge.expand`, `commit.set` | — | sort = commit.set (B.4) |
| Virtual-list/grid | `commit.set`, `shift.navigate` | `handle.scroll` (A.1) | programmatic scroll = shift (B.2), data resize = commit.set (B.3) |
| Clipboard | `commit.save`, `commit.fail` | — | timer-reset eliminado (B.5) |
| Month-grid / Year-grid | `shift.navigate`, `commit.set` | — | — |
| Password-field | `commit.toggle`, `signal.notify` | — | — |
| Textarea | `signal.warn`, `commit.submit` | — | — |
- **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.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.
---
## E. Resumen ejecutivo de cambios al canon respecto al libro literal
## 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 añadidos al canon respecto a las listas literales del libro**:
- `handle.scroll` (A.1) — extensión justificada por spirit
- `commit.acknowledge` (A.2) — heredado, status incierto
- `commit.remove` (A.3) — distinción de `delete`
- `commit.confirm` (A.4) — heredado, posible redundancia
- `commit.set` (A.5) — el libro lo usa pero no lo lista
- `commit.apply / partial / block / move / upload` (A.6) — el libro los usa en ejemplos
**Composiciones interpretadas no explícitas en el libro**:
- Drag cancellation (B.1)
- Scroll programático intra-lista (B.2)
- Data size auto-change (B.3)
- Sort de columnas (B.4)
- Estados que vuelven solos sin evento (B.5)
- Toggle con dos eventos por dirección (B.6)
- Intent en contact estrictamente visual (B.7)
**Pendientes doctrinales**:
- "Clear" (C.1) y "Unselect" (C.2) cubren 16 morfos sin verbo canónico claro
### 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)
---
## F. Cómo proceder
Para cada bullet arriba el autor del libro puede:
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:
1. **Aceptar como ampliación al libro**: pasar la decisión a una próxima edición/anexo.
2. **Rechazar y revertir en el proyecto**: el canon del proyecto debe alinear con el libro, no al revés.
3. **Reformular**: la decisión necesita un matiz distinto.
4. **Dejar como extensión local**: el proyecto puede tener vocabulario específico que no escala al libro.
- **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.
El proyecto seguirá la disposición del autor en cada caso. Hasta entonces, este documento es la fuente de verdad sobre dónde el proyecto se ha desviado del libro literal.
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.

@ -66,7 +66,7 @@ export const dialogMorfo = {
{
// User backed out — they did NOT execute the dialog's action.
// Intent is the evaluative load of the action; cancelling carries
// none. Leaving `intent` absent (emerge has `intentPolicy: 'optional'`)
// none. Leaving `intent` absent (emerge has `intentRequirement: 'optional'`)
// makes the perceptual signature neutral, so the `intent="threat"`
// dialog doesn't perceptually fire-alarm when the user opts out.
name: 'close-cancel',

@ -58,25 +58,27 @@ describe('isSemaEventLabel', () => {
});
describe('isSemaEvent — policy-driven', () => {
it('requires intent on `expected` families (commit, signal)', () => {
it('requires intent on `required` families (commit, signal) with `expected` guidance', () => {
for (const family of ['commit', 'signal'] as const) {
expect(SEMA_FAMILY_POLICY[family].intentPolicy).toBe('expected');
expect(SEMA_FAMILY_POLICY[family].intentRequirement).toBe('required');
expect(SEMA_FAMILY_POLICY[family].intentGuidance).toBe('expected');
expect(isSemaEvent({ family })).toBe(false);
expect(isSemaEvent({ family, intent: 'fulfill' })).toBe(true);
}
});
it('allows intent absence on `allowed` families (contact, handle)', () => {
for (const family of ['contact', 'handle'] as const) {
expect(SEMA_FAMILY_POLICY[family].intentPolicy).toBe('allowed');
expect(isSemaEvent({ family })).toBe(true);
expect(isSemaEvent({ family, intent: 'risk' })).toBe(true);
}
it('allows intent absence on `optional` families with `discouraged` guidance (contact)', () => {
// Cap 22 §11: el intent fuerte no debería vivir en el contacto.
expect(SEMA_FAMILY_POLICY.contact.intentRequirement).toBe('optional');
expect(SEMA_FAMILY_POLICY.contact.intentGuidance).toBe('discouraged');
expect(isSemaEvent({ family: 'contact' })).toBe(true);
expect(isSemaEvent({ family: 'contact', intent: 'risk' })).toBe(true);
});
it('allows intent on `optional` families (emerge, shift, sustain) — the post-canon fix', () => {
for (const family of ['emerge', 'shift', 'sustain'] as const) {
expect(SEMA_FAMILY_POLICY[family].intentPolicy).toBe('optional');
it('allows intent on `optional` families with `contextual` guidance (handle/emerge/shift/sustain/delegate)', () => {
for (const family of ['handle', 'emerge', 'shift', 'sustain', 'delegate'] as const) {
expect(SEMA_FAMILY_POLICY[family].intentRequirement).toBe('optional');
expect(SEMA_FAMILY_POLICY[family].intentGuidance).toBe('contextual');
expect(isSemaEvent({ family })).toBe(true);
expect(isSemaEvent({ family, intent: 'threat' })).toBe(true);
}

@ -21,7 +21,8 @@ export const SEMA_VALENCED_FAMILIES = [
export const SEMA_TRANSITIONAL_FAMILIES = [
'emerge',
'shift',
'sustain'
'sustain',
'delegate'
] as const satisfies readonly SemaTransitionalFamily[]
export const SEMA_FAMILIES = [
@ -98,11 +99,11 @@ export function isIntentBinding(value: unknown): value is IntentBinding {
export function isSemaEvent(value: unknown): value is SemaEvent {
if (!isRecord(value) || !isSemaFamily(value.family)) return false
const policy = SEMA_FAMILY_POLICY[value.family].intentPolicy
const requirement = SEMA_FAMILY_POLICY[value.family].intentRequirement
const intentValue = 'intent' in value ? value.intent : undefined
if (intentValue === undefined) {
return policy !== 'expected'
return requirement !== 'required'
}
return isIntent(intentValue) || isIntentBinding(intentValue)

@ -18,62 +18,95 @@ export type SemaFamily = SemaValencedFamily | SemaTransitionalFamily;
// ── Family policy ──────────────────────────────────────────────────────────
/**
* Whether `intent` is required at the type level for a given family.
* Drives compile-time enforcement of `MorfoEventSemantic.intent`.
*/
export type IntentRequirement = 'required' | 'optional' | 'forbidden';
/**
* Doctrinal guidance about whether using `intent` on a family is the norm,
* context-dependent, or actively discouraged. Does NOT affect the type —
* lives as documentation + possible lint signal in the future. Separating
* this from `intentRequirement` lets TypeScript express the strict rule
* without "lying" about the book's softer guidance (e.g. contact MAY take
* intent at the type level, but `contact + threat` is doctrinally
* discouraged per book Cap 22 §11).
*/
export type IntentGuidance = 'expected' | 'contextual' | 'discouraged';
/**
* @deprecated Legacy single-axis policy. Use `intentRequirement` +
* `intentGuidance` on `SEMA_FAMILY_POLICY` entries. Kept as a type alias
* for any external consumers transitioning off the old shape.
*/
export type IntentPolicy = 'allowed' | 'expected' | 'optional';
/**
* Per-family doctrinal policy. Today carries `intentPolicy` only;
* structured as an object so future fields (sequence default, allowed
* channels, hold preferences, gesture phases, …) live alongside without
* restructuring consumers.
* Per-family doctrinal policy. Two axes:
*
* intentPolicy:
* - `'expected'` — intent MUST be declared (compile error if
* missing). Used for families whose perceptual
* signature is fundamentally evaluative: commit
* (consummating a decision) and signal (alarm).
* - `'allowed'` — intent MAY be declared. Useful when the consumer
* wants to load the event affectively but the
* family can also be neutral. contact (any press),
* handle (any drag/scrub) are like this — their
* default is colourless, but a `delete-handle`
* can be marked `'risk'`.
* - `'optional'` — intent is unusual but not forbidden.
* Transitional families (emerge/shift/sustain)
* are like this: the default appearance is
* neutral, but a Dialog opening to confirm a
* destructive action carries threat in its very
* emergence.
* `intentRequirement` (compile-time type constraint):
* - `'required'` — intent MUST be declared (compile error if missing).
* - `'optional'` — intent MAY be declared.
* - `'forbidden'` — intent MUST NOT be declared. (Reserved; no family
* uses this today, but contact may move here if the
* book hardens its prohibition.)
*
* The types `SemaEvent` and `MorfoEventSemantic` derive from this map:
* families with `intentPolicy: 'expected'` REQUIRE `intent` at compile
* time; everything else makes it optional. Edit this const to refine
* the doctrine without rewriting every consumer.
* `intentGuidance` (doctrinal guidance, no type effect):
* - `'expected'` — using intent is the norm; absence is the
* exception. Applies to families whose perceptual
* signature is fundamentally evaluative
* (commit, signal).
* - `'contextual'` — intent depends on the situation. Many uses are
* neutral; some legitimately carry intent
* (handle, emerge, shift, sustain, delegate).
* - `'discouraged'` — using intent is unusual and frequently a smell.
* The book Cap 22 §11 says "el intent fuerte no
* debería vivir en el contacto"; contact's
* guidance is `discouraged` to reflect that even
* though the type system allows it.
*
* Future families slot in here when introduced (e.g. `delegate` for
* agentic interfaces): just add `delegate: { intentPolicy: 'optional' }`
* after extending `SemaTransitionalFamily` (or wherever it lands).
* The types `SemaEvent` and `MorfoEventSemantic` derive from the
* `intentRequirement` axis (see `IntentRequiredFamily` /
* `IntentOptionalFamily` below). The `intentGuidance` axis is for docs
* and future lint.
*
* Per `src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.3.
*/
export const SEMA_FAMILY_POLICY = {
contact: { intentPolicy: 'allowed' },
commit: { intentPolicy: 'expected' },
signal: { intentPolicy: 'expected' },
handle: { intentPolicy: 'allowed' },
emerge: { intentPolicy: 'optional' },
shift: { intentPolicy: 'optional' },
sustain: { intentPolicy: 'optional' },
// Book cap. 29 §4: "Delegate no tiene intent por defecto. Que el sistema
// actúe por el usuario no significa automáticamente que algo sea positivo,
// negativo, urgente o perdido." Intent appears when the delegated action
// produces a downstream signal/commit.
delegate: { intentPolicy: 'optional' }
} as const satisfies Record<SemaFamily, { intentPolicy: IntentPolicy }>;
/** Family classifications derived from `SEMA_FAMILY_POLICY.intentPolicy`. */
export type IntentExpectedFamily = {
[K in SemaFamily]: (typeof SEMA_FAMILY_POLICY)[K]['intentPolicy'] extends 'expected' ? K : never;
// Cap 22 §11: "el intent fuerte no debería vivir en el contacto, sino
// en la señal o consecuencia posterior" — guidance discouraged.
contact: { intentRequirement: 'optional', intentGuidance: 'discouraged' },
commit: { intentRequirement: 'required', intentGuidance: 'expected' },
signal: { intentRequirement: 'required', intentGuidance: 'expected' },
handle: { intentRequirement: 'optional', intentGuidance: 'contextual' },
emerge: { intentRequirement: 'optional', intentGuidance: 'contextual' },
shift: { intentRequirement: 'optional', intentGuidance: 'contextual' },
sustain: { intentRequirement: 'optional', intentGuidance: 'contextual' },
// Cap 29 §4: "Delegate no tiene intent por defecto."
delegate: { intentRequirement: 'optional', intentGuidance: 'contextual' }
} as const satisfies Record<
SemaFamily,
{ intentRequirement: IntentRequirement; intentGuidance: IntentGuidance }
>;
/**
* Families whose policy requires `intent` at the type level. Derived from
* `SEMA_FAMILY_POLICY[K].intentRequirement === 'required'`.
*/
export type IntentRequiredFamily = {
[K in SemaFamily]: (typeof SEMA_FAMILY_POLICY)[K]['intentRequirement'] extends 'required'
? K
: never;
}[SemaFamily];
export type IntentOptionalFamily = Exclude<SemaFamily, IntentExpectedFamily>;
/**
* @deprecated Use {@link IntentRequiredFamily}. Kept as alias for
* backward compatibility with the `intentPolicy: 'expected'` naming.
*/
export type IntentExpectedFamily = IntentRequiredFamily;
export type IntentOptionalFamily = Exclude<SemaFamily, IntentRequiredFamily>;
export type SemaMode = 'blocking' | 'advisory';
export type SemaRegime = 'replace' | 'collapse' | 'lock' | 'queue';

@ -34,14 +34,14 @@ export function validateSemaEvent(event: SemaActionEvent, ctx = 'sema.event'): v
const intent = 'intent' in event ? event.intent : undefined
// Policy enforcement: families marked `intentPolicy: 'expected'` MUST
// Policy enforcement: families marked `intentRequirement: 'required'` MUST
// declare intent.
if (
intent === undefined &&
SEMA_FAMILY_POLICY[event.family].intentPolicy === 'expected'
SEMA_FAMILY_POLICY[event.family].intentRequirement === 'required'
) {
throw new SemaInvariantError(
`${ctx}: family "${event.family}" requires intent (intentPolicy: 'expected')`
`${ctx}: family "${event.family}" requires intent (intentRequirement: 'required')`
)
}

Loading…
Cancel
Save

Powered by TurnKey Linux.