docs: closed architecture (Morfo + MorfoRuntime + Provider + Effects + Sema + Dom)

Reflect the architectural decisions reached on 2026-04-25 across the
layer-level READMEs:

- src/uix/README.md
  - rewrite ADom section: no longer a "broker semántico"; only DOM mutation surface
  - rewrite Sema section: vocabulary + EngineSemantic with Promise-returning emit
  - new §2.bis "Cómo se ejecuta un componente": six-piece chain with disjoint
    responsibilities (Morfo declares, Runtime transcribes, Provider supplies,
    Effects sync, Semantic emits, Dom applies)
  - update §8 dependency rules to match the closed design
  - new one-line summary in §9

- src/uix/morfo/README.md
  - new "How morfo gets executed" section: maps each morfo field to its runtime
    executor; documents trigger() sequence and provider responsibilities

- src/uix/sema/README.md
  - rewrite around the Promise contract: emit() resolves after 1 rAF
  - document lifecycle (id → write signal → wait frame → resolve → hold → cleanup)
  - error policy and the three composition scenarios with dom.apply

- src/uix/soma/SOMA_ARCHITECTURE.md
  - new §3.bis "Arquitectura cerrada" introducing MorfoRuntime as the missing
    piece between Morfo (declaration) and Provider (execution)
  - documents API V1, three commit operations, trigger() sequence, operational
    rules, and pilot order (Toggle → Collapsible → Toast → Dialog)

No code changes; this commit pins the architecture before implementation.
morfo-runtime
dev 6 months ago
parent d87421580d
commit 13caaf215f

@ -68,15 +68,20 @@ Ver: [src/uix/morfo/README.md](/G:/dev/svelte/vicen/src/uix/morfo/README.md)
### `Sema`
Vocabulario y contrato semantico.
Vocabulario semantico y canalizador de senales perceptivas.
No ejecuta sound, vibra ni CSS. Su trabajo es decir:
Su trabajo:
- que acciones existen
- que ocurrencias/eventos canónicos nombra el sistema
- como se relacionan esos nombres con el componente
- definir las familias canonicas (`contact | commit | alert | handle | emerge | sustain`)
- definir los intents canonicos (`neutral | affirm | fulfill | risk | threat`)
- exponer `EngineSemantic.emit(event)` para que el provider publique ocurrencias
- garantizar la secuencia perceptiva: escribir senal `data-event*`, esperar 1 rAF
para que CSS la observe, resolver, mantener y limpiar
En su version madura, `Sema` debe ser **vocabulario y validacion**, no runtime.
`Sema` no decide que ocurrio (eso lo decide el provider). Solo orquesta la
ocurrencia que recibe.
Ver: [src/uix/sema/README.md](/G:/dev/svelte/vicen/src/uix/sema/README.md)
### `Soma`
@ -104,15 +109,22 @@ headless del componente. Su responsabilidad es apariencia, no comportamiento.
### `ADom`
Runtime observable del DOM activo.
Runtime DOM activo de aplicacion.
No es semantic engine. No conoce Morfo, ni Sema, ni Soma, ni Eidos. Su unica
funcion es **coordinar y sincronizar mutaciones DOM**.
API publica (mutaciones):
- `app.dom.apply(change)` — aplica un paquete de attrs sobre un target
- `app.dom.remove(target, names)` — quita attrs
API publica (servicios reactivos pre-existentes):
No es un helper DOM puro ni un semantic engine. Es el broker infrastructural de
senales DOM activas:
- `viewport`, `breakpoints`, `currentBreakpoint`, `resolve`, `isAtLeast`, `matches`
- `BodyScrollLock`, `DOMContext`, `RovingFocusGroup`
- recibe emisiones de `Soma`
- publica a listeners tipados
- refleja `data-event*` en el DOM
- evita que cada engine monte su propio observer para el mismo hecho
ADom recibe instrucciones ya resueltas. No las interpreta.
Ver: [src/uix/adom/README.md](/G:/dev/svelte/vicen/src/uix/adom/README.md)
@ -133,6 +145,94 @@ No contiene el runtime activo. Ese papel pertenece a `ADom`.
---
## 2.bis Como se ejecuta un componente
La arquitectura cerrada (post-2026-04-25) define seis piezas con
responsabilidades disjuntas. Ninguna invade a la siguiente.
```
Morfo declara
MorfoRuntime transcribe
Provider aporta sources, targets y handlers
Effects sincronizan attrs derivados
EngineSemantic emite senales perceptivas
ADom aplica mutaciones DOM
```
### El reparto operativo
`Morfo` es DNA: un fichero por componente que declara `parts`, `data-*`,
`aria-*`, `role`, `keyboard`, `focus`, `events`. No ejecuta nada.
`MorfoRuntime` (en `soma/`) interpreta el morfo. Una instancia por componente
recibe del provider las fuentes de estado, los targets DOM y los handlers de
eventos. Expone:
- `partProps(part)` — devuelve solo identidad estatica del nodo (id, marker,
ref attachment). Nada mutable.
- `attachPart(part, target)` — el provider registra el nodo DOM real cuando
monta.
- `keydown(part, event)` — dispatch de teclas declaradas en `morfo.keyboard`.
- `trigger(eventName)` — orquesta la secuencia perceptiva + state.
`Provider` aporta lo que el morfo no puede inferir:
- getters reactivos para `states` y `props`
- getters reactivos para los `parts` (ids dinamicos)
- handlers sincronos para los `events`
- glue de layers ortogonales (Presence, Dismissal, ScrollLock — no son morfo)
`Effects` (registrados por el runtime al montar) escuchan cambios en los
sources y aplican los attrs derivados via `dom.apply`.
`EngineSemantic` recibe el evento desde `runtime.trigger`. Escribe la senal
`data-event*` via `dom.apply`, espera 1 rAF, resuelve, mantiene la senal el
hold configurado y limpia.
`ADom` solo aplica. No interpreta.
### La secuencia de `runtime.trigger(eventName)`
```
1. prewrite imperativo (transient markers como data-last-action)
2. await semantic.emit(event)
3. handler sincrono del provider muta state
4. effects derivan y aplican attrs estructurales (data-state, aria-*)
```
El handler muta state. Los effects ven el cambio y reescriben el DOM. ADom es
el unico escritor de attrs mutables.
### Tres escenarios de Soma
```ts
// Cambio estructural sin senal
provider.commitState(change)
// internamente: dom.apply(change)
// Cambio estructural con senal
provider.commitState(change, event)
// internamente: await semantic.emit(event); dom.apply(change)
// Senal sin cambio estructural
provider.emitEvent(event)
// internamente: void semantic.emit(event)
```
### Reglas operativas
- Lo que `dom.apply` escribe, Svelte no lo renderiza. `partProps` solo emite
identidad estatica (id, marker, ref).
- Los handlers de `events` son sincronos. Async va fuera del trigger.
- Los guards (`if (disabled) return`) van en el call-site, no dentro del
handler — si entran al handler, ya emitieron senal perceptiva.
- `Semantic` puede usar `Dom` (dependencia hacia abajo). `Dom` no conoce
`Semantic`.
- `morfo.events.commits` es descriptivo: documenta lo observable, no lo
ejecuta. La cadena causal real es handler -> state -> effect.
---
## 3. Que hace distinto a UIX
### 3.1 El contrato estructural es una capa propia
@ -321,26 +421,29 @@ pero tambien menos patrones externos que copiar. Hay que inventar con disciplina
## 8. Reglas de dependencia
UIX debe preservar una direccion clara de acoplamiento.
Version simplificada:
UIX preserva una direccion clara de acoplamiento.
```text
Morfo -> describe
Sema -> nombra y valida sobre Morfo
Soma -> implementa comportamiento y emite a ADom
ADom -> transporta y publica
Eidos -> materializa visualmente
App -> compone servicios y engines
Morfo -> declara contratos
MorfoRuntime -> interpreta morfo dentro de Soma
Provider -> aporta sources, targets, handlers
Effects -> sincronizan state -> attrs
EngineSemantic -> emite senales perceptivas (depende de Dom)
ADom -> aplica mutaciones DOM
Eidos -> materializa visualmente leyendo DOM
App -> compone servicios
```
Y, como regla general:
Reglas duras:
- `Morfo` no conoce `Soma`
- `Sema` no ejecuta engines
- `ADom` no conoce sonido ni vibracion
- `Soma` no conoce implementaciones modales concretas
- `Eidos` no duplica behavior headless
- `Morfo` no conoce `Soma`, ni codigo de runtime
- `MorfoRuntime` lee `Morfo` y depende de `Dom` y `Semantic`
- `Provider` no escribe attrs mutables al DOM directamente; los aporta como
sources al runtime
- `EngineSemantic` puede usar `Dom` (hacia abajo); `Dom` no conoce `Semantic`
- `ADom` no conoce `Morfo`, ni `Sema`, ni `Soma`, ni `Eidos`
- `Eidos` consume DOM y `data-*`, no internals de `Soma` ni `Sema`
- Lo que `dom.apply` escribe, Svelte no lo renderiza
---
@ -348,11 +451,10 @@ Y, como regla general:
Si hubiera que resumir UIX en una sola idea, seria esta:
> UIX trata la interfaz no como un componente monolitico, sino como un sistema de
> capas con contratos explicitos entre estructura, semantica, comportamiento,
> visualidad y transporte de eventos activos.
> Morfo declara, MorfoRuntime transcribe, Provider aporta, Effects sincronizan,
> Semantic emite, Dom aplica.
Esa es la apuesta.
Seis piezas, seis responsabilidades, ninguna invade a la siguiente.
---

@ -62,6 +62,95 @@ See [`types.ts`](./types.ts) for the full TypeScript shape.
---
## How morfo gets executed (architecture)
Morfo is **declarative**. By itself it doesn't render, doesn't bind events,
doesn't write to the DOM. The piece that does is `MorfoRuntime`, which lives
in `soma/` and consumes a morfo together with the provider's reactive sources.
The closed architecture (post-2026-04-25) has six pieces with disjoint
responsibilities:
```
Morfo declares
MorfoRuntime transcribes
Provider supplies sources, targets, handlers
Effects sync attrs from state
EngineSemantic emits perceptual signals
ADom applies DOM mutations
```
### What each morfo field maps to at runtime
| Morfo field | Runtime executor | Purpose |
|---|---|---|
| `parts[].data` (with `value`) | Effect of attrs | Reactive `data-*` |
| `parts[].aria` | Effect of attrs | Reactive `aria-*` |
| `parts[].role` | Effect of attrs | Stable role |
| `parts[].keyboard` | `runtime.keydown(part, event)` | Key dispatch |
| `events[].prewrite` | `trigger()` step 1 | Transient markers |
| `events[].semantic` | `trigger()` step 2 (emit payload) | Perceptual signal |
| `events[].commits` | **Nobody executes**; smoke validates | Documentation |
| `focus` | Configures FocusScope layer | Layer bootstrap |
`commits` is **descriptive**, not prescriptive. The actual causal chain is
`handler -> state mutation -> effect -> dom.apply`. The `commits` declaration
documents what an external observer will see and is checked by the smoke
suite.
### The `trigger(eventName)` sequence
```
1. prewrite imperative (data-last-action, etc.)
2. await semantic.emit(event)
3. provider's synchronous handler mutates state
4. effects derive and apply structural attrs (data-state, aria-*)
```
State is the only source of truth. The DOM is derivative.
### Provider responsibilities
The provider supplies what morfo cannot infer:
```ts
const runtime = createMorfoRuntime(morfo, {
dom: this.soma.dom,
semantic: this.soma.semantic,
states: { open: () => this.opts.open.current },
props: { disabled: () => this.opts.disabled.current },
parts: { content: () => this.contentId.current },
events: {
'open': () => { this.opts.open.current = true },
'close-cancel': () => { this.opts.open.current = false }
}
});
```
Each part-provider then renders only the static identity:
```ts
readonly props = $derived.by(() => runtime.partProps('trigger'));
// returns: { id, ref attachment, 'data-{component}-trigger': '' }
```
Everything mutable (`role` derived from prop, `aria-*`, `data-state`, `data-intent`)
is written by the runtime's effects via `dom.apply`. Svelte does not render
those attrs.
### Operational rules
- `partProps(part)` returns only static identity (id, ref, marker).
- `dom.apply` is the only writer of mutable attrs.
- Event handlers are synchronous. Async work happens before `trigger()` is called.
- Guards (`if (disabled) return`) live at the call-site, not inside the handler —
if they enter the handler, the perceptual signal already fired.
- `Semantic` may use `Dom` (downward dependency); `Dom` does not know `Semantic`.
See [src/uix/README.md](../README.md) §2.bis for the cross-layer view.
---
## Anatomy of a morfo file
Minimal template:

@ -1,6 +1,7 @@
# Sema
`Sema` define el dominio semántico canónico de UIX.
`Sema` define el dominio semántico canónico de UIX y orquesta la emisión de
señales perceptivas.
## Qué es
@ -8,7 +9,11 @@
- intents canónicos: `neutral`, `affirm`, `fulfill`, `risk`, `threat`
- normalización entre shape estructurado y label canónico
- validación mínima del dominio
- `SemanticEngine` como broker semántico pequeño
- `EngineSemantic` como canalizador de ocurrencias
`Sema` no decide qué evento ocurrió. El provider lo decide. `EngineSemantic`
recibe la ocurrencia y orquesta su materialización en el canal perceptivo
visual (DOM).
## Qué ya no es
@ -20,24 +25,87 @@ No contiene:
- sound/motion/color/presence engines
- mapa perceptivo por canal
- política global de accesibilidad por canal
- runtime DOM
Eso pertenece a capas futuras y separadas:
- `SemanticEngine` publica ocurrencias semánticas
- `ActiveDom` reflejará esas ocurrencias al DOM
- `EngineSemantic` publica ocurrencias semánticas
- `ActiveDom` materializa esas ocurrencias como `data-event*` en el DOM
- `SoundEngine`, `VibraEngine` y otros engines modales se suscribirán al engine
## El contrato `emit`
```ts
semantic.emit(event: SemanticEvent): Promise<void>
```
Una sola firma. Cubre los tres escenarios cuando se compone con `dom.apply`:
```ts
// Cambio estructural sin señal
dom.apply(change)
// Cambio estructural con señal
await semantic.emit(event)
dom.apply(change)
// Señal sin cambio estructural
void semantic.emit(event)
```
### Semántica de la Promise
`emit(event)` resuelve cuando:
- la señal `data-event*` ya fue escrita al DOM
- ha pasado **un rAF** para que CSS pueda observarla y arrancar transitions
- todavía está visible en el DOM
No resuelve antes (no hay frame para que CSS reaccione) ni después de la
limpieza (la señal ya no estaría visible cuando el commit estructural entre).
### Ciclo de vida interno de `emit`
```
1. Sema genera id/sesion del evento
2. Sema llama a dom.apply(eventSignal) // data-event, data-event-phase, data-intent
3. Sema espera 1 rAF
4. Sema resuelve la Promise // <- el caller hace su dom.apply estructural
5. Sema mantiene la señal N frames extra (hold del evento, default 1)
6. Sema llama a dom.apply(remove eventSignal)
```
### Política de errores
- Si el cambio estructural lanza tras el `await`, no afecta a Sema. Su trabajo
(escribir señal + esperar frame) ya terminó. La cleanup pasa igual.
- Si Sema falla escribiendo la señal, la Promise rechaza. El caller decide si
aborta el cambio estructural o lo aplica igual.
- En el escenario fire-and-forget (`void semantic.emit(event)`), una rejection
se propaga como unhandled promise — política consciente.
## Relación con Morfo y Soma
- `Morfo` declara los eventos semánticos del componente en `morfo.events`
- `Soma` decide cuándo ocurren y llama a `SemanticEngine.publish(...)`
- `Sema` aporta el vocabulario, la normalización y la validación de ese dominio
- `Provider` decide cuándo ocurren y llama a `semantic.emit(...)`
- `MorfoRuntime` 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
- `Sema` puede usar `Dom` (`semantic.emit` llama a `dom.apply` para escribir
`data-event*`). Dependencia hacia abajo, legítima.
- `Dom` no conoce `Sema`.
- `Sema` recibe `dom` por construcción, no lo importa duro de `$uix/adom`.
## Regla de arquitectura
`Morfo` autoriza la semántica del componente.
`Sema` define el vocabulario canónico.
`Sema` define el vocabulario canónico y orquesta la señal perceptiva.
`Provider` decide cuándo emitir.
`Dom` aplica.
`SemanticEngine` publica ocurrencias.
Ver [src/uix/README.md](../README.md) §2.bis para la vista cross-layer.

@ -143,6 +143,122 @@ Todas las props de todos los componentes llevan JSDoc en `types.ts`. Cada prop:
Cada componente se compara con ark-ui, bits-ui y radix-ui antes de implementar. Se documentan las props que otros tienen y soma no, con justificacion.
## 3.bis Arquitectura cerrada (post-2026-04-25)
El reparto de responsabilidades entre Morfo, Soma, Sema y ADom esta cerrado en
seis piezas con responsabilidades disjuntas:
```
Morfo declara
MorfoRuntime transcribe (vive en soma/)
Provider aporta sources, targets y handlers
Effects sincronizan attrs derivados
EngineSemantic emite senales perceptivas
ADom aplica mutaciones DOM
```
### MorfoRuntime — la pieza nueva
`MorfoRuntime` es la pieza que faltaba entre `Morfo` (declaracion) y
`Provider` (ejecucion). Lee el morfo y produce el comportamiento.
Una instancia por componente:
```ts
const runtime = createMorfoRuntime(morfo, {
dom: this.soma.dom,
semantic: this.soma.semantic,
states: { open: () => this.opts.open.current },
props: { disabled: () => this.opts.disabled.current },
parts: { content: () => this.contentId.current },
events: {
'open': () => { this.opts.open.current = true },
'close-cancel': () => { this.opts.open.current = false }
}
});
```
API V1:
- `runtime.partProps(part)` — devuelve `{ id, ref, marker }`. Solo identidad estatica.
- `runtime.attachPart(part, target)` — el provider registra el nodo DOM al montar.
- `runtime.keydown(part, event)` — dispatch de teclas declaradas en `morfo.keyboard`.
- `runtime.trigger(eventName)` — orquesta la secuencia perceptiva + state.
### Provider en el modelo nuevo
El provider deja de tener `resolveMorfoProps` con bindings repetidos. Solo
aporta:
- getters reactivos para `states`, `props`, `parts`
- handlers sincronos para los `events`
- glue de layers ortogonales (Presence, Dismissal, ScrollLock)
Cada part-provider devuelve solo identidad:
```ts
readonly props = $derived.by(() => runtime.partProps('trigger'));
```
### Tres operaciones que cubren todos los escenarios
```ts
// Cambio estructural sin senal
provider.commitState(change);
// Cambio estructural con senal
provider.commitState(change, event);
// Senal sin cambio estructural
provider.emitEvent(event);
```
Internamente:
```ts
async commitState(change, event?) {
if (event) await this.soma.semantic.emit(event);
this.soma.dom.apply(change);
}
emitEvent(event) {
void this.soma.semantic.emit(event);
}
```
### La secuencia de `runtime.trigger(eventName)`
```
1. prewrite imperativo (transient markers como data-last-action)
2. await semantic.emit(event)
3. handler sincrono del provider muta state
4. effects derivan y aplican attrs estructurales (data-state, aria-*)
```
Los effects del runtime escuchan los sources reactivos y reaplican attrs cada
vez que el estado cambia. ADom es el unico escritor de attrs mutables.
### Reglas operativas
- `partProps(part)` solo emite identidad estatica. Lo mutable lo escribe ADom.
- Lo que `dom.apply` escribe, Svelte no lo renderiza desde `partProps`.
- Event handlers son sincronos. Async va antes del trigger.
- Guards (`if (disabled) return`) van en el call-site, no dentro del handler.
- `Semantic` puede usar `Dom` (hacia abajo); `Dom` no conoce `Semantic`.
- `morfo.events.commits` es descriptivo, no ejecutable. El smoke valida.
### Pilotaje (orden incremental)
1. **Toggle** — primer caso, solo `partProps`.
2. **Collapsible** — anade `keydown`.
3. **Toast** — primer test real de `trigger()` con `intent`.
4. **Dialog** — al final, cuando layers + portal ya esten validados.
Ver tambien:
- [src/uix/morfo/README.md](../morfo/README.md) — declaracion y ejemplos
- [src/uix/sema/README.md](../sema/README.md) — contrato de `emit`
- [src/uix/adom/README.md](../adom/README.md) — `dom.apply`
## 4. Modelo de componente
La forma base de soma es `Componente.Parte`:

Loading…
Cancel
Save

Powered by TurnKey Linux.