You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/sema
dev 6476efffe7
Purge visual slices from Sema
5 months ago
..
chans Purge visual slices from Sema 5 months ago
components Purge visual slices from Sema 5 months ago
projection Refactor active uix architecture 5 months ago
README.md Purge visual slices from Sema 5 months ago
define-engine-semantic.ts Refactor active uix architecture 5 months ago
durations.test.ts sema + morfo: per-event hold override (declarative in morfo) 5 months ago
durations.ts sema + morfo: per-event hold override (declarative in morfo) 5 months ago
emit.test.ts Purge visual slices from Sema 5 months ago
engine.test.ts Refactor active uix architecture 5 months ago
engine.ts Purge visual slices from Sema 5 months ago
errors.ts Refactor active uix architecture 5 months ago
event.test.ts uix: drawer/popover/toast/accordion eidos wrappers + sema packs + /uix docs scaffold 5 months ago
event.ts uix: drawer/popover/toast/accordion eidos wrappers + sema packs + /uix docs scaffold 5 months ago
exports.ts Purge visual slices from Sema 5 months ago
index.ts sema: add runtime foundation and dialog integration 6 months ago
refactorizacion_codex.md Refactor active uix architecture 5 months ago
resolver.test.ts Purge visual slices from Sema 5 months ago
resolver.ts Purge visual slices from Sema 5 months ago
sema-map.ts Purge visual slices from Sema 5 months ago
signal.ts sema/morfo/eidos: typed selector builder + dialog wrapper + emerge color 5 months ago
stamp.ts Refactor active uix architecture 5 months ago
types.ts Purge visual slices from Sema 5 months ago
validation.ts Refactor active uix architecture 5 months ago
verbs.test.ts sema/morfo/eidos: align with GUIA_IMPLEMENTACION_SEMAUIX (canonical guide) 5 months ago
verbs.ts sema/morfo/eidos: align with GUIA_IMPLEMENTACION_SEMAUIX (canonical guide) 5 months ago

README.md

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: contact, commit, signal, handle, emerge, shift, sustain
  • intents canónicos: neutral, affirm, fulfill, risk, threat, loss
  • política de intent por familia (SEMA_FAMILY_POLICY): determina cuándo el intent es obligatorio ('expected'), opcional ('allowed' / 'optional')
  • normalización entre shape estructurado y label canónico
  • validación mínima del dominio
  • EngineSemantic como 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).

Handoff 2026-05-13

Sema ya tiene contrato explicito de DOM: si el canal visual esta activo, EngineSemantic debe recibir dom o projector. Si se construye desde ActiveUix, recibe el dom de ActiveUix; si se usa directamente fuera de UIX, el integrador debe pasar un writer explicito o usar visual:false. Sema no cae a escrituras DOM directas por defecto.

Tambien queda por decidir si emit debe seguir siendo secuencial estricto de forma global o si la secuenciacion pertenece al evento/morfo. No cambiar esto sin documentar antes la tabla de contratos minimos en ../active_architecture.md.

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:

  • EngineSemantic mantiene 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:

  1. el VisualChannel.prepare() proyectó data-event* al DOM
  2. los atributos vivieron en el DOM durante el hold configurado
  3. los atributos ya fueron retirados (cleanup completo)
  4. 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, vibra) 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, vibra) → 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-*)
├── sema-map.ts           SEMA_MAP + SemaChannelSignatures registry + Sema
├── 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 6 capas

El engine, en cada emit:

  1. Ejecuta los prepare de canales. El VisualChannel proyecta los tokens semánticos data-event, data-event-family, data-event-intent, data-event-phase, data-event-id al signal.target. Estos son los tokens que la cascade y la CSS de eidos leen.
  2. Llama a resolveSignature(signal, opts) que aplica la cascada de 6 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. soundPack URL     — SEMA_MAP.soundPack[<family>-<intent>]
4. MORFO overrides   — signal.overrides + signal.channels
                       (numbers REPLACE por defecto — son set values)
5. RUNTIME overrides — engineOpts.overrides.runtime (path-based globals;
                       baked into el map en el constructor; numbers REPLACE)
6. 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.
  1. Despacha a cada canal con la signature resuelta.
  2. Awaita el VisualChannel (que aporta el hold).
  3. Ejecuta el cleanup de los handles devueltos por prepare.

Convención de overrides vs deltas — números:

  • Capa 2 (intent.deltas): pitch: -200 significa "restar 200 al pitch base". Modificadores compositivos.
  • Capas 4, 5, 6 (overrides): pitch: 720 significa "set pitch a 720". Como CSS — gain: 0.4 no 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:

  • partKebab is checked against morfo.parts[].kebab.
  • eventName is checked against morfo.events[].name.
  • eventFamily / eventIntent are typed against the canonical sema unions.
  • state / aria / pseudo accept 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';

export const dialogSema: Sema = {
	name: 'dialog',
	preloadSamples: ['/sounds/dialog/saved.wav', '/sounds/dialog/failed.wav'],
	cascade: [
		{
			selector: '[data-dialog-content][data-event-intent="threat"]',
			sound: { sampleUrl: '/sounds/dialog/failed.wav' },
			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() en el constructor del engine — los WAVs quedan decodeados antes del primer emit.

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:

  1. signal.hold — override imperativo per llamada.
  2. effective.hold — viene del resolver desde SEMA_MAP.families[*].hold.
  3. Family fallback table (SEMA_DURATIONS + label per familia) — defensa si una familia externa no declara hold.
  4. defaultHold global 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: vibra el 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-reduced-sound="true"] [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 accesibilidad priority: 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 AudioContext con GainNode master por engine.
  • Prepare-time priming, no side-effect en el constructor. El canal crea + resume el AudioContext en prepare() cuando la señal admite sound, de forma síncrona dentro del gesto de usuario.
  • Despues de crear el contexto, registra un listener de click / touchstart / keydown en document (capture phase) 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ía osc.detune.
  • Si la signature trae sampleUrl, reproduce el sample (con caché de AudioBuffer) 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/sema-implementation-guide.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 emit rechaza. 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

La doctrina sobre cuándo el intent es obligatorio vive en una const en src/uix/sema/types.ts. Tres niveles:

export const SEMA_FAMILY_POLICY = {
	contact: { intentPolicy: 'allowed' }, // OPCIONAL — neutral por default
	commit: { intentPolicy: 'expected' }, // REQUERIDO — toda consumación es evaluativa
	signal: { intentPolicy: 'expected' }, // REQUERIDO — alarmas son inherentemente evaluativas
	handle: { intentPolicy: 'allowed' }, // OPCIONAL — drag/scrub suelen ser neutrales
	emerge: { intentPolicy: 'optional' }, // OPCIONAL — Dialog que abre para confirmar threat sí declara
	shift: { intentPolicy: 'optional' },
	sustain: { intentPolicy: 'optional' }
} as const;

Cada entry es un objeto — anticipa otros campos doctrinales por familia (default sequence, channels permitidos, hold preferences, gesture phases, …). Cuando se añadan se ubicarán dentro del mismo objeto sin reestructurar.

Cómo se aplica:

  1. Compile time — SemaEvent y MorfoEventSemantic son discriminated unions derivadas de la const. Cambiar 'optional' → 'expected' en contact (por ejemplo) hace que cada morfo de toggle/switch/etc. tenga que declarar intent o falle el typecheck.

  2. Runtime — validateSemaEvent lanza SemaInvariantError cuando un evento de family 'expected' 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)

Cross-component action verbs grouped by family per src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md §1.3. morfo.events[].name debería alinear con este vocabulario para que sema/sound/vibra 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',
		'cancel',
		'complete',
		'fail',
		'delete',
		'restore',
		'reset',
		'discard',
		'expire',
		'acknowledge',
		'set',
		'remove',
		'reorder'
	],
	signal: ['announce', 'notify', 'warn', 'alert', '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',
		'end'
	]
};

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 }. Advisory — no rechaza morfos, solo flagea drift para tooling y revisión.

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

  • Morfo declara los eventos semánticos del componente en morfo.events
  • Provider decide cuándo ocurren y llama a semantic.emit(...)
  • SomaRuntime orquesta la secuencia prewrite -> emit -> handler -> effects
  • Sema aporta el vocabulario, la normalización y la validación del dominio, y publica las ocurrencias

Dependencias

  • EngineSemantic no escribe atributos directamente. Orquesta hooks de canales (prepare, handle, cleanup) sin conocer los attrs DOM.
  • Cada canal gestiona su propia modalidad:
    • VisualChannel.prepare() proyecta data-event-* y luego mantiene el hold perceptivo.
    • DomSignalProjector es el escritor DOM usado por el canal visual; escribe mediante el ActiveDom recibido desde ActiveUix.
    • Futuros canales (sound, vibra) accederán a sus APIs respectivas (AudioContext, navigator.vibrate, etc.).
  • En uso normal, ActiveUix inyecta el ActiveDom en EngineSemantic. El uso directo de Sema fuera de ActiveUix debe pasar un dom/projector explicito 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/sema-implementation-guide.md para las convenciones doctrinales del API (Parte IV: single-event para operaciones instantáneas, intent ↔ visual token resolution, sound prepare-time priming, etc.).

Powered by TurnKey Linux.