70 KiB
| title | type | audience | authority | status | source |
|---|---|---|---|---|---|
| Registro de desviaciones entre implementación y canon editorial | decision-log | human + agent | authoritative registry — where the implementation deviates from the book canon, with per-entry status | current | 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.
scrollno 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
- Scroll gestual del usuario →
- Texto doctrinal para el libro:
handle.scrollcubre 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 ashift.navigate.
A.2 commit.acknowledge
- Status: CANDIDATE
- Libro Cap 23 §5 lista 12 verbos de commit.
acknowledgeno 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).removeno aparece. - Decisión del autor: distinción de tres verbos sobre operaciones de retirada/eliminación.
- Texto doctrinal para el libro:
removeno 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, correspondedelete. Si solo deja de estar seleccionado, correspondeunselect.
A.4 commit.confirm
- Status: CANDIDATE
- Libro Cap 23 §5 lista
submit,complete.confirmno 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,authorizeoacknowledge. - 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 + affirmpero 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.setcomunica que un valor, criterio o parámetro ha quedado aplicado. Diferente desave(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.applyen ejemplos. Cap 23 §5 no lo lista. - Decisión del autor: formalizar en Cap 23 §5.
- Texto doctrinal para el libro:
commit.applycomunica que un conjunto de cambios o una operación se ha aplicado. Diferente desave(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 lugarcommit.reorder→ una colección cambia de orden
- Caso típico:
handle.drop → commit.move + affirmohandle.drop → commit.reorder + affirmsegú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 encommit.complete(éxito) ocommit.fail(error). Como verbo de resultado quizá es redundante concommit.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+ variantpartialcommit.fail + risksustain.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 + threatcommit.fail + risksustain.blocked + risk(estado)
- A revisar antes de canonizar.
A.11 commit.unselect
- Status: BOOK_CANON
- Libro Cap 23 §5 lista
selectpero nounselect. 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:toggledescribe 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óntoggle→ inversión binaria genéricaremove→ 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.navigatecon ejemplos a escala "Lista → detalle. Página A → página B". - Decisión del autor: aclarar que
shift.navigatepuede 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 crearshift.scrolltotodaví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 itemsemerge.reveal— si aparecen elementoscommit.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
- sort by criterion →
- Para el libro: añadir como ejemplo de
commit.setcuando 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=falsetras 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=neutralcomo 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; elintentprop del componente puede afectar visualmente (data-color, forma) pero el evento sema se mantiene neutro. - Aplicado en: Button (
contact.activatesin intent en el evento;intentprop 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.resetcuando se introduzcacommit.set/commit.resetal 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-slidefeed.shift-focus-item→shift-navigate-focus-itemfeed.commit-load-more→commit-submit-load-morefile-upload.commit-add→commit-set-addfile-upload.signal-reject→signal-warn-rejectform.signal-invalid→signal-warn-invalidnumber-field.handle-scrub→handle-drag-scrubrange-calendar.commit-start→commit-select-startrange-calendar.commit-range→commit-select-rangetags-input.commit-add→commit-set-addtags-input.signal-reject→signal-warn-reject
C.3.b Correcciones doctrinales
- Status: BOOK_CANON (precedent Button)
command.commit-invoke(declaredsubmit + 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 + losssi solo registra el click; declaracontact.activate. El flujo, diálogo o acción que realmente elimina declaracommit.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:
emission: 'runtime' | 'host' | 'external' | 'declared-only';
// o:
implemented: true | false;
- Acción del proyecto: añadir flag
emissional 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:
type SemaExpressionMode = 'pack' | 'family-default' | 'delegated' | 'none';
-
pack: el morfo tiene un archivosrc/uix/sema/components/{kebab}.tscon cascade rules específicas (sonido, háptica, prioridad). -
family-default: el morfo descansa enSEMA_MAP.families[family].basesin 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:vocabularyvalida que todo morfo conevents.length > 0cumpla:scopeincluye'sema'(FAIL).- Existe un pack en
src/uix/sema/components/{kebab}.tsoexpression !== 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-defaultsi: 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-groupemitencommit-toggleen bucle (settings panels, toolbars, segmented controls). Sin tuning, heredan elfamily.commit.base.gain = 0.3que 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 } }ensrc/uix/sema/sounds.ts. Cancelaba exactamente elgaindel 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 elkindpara que la háptica no oscile con el intent — la carga evaluativa de un toggle se lee endata-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,toggleytoggle-group, que pasan acommit.medium(gain 0.1) — audible, y un tercio de una pulsación de botón (contactbase 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) ymedia-player(su propia salida ES audio, D-AP2.7). Además, emitir silencio dejó de costar:SoundChannel.handlecorta antes de sintetizar cuando la ganancia resuelta es ≤ 0 (chans/sound.ts), así que un{family}.silentya 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ó comoform.toggle.silenty pasó a llamarsecommit.silent— una clave que vivió UN DÍA: la nota siguiente la retira del catálogo entera. El prefijoform.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íaform.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 ensema.md§ Tuning naming shapes — el primer segmento es la familia cuya base modifica el tuning, nunca un componente — y la vigilasrc/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ólocommit.soft,commit.subtle,commit.select.soft,commit.mediumy lasemerge.*— el enumerado generado está encanon/vocabularies.md. - 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 haySILENT, un único valor exportado por$uix/semaque 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 a0still 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 alAudioParamde la envolvente —y conectar a unAudioParamSUMA, no multiplica— el trémolo pasaba a ser la señal entera. Renderizado offline con el grafo real:risk−13,9 dBFS ythreat−6,7 dBFS desde una firma declarada again 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 enresolver.test.ts(universal para toda familia e intent, y liftable sólo por REEMPLAZO) y porsounds-grammar.test.ts, que ahora prohíbe tanto una clave*.silentcomo cualquier afinado que deje su familia engain <= 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, heredanfamily.emerge.base.gain = 0.2yfamily.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) +tapleve. 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.
- emerge.open (menús, expand):
- 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-menusale del alcance de esta entrada. SuLinkya no emitecommit-select: navegar no fija nada, y uncommitahí 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-activateen el enlace yshift-navigateen el<nav>—, y ninguna de las dos familias se afina: traen su carácter del mapa (touch+tickháptico ·slide), que es el mismo silencio que guarda el pack delSidebarpara 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 elcommitque 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-menudeclaróemerge-open/emerge-closesobre sutrigger; (2) elcommit.subtle(0.03) que esta entrada prescribía paranavigation-menullevaba tiempo sin estar implementado — murió en86fa9a1b7(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 elemerge.soft(0.08) de esta entrada lo absorbió el catálogo: la baseopenes 0.09, por eso las reglasemergede los menús de clic están vacías y siguen siendo correctas. Paranavigation-menu, en cambio, la respuesta NO es «soft» sinoSILENT(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 quetooltipy que el flyout delsidebar(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. menubarqueda pendiente y NO se toca aquí: sucommit-selectvive en eltriggerde 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 (reglano-cascade-changes).- Caveat (
tree-view/tree-grid) — RESUELTO (verificado 2026-07-12, SEM-4): la emisión aterriza víatargetOverrideen el elemento real (branchEl/rowEl) y los packs construyen sus selectores cononBranch/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-closedesde 2026-08-05 ycontact-activate/shift-navigatedesde 2026-08-19, ver la ENMIENDA de arriba; trees:emerge-expand/emerge-collapse+commit-select). Verificado en vivo: stampsopen · emerge · activeycommit-select · commit · affirmen 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 modoreplaceDESPUÉS de los deltas de intent, así que canonizar un sample sobre un eventocommitborraba el perfil evaluativo — unfulfillperdí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 ahandle/sustain/delegate(familias sinbase.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] ?? nadaEl intent selecciona un sonido entero (
tick.threatno es untickdeformado) 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 unSoundNameoSILENT, 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 poroverrides.cascade, que sigue abierto a propósito.
- «El silencio es una firma válida» →
-
⚠️ EL LÍMITE FÍSICO, medido — lo único de esta entrada que hay que seguir sabiendo.
playSamplelee exactamente dos campos de la firma:sampleUrlygain. No hayplaybackRatenidetune. 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.mp3Ytick.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
signales 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
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:
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 derivaIntentExpectedFamilydesdeintentRequirement === 'required'.intentGuidancequeda 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
Announcenecesita ser pluggable … entonces vale convertirlo en canal formal. Hoy no». Ese futuro llegó: elAnnounceChanneles built-in y exportado; opt-in en el MOTOR desnudo, y las raíces de composición lo cablean por defecto (verarchitecture/sema.md§Announce channel) (src/uix/sema/chans/announce.ts), ysema.md§Announce channel 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) —visual/sound/hapticEXPRESAN,announceSUSTITUYE.
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):
channelses la palanca declarativa por evento o por regla, y ajusta en las DOS direcciones. Restringir: el morfo del dialog usachannels: ['haptic']enclose-after-failpara no competir con la live region; el transporte del player hace lo mismo (D-AP2.7 v2);VirtualListusachannels: []. Ampliar:sema.md§componentes continuos MANDA que el pack del slider pongachannels: ['sound', 'haptic']sobre la familiahandle.- 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.handledeclara hoysounds: { 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 -Sdemostró 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. LedgerAUDIT-docs-code-ledger.md§D10. 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
dialogdeclara háptico sobre eventos de familiaemerge(dos reglas), más otras tres del mismo tipo en el catálogo. Cada una o justifica su ampliación conchannels, 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:
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) depersistence(cuánto tiempo dura realmente la señal). La implementación trataba todo como transient con un hold numérico — regresión: unsignal.warn + riskde validación desaparecía a los 240 ms aunque el formulario siguiera inválido.
Tipos (src/uix/sema/types.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 y se enumera generada en
docs/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.fulfilldenoticedasettledysignal.lossdenoticedabrief, 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()devuelveEmitHandle { id, settled }de forma SÍNCRONA (D-full, 2026-09-15; antes eraPromise<string>que resolvía tras el hold). Elidse 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. Parapersistence !== 'transient'la proyección sigue viva pasado el hold; el caller posee el cleanup víaengine.clear(id)oengine.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: devuelvetrue, la ocurrencia no se registra como persistente y se limpia como transitoria al terminar. Responderfalsehabría dejado una señal en pantalla que su dueño ya había retirado, sin handle con el que volver a retirarla.clearTargethace lo mismo con las ocurrencias en vuelo sobre ese elemento, y las cuenta. SomaRuntime.trigger()devuelveTriggerResult { id?, persistence?, settled }.settledes 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. Exponeruntime.clearSignal(id)yruntime.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
holdque es la duración mínima necesaria para que el usuario la registre como evento, y unapersistenceque dice cuánto tiempo permanece visible una vez registrada. Para la mayoría de eventos coinciden: la señal aparece, duraholdms, y desaparece. Pero parasignal.warn + riskysignal.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):
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:
{ name: 'signal-warn-invalid', semantic: {...}, a11ySemantic: { requiresPersistentTrace: true, requiresFocusMove: true, reducedMotionFallback: 'text' } }
Infraestructura nueva:
ActiveDom.prefersReducedMotion: tracker reactivo del media query, vive ensrc/arts/adom/reduced-motion.svelte.ts. SSR-safe (devuelvefalsecuando no haymatchMedia).ActiveUix.announce(message, priority?, timeout?): live region compartida lazy-creada en document body. NO depende de soma — usadom.writeNodedirecto. 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.triggerhonra su mitad tras el DESPACHO (ya no tras el hold — ver D.9):requiresLiveRegion∧opts.message∧sources.announce→ llamaannounce(message, priority)donde la prioridad se deriva del intent (threat/loss→ assertive, resto polite).requiresFocusMove∧ target →dom.focus(target).reducedMotionFallback === 'text'∧ preferenciareduce→ llama announce aunquerequiresLiveRegionno esté seteado.reducedMotionFallback === 'focus'∧ preferenciareduce→ focusea target aunquerequiresFocusMoveno esté seteado.reducedMotionFallback === 'none'→ no hace nada (el motion era incidental).
reducedMotionFallback === 'state'lo enacta el MOTOR, no el runtime: bajo preferenciareducela 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 unuix.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 dea11ySemanticen el motor). - La doctrina S5 no se toca: la preferencia del usuario gana al
channelsdel 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
MotionSourceno puede preguntar y no se inventa la respuesta. Las dos raíces de composición siempre la inyectan.
- 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
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
closecon/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:
{
name: 'close',
semantic: {
family: 'shift', // default
verb: 'exit-mode',
target: v.partRef('content'),
allowedFamilies: ['shift', 'commit', 'emerge'] // polymorphic capacity
}
}
Trigger:
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
familydeclarado en el morfo es IMPLÍCITAMENTE allowed; no hace falta repetirlo enallowedFamilies(que enumera SOLO las alternativas). - El override falla con
SomaRuntimePolymorphicErrorsi la family no está en allowedFamilies y no es el default. TriggerOptions.semanticopcional. 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'YallowedFamilies: ['shift', 'commit', 'emerge']cumple la misma intención que el shape del libro con menos anidamiento. - Cero overhead para no-polymorphic: morfos sin
allowedFamiliesno 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 opcionalallowedFamiliespuede enumerar las alternativas que el provider está autorizado a emitir según contexto. Por ejemplo, elclosede un diálogo es típicamenteshift.exit-mode, pero si hay cambios sin guardar puede convertirse encommit.discard + loss, o si simplemente se descarta sin acción, enemerge.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:
{
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:
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 "cadadata-last-action.values[]debe ser prewritten por algún event". El otro sentido sigue estricto (un prewrite con valor fuera devalues[]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 matcheabaneventName: 'close-dismiss-outside'oeventNamePrefix: 'close-'se reescribieron para usareventName: 'close'+ matchers adicionales (state: { attr: 'data-last-action', value: 'dismissed-outside' }yeventFamily: 'emerge'). El helpersemaSelectorsoporta el matcherstatenativamente. - Eidos CSS (
dialog.css): NO requirió cambios — los selectores ya leendata-last-actionpara tintar la animación de salida, no los nombres de evento. - Tests del morfo/runtime: actualizados para esperar
closeen lugar declose-cancel/etc. El test de prewrite del compiler se movió adrawerMorfo(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
closeevent en su morfo - DISMISS_CAUSES + dismissWith adaptado en su provider
- Cascade sema reescrito (
eventName: 'close'+statematchers endata-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 comoclose-range-commiten date-range-picker), todos con prewrite individual dedata-last-action. - Colapsados a 1 evento
closepolymorphic confamily: 'emerge', allowedFamilies: ['emerge', 'commit', 'signal'], sin prewrite. - Hallazgo: los providers de los pickers NO disparan los close events vía
runtime.trigger. Sólo toggleanopts.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-pickertiene 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 declaranclosepropio (expression: 'delegated'— la firma pertenece al Popover compuesto); el cierre programático (commit/cancel/select-close) ahora enruta por causa víaPickerShellHandle.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.tscitaba 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):
- Los ms son autoría del framework (materialización de las regiones sobre
SEMA_DURATIONS), nunca "transcripción del libro". Procedencia corregida enholds.ts/durations.ts. - Peldaño nuevo
settled(400 ms) — la escala no tenía paso entrebrief(240) ynoticed(600) y la región "breve-media" lo exigía.commit.fulfill600→400 (settled);signal.loss600→240 (brief— la huella es del caller: undo/estado, no señal más larga). - Tabla única:
SEMA_HOLDS_BY_INTENT(familia+intent) es LA fuente de holds; elholdpor-familia duplicado deSEMA_MAPse eliminó (había derivado: signal 600 contra la región "breve o contextual" → vuelve a 240). El resolver componesignal.hold ?? resolveHoldsByIntent(family, intent). - "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
finishedde las animaciones activas del target (VisualChannel.awaitExpression), con tope ABSOLUTOMAX_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 deEmitHandle.settled, que no gatea a nadie. Lo que retiene el nodo mientras la expresión corre esPresence, no el retraso del commit — y eso ya era así. - Firmas re-materializadas a las regiones: announce neutral/affirm
deliberate(600)→moderate(240)("breve"); fulfill →slower(400)("breve-media"); riskemphatic(800)/ threatsustained(1000)conservan la escalación que el libro sí quiere saliente/persistente. - 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 deholds.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 dedialogdeclaraemerge-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:
dialog(yalert-dialog, que delega en sus eventos) CONSERVAemerge-open/emerge-close. No hay migración a shift.- Un uso que de verdad cambie el régimen (bloquea el fondo, captura el foco,
exige reorientación como MODO) puede componer
shift.enter-modea nivel de aplicación — la frontera se decide POR USO, no por componente. - 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 fuertescommit.confirm(A.4) — solapa con submit/apply/acknowledgecommit.upload(A.8) — probable redundancia con completecommit.partial(A.9) — probable estado, no verbocommit.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
expressionen 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:
- BOOK_CANON aceptados: ya están en
SEMA_VERBSdel proyecto. La próxima edición del libro puede formalizarlos. Los textos doctrinales recomendados están en bloques citados arriba. - CANDIDATE: mantener en
SEMA_VERBSpero no promover al libro hasta acumular casos. - IMPLEMENTATION_CONTRACT: requiere cambios en types (flag
emission) en una iteración futura. - Cambio inmediato pendiente en proyecto: split de
intentPolicyenintentRequirement+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_CANONaBOOK_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
IntentExpectedFamilyse borró en13246a2c2(M6, «un nombre por concepto»). El nombre vivo esIntentRequiredFamily. - La derivación ya no son dos cubos.
1a174d5a6(S-33) la hizo positiva y triple sobreintentRequirement:IntentRequiredFamily('required'),IntentOptionalFamily('optional') eIntentForbiddenFamily('forbidden'). Derivar el segundo cubo conExclude<…, 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. intentGuidancesigue 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). 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.