From 13caaf215fb4afc65b0c8f5291b3f115155b5644 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 25 Apr 2026 13:52:17 +0200 Subject: [PATCH] docs: closed architecture (Morfo + MorfoRuntime + Provider + Effects + Sema + Dom) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/uix/README.md | 166 ++++++++++++++++++++++++------ src/uix/morfo/README.md | 89 ++++++++++++++++ src/uix/sema/README.md | 86 ++++++++++++++-- src/uix/soma/SOMA_ARCHITECTURE.md | 116 +++++++++++++++++++++ 4 files changed, 416 insertions(+), 41 deletions(-) diff --git a/src/uix/README.md b/src/uix/README.md index 0a66ff6db..68a9b58c9 100644 --- a/src/uix/README.md +++ b/src/uix/README.md @@ -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. --- diff --git a/src/uix/morfo/README.md b/src/uix/morfo/README.md index 4e13fe1fb..3ec6e045d 100644 --- a/src/uix/morfo/README.md +++ b/src/uix/morfo/README.md @@ -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: diff --git a/src/uix/sema/README.md b/src/uix/sema/README.md index b5b2ab038..26496f427 100644 --- a/src/uix/sema/README.md +++ b/src/uix/sema/README.md @@ -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 +``` + +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. diff --git a/src/uix/soma/SOMA_ARCHITECTURE.md b/src/uix/soma/SOMA_ARCHITECTURE.md index 36aecd37e..3d9346618 100644 --- a/src/uix/soma/SOMA_ARCHITECTURE.md +++ b/src/uix/soma/SOMA_ARCHITECTURE.md @@ -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`: