29 KiB
Sema
Sema define el dominio semántico canónico de UIX y orquesta la emisión de
señales perceptivas.
Qué es
- familias canónicas (8):
contact,commit,signal,handle,emerge,shift,sustain,delegate - intents canónicos:
neutral,affirm,fulfill,risk,threat,loss - política de intent por familia (
SEMA_FAMILY_POLICY), dos ejes:intentRequirement('required'|'optional'|'forbidden') — el gate de compilación que da forma a la unión discriminada — eintentGuidance('expected'|'contextual'|'discouraged') — hint doctrinal para lint/tooling - normalización entre shape estructurado y label canónico
- validación mínima del dominio
EngineSemanticcomo registry de canales + dispatch de ocurrencias
Sema no decide qué evento ocurrió. El provider lo decide. EngineSemantic
recibe la ocurrencia y la despacha a los canales perceptivos registrados.
Cada canal materializa la señal en su modalidad (DOM, audio, vibración).
Qué ya no es
Sema ya no es un runtime multimodal monolítico.
El EngineSemantic no contiene:
- política global de accesibilidad
- mapa perceptivo cross-canal
- decisiones sobre qué efecto modal aplicar
Eso vive en cada canal por separado:
EngineSemanticmantiene un registry de canales y despacha cada signal- el proyector DOM materializa la señal como
data-event*en el DOM SoundChannel,HapticChannel, futuros engines modales se registran como canales independientes que reciben la señal y deciden cómo materializarla en su plano
Registry de canales — abierto
El conjunto de canales NO está cerrado. El framework ships con firmas
canónicas para sound y haptic. El canal visual existe como meta-canal
de proyección/hold, pero no tiene slice propia en EffectiveSignature.
Apps pueden añadir canales con firma vía declaration merging:
// app bootstrap
declare module '$uix/sema' {
interface SemaChannelSignatures {
a11y: A11ySignature; // narrador / live-region
voice: VoiceSignature; // text-to-speech
}
}
Sólo el visual channel es obligatorio (con escape visual: false).
Sound y haptic son opt-in. Cualquier canal nuevo registra su Channel y
recibe el dispatch.
El contrato emit
semantic.emit(signal: SemanticSignal): Promise<void>
Una sola firma. Cubre los tres escenarios cuando se compone con dom.apply:
// Cambio estructural sin señal
dom.apply(change);
// Cambio estructural con señal
await semantic.emit(signal);
dom.apply(change);
// Señal sin cambio estructural
void semantic.emit(signal);
Semántica de la Promise — sequential strict
emit(signal) resuelve cuando el VisualChannel ha completado su
materialización entera:
- el
VisualChannel.prepare()proyectódata-event*al DOM - los atributos vivieron en el DOM durante el
holdconfigurado - los atributos ya fueron retirados (cleanup completo)
- la Promise resuelve
Esto es sequential strict: el caller aplica el commit estructural DESPUÉS de que la señal perceptiva haya terminado. No hay paralelismo entre evento y cambio de estado.
Los canales no-visuales (sound, haptic) son fire-and-forget: arrancan en paralelo con el visual pero no afectan al timing de la Promise.
Ciclo de vida interno de emit
1. Engine genera id/session de la ocurrencia
2. Engine ejecuta `channel.prepare(...)` en los canales registrados
- `VisualChannel.prepare()` proyecta `data-event*` para la cascade
3. Engine resuelve la cascade y despacha la señal a TODOS los canales registrados
- canales no-visuales (sound, haptic) → fire-and-forget (no awaited)
- canal visual → awaited
4. Engine resuelve la Promise cuando el visual ha terminado
(semántica secuencial estricta: cleanup ANTES del resolve)
Canales como módulos
Sema está organizada en canales perceptivos simétricos:
src/uix/sema/
├── engine.ts registry + channel prepare/dispatch + cascade composition
├── resolver.ts resolveSignature(signal, opts): EffectiveSignature
├── stamp.ts stampEventAttrs / unstampEventAttrs (data-event-*)
├── channels.ts channel ids, signatures and override types
├── sounds.ts repositorio nominal de sonidos + recetas dinámicas
├── sema-map.ts SEMA_MAP data + per-component Sema packs
├── components/ per-component perceptual packs (CSEM)
│ ├── dialog.ts dialogSema — cascade rules + preloadSamples
│ ├── toast.ts (futuro)
│ └── …
└── chans/
├── types.ts interfaz Channel — handle(signal, effective)
├── visual.ts VisualChannel (data-event projection + hold)
├── sound.ts SoundChannel (Web Audio earcons + sample playback)
└── haptic.ts HapticChannel (Vibration API + categorical kinds)
El engine no muta atributos DOM directamente: ejecuta el hook genérico
channel.prepare(...). En el canal visual, ese hook delega la proyección a un
SignalProjector. Cuando lo construye ActiveUix, ese proyector escribe via
el ActiveDom de UIX. El engine resuelve cada signal en una
EffectiveSignature con hold, sound, haptic y futuros canales tipados,
y dispatcha (signal, effective) a cada canal. Cada canal lee su slice
(effective.sound para audio, effective.haptic para vibración, etc.) o
ignora el signature si no lo usa. Solo el canal visual bloquea al caller con
el hold perceptivo; los demás son fire-and-forget.
Resolver y sema-map — cascada de 5 capas
El engine, en cada emit:
- Ejecuta los
preparede canales. ElVisualChannelproyecta los tokens semánticosdata-event,data-event-family,data-event-intent,data-event-phase,data-event-idalsignal.target. Estos son los tokens que la cascade y la CSS de eidos leen. - Llama a
resolveSignature(signal, opts)que aplica la cascada de 5 capas (cada una sobreescribe la anterior):
1. FAMILY base — SEMA_MAP.families[signal.family].base
sound / haptic + activeChannels + hold
2. INTENT deltas — SEMA_MAP.intents[signal.intent] cuando exista;
numbers ADD por defecto — son modificadores)
3. MORFO overrides — signal.overrides + signal.channels
(numbers REPLACE por defecto — son set values)
4. RUNTIME overrides — engineOpts.overrides.runtime (path-based globals;
baked into el map en el constructor; numbers REPLACE)
5. CASCADE rules — engineOpts.components (per-component packs prepended)
+ engineOpts.overrides.cascade (app-level appended;
gana en empate de specificity por declaration order).
Selectors CSS-like contra signal.target con los
data-event-* ya stampados; numbers REPLACE.
- Despacha a cada canal con la signature resuelta.
- Awaita el VisualChannel (que aporta el hold).
- Ejecuta el cleanup de los handles devueltos por
prepare.
Convención de overrides vs deltas — números:
- Capa 2 (intent.deltas):
pitch: -200significa "restar 200 al pitch base". Modificadores compositivos. - Capas 3, 4, 5 (overrides):
pitch: 720significa "set pitch a 720". Como CSS —gain: 0.4no añade, asigna. - Para sumar explícitamente desde una capa de override:
{ op: 'add', value: 100 }. - Para multiplicar:
{ op: 'multiply', factor: 1.2 }. - Para reemplazar primitivos no-numéricos:
{ op: 'replace', value: ... }.
Si signal.family falta o no está en el map, devuelve un
EffectiveSignature vacío — los canales hacen no-op.
Tokens semánticos — data-event-*
El VisualChannel.prepare() proyecta los siguientes attrs en signal.target
ANTES de resolver la cascade. Las rules con selectores sobre estos attrs
matchean nativamente vía target.matches() / target.closest():
| Attr | Valor | Origen |
|---|---|---|
data-event |
'close-after-fail' |
signal.name |
data-event-family |
'signal' |
signal.family |
data-event-intent |
'threat' |
signal.intent (cuando exista) |
data-event-phase |
'active' |
mientras dure el hold |
data-event-id |
'sig-42' |
id de ocurrencia |
Esos tokens son la superficie de contacto cross-channel: la cascade
de sema (sound, haptic y futuros canales) los lee con selectores CSS,
igual que el CSS de eidos los lee para tintar bordes / animar estados durante
el hold. Una sola superficie perceptiva, con dueños separados.
Cascade rules — forma plana CSS-like
interface SemaCascadeRule {
selector: string // CSS selector — matchea state attrs + tokens evento
priority?: number // override de specificity CSS (opcional)
channels?: readonly SemaChannelId[] // restringe / silencia canales activos
sound?: …
haptic?: …
}
Una rule = un selector + un block de deltas. Sin capa intermedia
overrides: { eventLabel: ... } — la identidad del evento se lee del
selector mediante los tokens [data-event*].
Cascade selectors — typed builder, no hand-written strings
Cascade rules in sema/components/*.ts MUST build their selector via
semaSelector(morfo, partKebab, matchers?) from $uix/morfo. The
helper closes the loop between morfo's part/event contract and the
selectors the cascade evaluates.
import { semaSelector } from '$uix/morfo';
import { dialogMorfo } from '$uix/morfo/components/dialog';
const onContent = (matchers?: Parameters<typeof semaSelector<typeof dialogMorfo>>[2]) =>
semaSelector(dialogMorfo, 'content', matchers);
cascade: [
// [data-dialog-content][data-event-family="commit"]
{ selector: onContent({ eventFamily: 'commit' }), haptic: { kind: 'tap' } },
// [data-dialog-content][data-event="close-after-fail"]
{ selector: onContent({ eventName: 'close-after-fail' }), sound: { sampleUrl: '/fail.wav' } },
// [data-dialog-content][data-event^="close-"][data-event-family="emerge"]
{
selector: onContent({ eventNamePrefix: 'close-', eventFamily: 'emerge' }),
sound: { contour: 'descending', pitch: { op: 'add', value: -150 } }
}
];
Compile-time guarantees:
partKebabis checked againstmorfo.parts[].kebab.eventNameis checked againstmorfo.events[].name.eventFamily/eventIntentare typed against the canonical sema unions.state/aria/pseudoaccept plain strings (the data-attr vocabulary is per-component and not yet typed-derived).
Renaming a part or event in morfo breaks the cascade at type-check time, not silently in production. Hand-written selector strings in cascade rules are a code smell — review them as drift.
Per-component packs — sema/components/{name}.ts
Cada componente trae su pack de defaults perceptivos en
src/uix/sema/components/{name}.ts, simétrico a soma y eidos:
import type { Sema } from '../sema-map';
import { sound, soundSampleUrls } from '../sounds';
export const dialogSema: Sema = {
name: 'dialog',
preloadSamples: soundSampleUrls(['notification.ping']),
cascade: [
{
selector: '[data-dialog-content][data-event-intent="threat"]',
sound: sound('notification.ping'),
haptic: { kind: 'error', pattern: [50, 80, 50, 80, 50] }
}
]
};
App importa los packs que use (tree-shakable):
import { dialogSema } from '$uix/sema/components/dialog';
defineEngineSemantic({
components: [dialogSema],
overrides: {
cascade: [
// App-level rules ganan sobre los packs en empate de specificity
{ selector: '#critical [data-dialog-content]', sound: { sampleUrl: '/x.wav' } }
]
}
});
preloadSamples se concatena entre todos los packs y se le pasa a
SoundChannel.preloadSamples() al crear el engine cuando la app lo pide
explicitamente — los WAVs pueden quedar decodeados antes del primer emit.
El listener global de unlock no se registra en el constructor del canal; se
instala solo cuando existe un AudioContext que desbloquear.
Repositorio de sonidos — sounds.ts
Los componentes no deben declarar firmas sonoras completas en cada pack. Sema tiene un repositorio nominal:
import { sample, sound, soundTuning } from '$uix/sema';
sound('handle.pickup.air');
soundTuning('emerge.exit.deep');
Una entrada puede ser sintética o apuntar a un fichero externo .wav con
fallback sintético:
sample('/sounds/uix/dialog-fail.wav', sound('handle.release.soft'), { preload: true });
SoundChannel ya entiende sampleUrl: intenta reproducir el WAV y, si
fetch/decode falla, cae a la firma sintética. La regla es que los componentes
referencian nombres; las URLs y parámetros viven en un solo sitio.
Canales y signatures
| Canal | Slice consumido | Comportamiento si no aplica |
|---|---|---|
visual |
effective.hold para hold |
Cae al family fallback table (SEMA_DURATIONS) o al defaultHold |
sound |
effective.sound (skip si 'sound' no en activeChannels) |
no-op |
haptic |
effective.haptic (Vibration API, respeta prefers-reduced-motion) |
no-op si no hay navigator.vibrate |
| (custom) | declaración merging del SemaChannelSignatures |
leído por el canal registrado |
Namespace de attrs del canal visual
El VisualChannel.prepare() proyecta solo atributos bajo el prefijo
data-event-*:
| Attr | Cuándo |
|---|---|
data-event |
siempre |
data-event-id |
siempre |
data-event-phase |
siempre ('active') |
data-event-family |
si signal.family |
data-event-intent |
si signal.intent |
Regla: el canal nunca toca state attrs (data-state, data-intent,
data-disabled, ...). El estado lo gestiona el runtime/morfo. Razón:
state es persistente y signal es transitorio; pisar el mismo nombre
fuerza al canal a borrar estado al limpiar (o a save/restore frágil si
el estado muta durante el hold).
Para CSS:
[data-event-intent='risk']→ reacciona al intent de la ocurrencia transitoria[data-intent='risk']→ reacciona al estado persistente del componente
Ambos pueden coexistir en el mismo elemento con semánticas distintas.
Hold — cadena de resolución del canal visual
El VisualChannel resuelve su hold (cuánto viven los data-event-*
en el DOM) en este orden:
signal.hold— override imperativo per llamada.effective.hold— viene del resolver desdeSEMA_MAP.families[*].hold.- Family fallback table (
SEMA_DURATIONS+ label per familia) — defensa si una familia externa no declarahold. defaultHoldglobal del canal (240ms por defecto).
Ver SEMA_MAP.families[*].hold para los valores concretos por familia.
// Override per signal
semantic.emit({ ..., hold: 1200 })
// Override default global del canal visual
const semantic = new EngineSemantic({ dom, visual: { defaultHold: 400 } })
// Desactivar visual (entornos sin DOM)
const semantic = new EngineSemantic({ visual: false })
// Activar el canal de sonido built-in (opt-in: tiene side effect audible)
const semantic = new EngineSemantic({ dom, sound: true })
// Con opciones de SoundChannel
const semantic = new EngineSemantic({
dom,
sound: { masterGain: 0.6 }
})
// Activar el canal háptico built-in (opt-in: feedback del dispositivo)
const semantic = new EngineSemantic({ dom, haptic: true })
// Con opciones de HapticChannel
const semantic = new EngineSemantic({
dom,
haptic: { masterIntensity: 0.7 }
})
// Registrar canales a medida (a11y, voice, futuros)
class A11yChannel implements Channel {
readonly id = 'a11y'
async handle(signal, effective) { /* live-region updates, etc. */ }
}
semantic.register(new A11yChannel())
Override layers — recipes
import { dialogSema } from '$uix/sema/components/dialog';
const semantic = new EngineSemantic({
dom,
sound: true,
haptic: true,
// Capa 6a — packs de componentes (defaults shipped con cada componente)
components: [dialogSema /* , toastSema, drawerSema, … */],
overrides: {
// Capa 5 — edits puntuales del SEMA_MAP, válidos a TODA la app
runtime: {
'families.commit.base.sound.pitch': 850,
'intents.threat.deltas.haptic.intensity': 0.4
},
// Capa 6b — rules CSS-like de la app, matched contra signal.target
// AFTER de los packs de componente
cascade: [
{
selector: '#delete-confirm-dialog [data-dialog-content][data-event-intent="threat"]',
sound: { sampleUrl: '/sounds/scary.wav', gain: 0.25 },
haptic: { kind: 'error', pattern: [50, 80, 50, 80, 50] }
},
{
selector: ':root[data-sound="reduce"] [data-event-phase="active"]',
priority: 100,
sound: { gain: 0.05 },
channels: ['sound']
}
]
}
});
// Capa 4 — per-event override declarado en la propia morfo del componente.
// Propaga vía SomaRuntime → SemanticSignal → resolver. La app puede
// seguir sobreescribiendo desde la cascade (capa 6).
// src/uix/morfo/components/dialog.ts
{
name: 'close-after-fail',
semantic: {
family: 'signal',
verb: 'alert',
target: v.partRef('content'),
sequence: 'pre',
intent: 'threat',
// Per-event silencing: sin sonido para evitar competir con el live-region
channels: ['haptic'],
// Per-event override: sample propio del Dialog
overrides: {
haptic: { kind: 'error', pattern: [60, 80, 60, 80, 60] }
}
}
}
Specificity entre rules
Como CSS:
- Specificity computada del selector — IDs × 100, atributos × 10, classes × 10, pseudo-classes × 10, elementos × 1.
- Rules con specificity ascendente se aplican en orden — la última aplicada gana en cada conflicto puntual.
- Empate → orden de declaración. Componentes packs aparecen ANTES
de
overrides.cascade, así app rules ganan en empate. priority?: number— override del valor computado para casos que necesitan ganar sin contar atributos (típicamente accesibilidadpriority: 100+).
Para variar por evento, intent, family, name, etc. — todo va en el
selector mediante los tokens [data-event-*]:
// Variación por intent del evento
{ selector: '[data-toast-root][data-event-intent="threat"]', haptic: { kind: 'error' } }
// Variación por family
{ selector: '[data-toast-root][data-event-family="signal"]', sound: { gain: 0.4 } }
// Variación por nombre exacto de evento
{ selector: '[data-toast-root][data-event="close-after-fail"]', sound: { sampleUrl: '/fail.wav' } }
// Variación por prefijo de evento
{ selector: '[data-dialog-content][data-event^="close-"]', sound: { contour: 'descending' } }
SoundChannel
Earcons cortos sintetizados vía Web Audio API a partir de
effective.sound (pitch / centroid / roughness / attack / decay /
duration / contour / gain). Detalles:
- Un único
AudioContextconGainNodemaster por engine. - Prepare-time priming, no side-effect en el constructor. El canal
crea + resume el
AudioContextenprepare()cuando la señal admitesound, de forma síncrona dentro del gesto de usuario. - Despues de crear el contexto, registra un listener de
click/touchstart/keydownvia la superficie DOM inyectada (ActiveDom.listen(ActiveDom.getDocument(), ...)) para re-resume tras suspends pasivos (tab switch, etc.). Si el canal nunca prepara una señal sonora, no instala listeners globales. - Síntesis: dos osciladores (sine + 5ª) → biquad lowpass (centroid)
→ envelope ADSR-lite. Si
roughness > 0.2, modulador AM rápido. contour(flat/ascending/descending/arc/bell) se aplica víaosc.detune.- Si la signature trae
sampleUrl, reproduce el sample (con caché deAudioBuffer) en lugar de sintetizar. - Cualquier fallo (no-AudioContext, decode failure) se absorbe — sema es ornamental.
El patrón prepare-time priming aplica en general a cualquier canal cuyo
backend tenga restricción de "primera vez ha de ocurrir en gesture":
audio, vibration, fullscreen, clipboard write. Documentado como
convención 12 en
src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md.
Política de errores
- Errores en canales NO-visuales se loguean pero no propagan. Sema es ornamental: un fallo de audio context o vibration API no debe abortar la operación del provider.
- Si el canal visual lanza, la Promise de
emitrechaza. El caller decide. - En fire-and-forget (
void semantic.emit(...)), una rejection del visual se propaga como unhandled promise — política consciente.
Política de intent por familia — SEMA_FAMILY_POLICY
Canonical:
docs/CANON.md§4 is the single source for this policy. The shape below mirrorsSEMA_FAMILY_POLICYintypes.ts; if they ever disagree, the code + canon win.
La doctrina sobre cuándo el intent es obligatorio vive en una const en
src/uix/sema/types.ts. Cada familia declara dos ejes independientes:
export const SEMA_FAMILY_POLICY = {
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' },
delegate: { intentRequirement: 'optional', intentGuidance: 'contextual' }
} as const;
intentRequirement('required' | 'optional' | 'forbidden') — el gate de compilación que da forma a la unión discriminada.'required'(solocommitysignal) →intentes OBLIGATORIO enMorfoEventSemantic.'forbidden'está reservado; ninguna familia lo usa hoy.intentGuidance('expected' | 'contextual' | 'discouraged') — hint doctrinal, sin efecto de tipo.commit/signalson'expected';contactes'discouraged'(cap. 22 §11: "el intent fuerte no debería vivir en el contacto"); el resto es'contextual'.
Cómo se aplica:
-
Compile time —
SemaEventyMorfoEventSemanticson uniones discriminadas derivadas del ejeintentRequirement. Cambiar'optional'→'required'en una familia obliga a cada morfo de esa familia a declarar intent o falla el typecheck. -
Runtime —
validateSemaEventlanza cuando un evento de una familia conintentRequirement: 'required'se construye sin intent (defensa contra morfos mal-formados o inputs externos).
Por qué esta política reemplazó al split valenced/transitional:
La doctrina canónica original asumía que sólo familias valenced (contact, commit, signal, handle) podían declarar intent. Las transitional (emerge, shift, sustain) eran intent-less por definición. La realidad UX lo contradijo: un Dialog que se abre para confirmar borrar algo destructivo carga threat en su misma aparición. La política distingue por NECESIDAD práctica de intent, no por categoría taxonómica.
La distinción valenced/transitional sigue existiendo como clasificación, pero ya no dicta las reglas de intent — eso lo hace la política.
Vocabulario canónico de verbs (SEMA_VERBS)
Canonical:
docs/CANON.md§6 +verbs.tsare the source of truth for the verb vocabulary. This section documents how sema consumes it (validation, naming shapes).
Cross-component action verbs grouped by family. El canon vive en
verbs.ts y refleja
src/docs/Disenando_lo_que_ocurre_manuscrito_completo_revisado_v2.docx
cap. 22-29 (familias) + cap. 10 (intents). morfo.events[].semantic.verb
DEBE estar en este vocabulario; morfo.events[].name debería seguir la
forma {family}-{verb}[-{variant}] para que sema/sound/haptic puedan
suscribir por verb y eidos pueda escribir selectores transversales
([data-event^="dismiss"]).
SEMA_VERBS = {
contact: ['press', 'tap', 'activate', 'focus', 'trigger', 'release'],
commit: [
'select', 'toggle', 'save', 'submit', 'confirm', 'complete', 'fail',
'cancel', 'reset', 'discard', 'delete', 'restore', 'expire',
'acknowledge',
// Verbos contextuales del libro (cap. 29 + cap. 30 + apéndice C):
'apply', 'partial', 'block', 'move', 'set', 'remove', 'reorder', 'upload'
],
signal: ['announce', 'notify', 'warn', 'alert', 'inform', 'emphasize', 'remind'],
handle: ['pick', 'carry', 'drop', 'drag', 'resize', 'reorder', 'rotate', 'scroll'],
emerge: ['present', 'dismiss', 'open', 'close', 'expand', 'collapse', 'reveal', 'hide'],
shift: ['enter-mode', 'exit-mode', 'navigate', 'route', 'step', 'return', 'context'],
sustain: [
'start', 'progress', 'loading', 'waiting', 'syncing', 'processing',
'streaming', 'pending', 'retrying', 'upload', 'end'
],
// Familia delegate (libro cap. 29): eventos donde cambia quién lleva la
// iniciativa. No requiere IA — workflows, macros, aprobaciones.
delegate: ['offer', 'plan', 'authorize', 'act', 'review', 'escalate', 'return']
};
Verbs que parecen de una familia pero pertenecen a otra per la
canónica: select/toggle/acknowledge son commit (fijan estado);
edit se expresa como shift.enter-mode (cambia régimen, no hay verb
edit en handle).
Definido en verbs.ts:SEMA_VERBS.
Naming shapes
Un morfo.events[].name puede tomar dos formas canónicas:
// Forma 1: {verb}-{variant} — head es el verbo, tail explica el matiz.
'dismiss'; // bare verb
'dismiss-outside'; // verb + variant
'close-cancel'; // verb (close) + variant (cancel)
// Forma 2: {family}-{verb} — head es la familia, tail es el verb canónico.
'commit-toggle'; // family=commit, verb=toggle
'commit-save'; // family=commit, verb=save
validateEventName(name) reconoce ambas formas y devuelve { family, verb, variant, matchesCanonical }. Usada por scripts/morfo-vocabulary-check.ts
(npm run morfo:vocabulary), que hard-falla cuando el semantic.verb
declarado en un morfo no está en el canon de su familia, y soft-warns
cuando la NAME del evento no calza con {family}-{verb}[-{variant}]
aun siendo el verb canónico. Allowlist temporal en el script para
divergencias deliberadas.
import { validateEventName } from '$uix/sema';
validateEventName('commit-toggle');
// { name: 'commit-toggle', head: 'commit', matchesCanonical: true,
// variant: 'toggle', family: 'commit', verb: 'toggle' }
validateEventName('dismiss-outside');
// { name: 'dismiss-outside', head: 'dismiss', matchesCanonical: true,
// variant: 'outside', family: 'emerge', verb: 'dismiss' }
validateEventName('frob-glob');
// { name: 'frob-glob', head: 'frob', matchesCanonical: false,
// variant: 'glob', family: undefined, verb: undefined }
Relación con Morfo y Soma
Morfodeclara los eventos semánticos del componente enmorfo.eventsProviderdecide cuándo ocurren y llama asemantic.emit(...)SomaRuntimeorquesta la secuenciaprewrite -> emit -> handler -> effectsSemaaporta el vocabulario, la normalización y la validación del dominio, y publica las ocurrencias
Dependencias
EngineSemanticno escribe atributos directamente. Orquesta hooks de canales (prepare,handle,cleanup) sin conocer los attrs DOM.- Cada canal gestiona su propia modalidad:
VisualChannel.prepare()proyectadata-event-*y luego mantiene el hold perceptivo.DomSignalProjectores el escritor DOM usado por el canal visual; escribe mediante elActiveDomrecibido desdeActiveUix.- Futuros canales (sound, haptic) accederán a sus APIs respectivas
(
AudioContext,navigator.vibrate, etc.).
- En uso normal,
ActiveUixinyecta elActiveDomenEngineSemantic. El uso directo de Sema fuera deActiveUixdebe pasar undom/projectorexplicito o elegir una degradacion documentada.
Regla de arquitectura
Morfo autoriza la semántica del componente.
Sema define el vocabulario canónico y despacha señales a los canales.
Provider decide cuándo emitir.
Cada Channel materializa la señal en su modalidad.
Ver src/uix/README.md §2.bis para la vista cross-layer y
src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md
para las convenciones doctrinales del API (single-event para
operaciones instantáneas, intent ↔ visual token resolution, sound prepare-time
priming, etc.).