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/docs/architecture/soma-architecture.md

1093 lines
46 KiB

---
title: Soma Architecture — the deep reference
type: reference
audience: human + agent
authority: E1 architecture — soma's deep reference: runtime, layers, contracts, helpers, anti-patterns
status: current
source: migrated from src/uix/soma/SOMA_ARCHITECTURE.md (2026-07-02, docs-book F7.2)
---
# Soma Architecture
The architectural reference for `src/uix/soma`.
`soma` is the headless-primitives layer of the UIX system. It implements
behavior, accessibility and part composition; visual presentation is eidos's
responsibility. Every component is first described as a **morfo** (the
declarative contract) and soma materializes it through concrete state classes
that register their parts with `SomaRuntime`.
## 1. Purpose
`soma` is UIX's headless-primitives layer: behavior, accessibility and part
composition, on top of which complex interfaces are built without repeating
context, focus, keyboard, `aria-*`, `data-*`, state synchronization,
animations and floating positioning. It is neither a visual nor a product
layer; the visual layer decides the look and feel.
The narrative purpose and the membership criteria (which component belongs in
soma, which stays out) live in [`architecture/soma.md`](./soma.md) §1–§2.
This document is the deep architectural reference.
## 2. Layer architecture
```
soma → headless: behavior, accessibility, data-* contracts, context, services
eidos → visual: tokens, CSS, themes, recipes, reactions to data-event-*
events → perception: sound/haptic/hold and dispatch of semantic occurrences
app → product: final composition, content, business logic
```
Each layer has strict responsibilities:
### soma provides
- behavior (keyboard, focus, dismiss, scroll lock)
- accessibility (ARIA, roles, live regions)
- stable, validated `data-*` contracts
- context and part composition
- runtime services (`langs`, `format`, `logger`)
- the animation system (presence, data-starting/ending-style, onComplete)
- floating positioning (the in-house engine: `layers/floating` + `$ethereal`)
### The visual layer provides
- appearance (tokens, colors, typography, spacing)
- visual tone (light/dark themes, variants)
- opinionated design decisions (sizes, recipes)
- CSS motion and visual reactions to `data-event-*`
- responsive design
### The visual layer NEVER
- imports soma's internal state classes
- depends on incidental DOM structure
- accesses private properties
- duplicates behavior soma already solves
- uses `data-*` outside the published contracts
The boundary is the `data-*` attrs and the CSS variables soma exposes.
## 3. Design principles
### 3.1 The developer doesn't need to know the internals
The layers, the reactive system, the floating engine — they are internal
implementation. The component developer interacts with:
- concrete state classes + `SomaRuntime`
- the `Soma` class for services
- hierarchical barrel imports (`import { Dialog } from '$soma/components'`)
- explicit subpaths when public helpers are needed (`$soma/provider`,
`$soma/keyboard`, `$soma/runtime.svelte`)
### 3.2 One pattern, not three
Every component follows the same pattern:
1. A concrete state class registers its parts with `SomaRuntime.part(...)`
2. A thin `.svelte` wrapper converts props → Active/State
3. Derived props via `$derived.by` + `runtimePart.assert`
4. Context for parent–child communication
DOM-less roots use `ProviderOpts` (optional ref); parts with DOM use
`WithRefOpts`. The only exceptions are declarative parts that may live
outside their provider (`AnnounceRegion`, `FeedSentinel`): if they find a
provider they reuse its `runtime`; otherwise they create their own runtime
from the current scope's `Soma`.
### 3.3 Layers as behaviors, not as wrappers
Layers are instantiated in the Provider's constructor and expose `.props` for
merging. There is no wrapper-component nesting in templates.
```ts
// Correct: integrated behaviors
readonly focusScope = FocusScope.use({...});
readonly dismissal = Dismissal.use({...});
readonly props = $derived.by(() => this.runtimePart.assert({
...this.runtimePart.props,
...this.focusScope.props,
...this.dismissal.props,
}));
```
```svelte
<!-- Incorrect: wrapper nesting (the terra pattern) -->
<ScrollLock>
<FocusScope>
<DismissibleLayer>
{content}
</DismissibleLayer>
</FocusScope>
</ScrollLock>
```
### 3.4 Soma is a class, not a configuration
`Soma` is the framework's runtime identity. It is not a configuration file —
it is the root object that provides services via context.
```ts
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// In the wrapper: the prop and the preference resolve here
dir: activeDir(() => dir, soma),
// In a Provider: the default lands once
readonly soma = Soma.require();
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
readonly resolvedDir = $derived.by<Direction>(() => this.opts.dir.current ?? 'ltr');
```
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`soma.prefs.getDir()` is one link of that chain, never the whole of it: a
provider that calls it directly drops the consumer's `dir` prop.
Contract: [`canon/direction-contract.md`](../canon/direction-contract.md).
### 3.5 The data-\* attrs are a public contract
The `data-*` attrs are the boundary between soma and the visual layer.
Changing them is a breaking change.
Convention (mandatory, no exceptions):
- provider: `data-{component}` (not `data-{component}-provider`, **never
`data-soma-*`**)
- part: `data-{component}-{part}`
- state: `data-state`, `data-disabled`, `data-side`, `data-align`,
`data-orientation`
- animation: `data-starting-style`, `data-ending-style`
- nesting: `data-nested`, `data-nested-open`
The names are emitted by the morfo compiler that `SomaRuntime` consumes.
`createAttrs(morfo)` remains a typed helper for `querySelector` and tooling —
not a registration system, and it writes nothing to the DOM. Any CSS
selector, README string or snippet must match those generated names exactly.
The contract validator (`assertContract`) only verifies enumerated values,
not names or presence — name consistency is the component author's
responsibility (checklist item 27).
### 3.6 Base accessibility is not delegated
soma solves ARIA by default. The consumer doesn't need to add `role`,
`aria-modal`, `aria-expanded`, `aria-controls`, etc. — the Provider generates
them.
Functional text resolves via `langs.ts()` with an idlangref. The morfo
declares its text slots in `morfo.texts` (idlangrefs) and references them
with `v.translationRef(...)`; the multilingual catalog lives in
`src/uix/langs/components/{kebab}.ts`. Shared text like close/cancel/save
lives in `common.*` and is referenced with `v.commonRef(...)` or an absolute
idlangref. A per-component `langs.ts` remains an optional convenience for
imperative constants, not the canonical catalog.
### 3.7 Global DOM via ActiveDom
Soma uses the scope's `ActiveDom` for UIX-managed writes, `document/window`
listeners, global queries, imperative focus and window scrolling. There is no
`soma/events` façade: `dom.listen(...)` is the canonical surface for
registering listeners with cleanup.
Local reads of an owned element (`contains`, `closest`,
`getBoundingClientRect`, `clientWidth`, `scrollTop`) are not wrapped in
`ActiveDom`; they are part of the component's local behavior.
### 3.8 Props documented, mandatorily
Every prop of every component carries JSDoc in `types.ts`. Each prop: a
description, `@default`, behavior notes.
### 3.9 Comparison with references
Every component is compared against ark-ui, bits-ui and radix-ui before
implementation. Props others have and soma doesn't are documented, with
justification.
## 3.bis The closed architecture (post-2026-04-25)
The split of responsibilities between Morfo, Soma, Sema and ADom is closed in
six pieces with disjoint responsibilities:
```
Morfo declares
SomaRuntime transcribes (lives in soma/)
Provider supplies sources, targets and handlers
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
Render bag re-derives attrs from state (Svelte renders them)
EngineSemantic dispatches signals to perceptual channels
VisualChannel materializes the signal in the DOM (data-event*, hold, cleanup)
ADom applies DOM mutations (the structural commit)
```
### SomaRuntime — the missing piece
`SomaRuntime` is the piece that was missing between `Morfo` (declaration) and
`Provider` (execution). It reads the morfo and produces the behavior.
One instance per component:
```ts
readonly soma = Soma.require();
readonly runtime = this.soma.runtime(morfo, {
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;
}
}
});
```
V1 API:
- `runtime.part(part, opts)` — the single public API to register a part.
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
Returns the handle (`props`, `resolveProps`, `assert`).
- `runtime.partProps(part)` — the render bag: static identity (id, marker,
`data-archetype?`, dir, ref attachment) PLUS the full morfo contract —
`staticAttrs` (role, literals) and every dynamic plan resolved against the
part's registered sources. It is the SINGLE attr pipeline (P0 fase C, audit
2026-08-26): the same values server-render and re-derive through Svelte's
reactivity. The old client-only `syncAttrs` effect and the separate
`renderProps()` accessor are gone — the bag absorbed both.
- `runtime.keydown(part, event)` — dispatches keys declared in
`morfo.keyboard`.
- `runtime.trigger(eventName)` — orchestrates the perceptual + state
sequence.
feat(morfo,soma): la emisión deja de aceptar un elemento anónimo El defecto que tres auditorías encontraron y ninguna cerró no era que N eventos redirigieran sin declararlo: era que la API de emisión aceptaba un HTMLElement suelto, compensando que el registro de partes perdía la identidad de instancia. Censar y arreglar a mano reproduce el modo de fallo. Esto lo hace inexpresable. F1 — SomaRuntime<M> genérico, SIN default. Nombres de evento, kebabs de parte y claves de events/actions tipados contra la declaración (EventNameOf / PartKebabOf / ActionNameOf, los extractores que semaSelector ya usaba del otro lado). 104 anotaciones migradas + 3 wrappers que devolvían tipado pero tomaban sources ANCHO. SomaRuntime<Morfo> queda sólo en la tabla de contratos. F2 — registro por instancia: Map<part, PartRegistration[]>, pertenencia mantenida por el stream del attachment (poda al desmontar, re-entrada al remontar), resolución liveReg = la instancia VIVA más nueva. Mata de paso la clase "la más nueva muerta ensombrece a una viva". F3 — tres formas legales de destino: el target declarado; el anclaje por instancia (SomaRuntimePart.trigger, tipado por EventNameTargeting, + runtime.partInstance por identidad de referencia); y targetFallback, la cadena por estado de montaje declarada en el morfo y resuelta por resolveEmitTarget — LA resolución única para validación, emit y foco a11y, que ya no pueden discrepar. ~30 componentes migrados; censo de targetOverride 148 -> 89. F4 — A-36 tenía un superviviente. Medido en dropdown-menu: bajo `pre` el `open` no colisionaba con el contact-activate del Button compuesto, se PERDÍA entero porque su content no había montado. Aplicada la receta de float-panel a dropdown-menu Y context-menu. Ahora: contact-activate en trigger, open en content — dos superficies, ambas expresando. Submenús mudos, tres componentes. No era cableado: el morfo no declaraba el evento. onion-menu recibe emerge-expand/emerge-collapse (la forma de TreeView: un nivel revela sus hijos en el sitio); dropdown-menu y context-menu reciben sub-open/sub-close sobre sub-content, distintos del open raíz a propósito porque el sub es su propia superficie flotante. Nacen con la receta A-36 para no repetirla en código nuevo. chronos — declaraba 26 partes y registraba UNA; 17 role= a mano y el ARIA declarado no llegaba nunca al DOM. Se anula la mitad sobre-extendida de la decisión de 29ffe902a: la reutilización sigue viajando por el motor puro, pero la ESTRUCTURA se compone como en Table y Calendar. C1 (las 6 partes que emiten, chronos con cero targetOverride) + C2 (8 singletons) + C3 8/9. Registrar una parte DESTAPA declaraciones falsas, tres veces en un pase: el chip declaraba defaultElement button y ponía type= sobre un div (anida el asa de resize); grid-row declaraba archetype item y pintó la semana entera como fila de lista; y un aria por propRef desaparece en SILENCIO si el provider no publica la fuente. En los tres el arreglo fue la DECLARACIÓN, no el DOM. Verificado: check 74 = línea base medida con stash, DELTA CERO sostenido en cada lote · 458 pasan (chronos+morfo+sema) · navegador limpio en las tres vistas de chronos, con el contrato ARIA vivo por primera vez (aria-labelledby resolviendo a «junio de 2026», aria-readonly/disabled que no se habían emitido nunca). Handoff en docs/process/CONTINUE-perceptual-surface.md. Aparte, y decidido por el autor: docs/process/PLAN-event-name-normalization.md — prefijo de familia siempre; 36 de 252 sin prefijo, cero ambiguos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### The runtime is typed by ITS morfo (2026-08-10)
`SomaRuntime<M>` carries the morfo type, with **no default argument**: part
kebabs (`part` / `partProps` / `keydown` / `partRef`), event names
(`trigger`), and the `events` / `actions` handler keys in
`SomaRuntimeSources<M>` are all checked against the declaration
(`PartKebabOf<M>` / `EventNameOf<M>` / `ActionNameOf<M>`, the same extractors
`semaSelector` uses on the consumption side). A provider annotates
`SomaRuntime<typeof xxxMorfo>`; a wrapper that creates a runtime for one
morfo narrows its sources the same way (`createMenuDialRuntime`).
Why no default: an untyped runtime is an anonymous emission surface — an
event name or handler key the morfo never declared compiles, runs, and every
consumer written FROM the morfo (sema packs, eidos recipes) aims at something
that never happens. That is the drift class three audits measured (S-12,
S-08/S-15, A-36) and the pack census guards on the consumption side; the
emission side is closed at the type level instead of by census.
`SomaRuntime<Morfo>` (the widened instantiation) re-opens it — the only
honest use is the layer-contract table in `src/uix/contracts.ts`; anywhere
else, treat it as drift.
### The Provider in the new model
The provider no longer has local morfo-resolution helpers. It only supplies:
- reactive getters for `states`, `props`, `parts`
- synchronous handlers for the `events`
- the glue for orthogonal layers (Presence, Dismissal, ScrollLock)
Each part-provider keeps the handle returned by `runtime.part(...)`:
```ts
readonly runtimePart = runtime.part('trigger', {
id: opts.id,
ref: opts.ref,
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
owner: this
});
readonly props = $derived.by(() => this.runtimePart.props);
```
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### Three operations covering every scenario
```ts
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// Structural change without a signal
provider.commitState(change);
// Structural change with a signal
provider.commitState(change, event);
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// Signal without structural change
provider.emitEvent(event);
```
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Internally:
fix(uix): la auditoría del sistema — lo que los guards no veían Auditoría clean-room de todo ActiveUIX (excluido `web/`), componente a componente. Lo que sale de aquí no es una lista de bugs: es un patrón. El framework validaba que lo escrito fuese VÁLIDO, no que lo declarado se CUMPLIESE — y sus guards fallaban ABIERTOS. ## El colapso de las uniones de props (95 → 0) Un `Props` de eidos es `{ …props propias… } & <atributos nativos>`. Cuando el elemento declara un atributo homónimo, la intersección funde ambos y una unión estrecha contra el `string` nativo COLAPSA a `string`. Causa: `Without<T, U> = Omit<T, keyof U>` invocado como `Without<T, {}>` — `Omit<T, never>`, un no-op — 433 veces en soma; sólo 3 con argumento real. Invisible para `svelte-check`: ensanchar un tipo no es un error, es una garantía perdida. Medido: 95 props en 72 componentes. `<Avatar color="nonsense">` compilaba. `ComboboxInput.size` chocaba con el `<input size>` numérico y era inusable. Migrado con codemod sobre AST (nunca regex) a `Own & Omit<Nativos, keyof Own>`: 92 tipos en 73 ficheros + carousel a mano. `check` no se movió. Garantía nueva: `eidos/prop-surface.test.ts` (PROP-1) compara los literales de la anotación del autor contra los de la propiedad pública. Verificado que falla reintroduciendo el defecto. ## Los cuatro guards que fallaban abiertos - `translations:check` crasheaba en CADA ejecución de su historia — un stripper de comentarios borraba `//` dentro de strings. Sustituido por import dinámico. Al arrancar destapó 8 slots `texts` sin traducción. - `soma-attr-audit` agotaba el timeout de 5 s: sin veredicto, verde por omisión. - `component-audit` D-7.4 hacía `continue` mudo cuando el tipo no resolvía. Ahora resuelve con el checker de TypeScript (`scripts/prop-unions.ts`): puntos ciegos de 124 → 3. - `component-audit` R-1.1: el regex casaba `[data-motion='reduce']` y daba PASS por el motivo equivocado. Regla adoptada: un guard que no puede evaluar TIENE que decirlo. El informe lleva ahora bloque «Not verified» y recuento en el resumen. ## D-1 · tooltip y D-2 · card, cableados `tooltip` declaraba 3 eventos `emerge` que nadie emitía. Ahora emiten; `present` pasa a `sequence: 'post'` — con `'pre'` el hold de ~240 ms gateaba el montaje del propio overlay. `card` declaraba `commit-select` sin emisor posible (scope sin soma). Puente headless en `soma/components/card/` con la forma ya establecida por `menu-dial` / `onion-menu`: eidos posee estado y render, soma posee sólo el `SomaRuntime` que emite. ## Documentación: 22 mentiras corregidas `docs/` afirmaba guards inexistentes (`NO_MISSING_PROVIDER_TESTS`), APIs con firma equivocada y un modelo de Motion que el código no implementa. Corregido en CANON, arquitectura, glosario, theming/motion, checklist y los README de `motion` / `callout` / `arts/motion`. ## Además - CardGroup: la descripción se metía en la primera celda del grid. - Motion: `data-state` siempre estampado, salida real en `leave()`, token fantasma `--motion-stagger-each-default` eliminado. - `engine-motion`: `handoffState` Map → WeakMap (fuga por nodo). - `mockup`: primitivo crudo → token de rol (R-4.6). - 5 catálogos de traducción que faltaban. Handoff: `docs/process/CONTINUE-audit-2026-07-29.md`. Batería: check 0 errores en src · vitest server 3701/3701 · docs:check 0/0 · component:audit 161 PASS / 2 NEEDS-WORK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```ts
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
async commitState(change, event?) {
if (event) await this.soma.events?.emit(event);
this.soma.dom.apply(change);
}
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
emitEvent(event) {
void this.soma.events?.emit(event);
}
```
fix(uix): la auditoría del sistema — lo que los guards no veían Auditoría clean-room de todo ActiveUIX (excluido `web/`), componente a componente. Lo que sale de aquí no es una lista de bugs: es un patrón. El framework validaba que lo escrito fuese VÁLIDO, no que lo declarado se CUMPLIESE — y sus guards fallaban ABIERTOS. ## El colapso de las uniones de props (95 → 0) Un `Props` de eidos es `{ …props propias… } & <atributos nativos>`. Cuando el elemento declara un atributo homónimo, la intersección funde ambos y una unión estrecha contra el `string` nativo COLAPSA a `string`. Causa: `Without<T, U> = Omit<T, keyof U>` invocado como `Without<T, {}>` — `Omit<T, never>`, un no-op — 433 veces en soma; sólo 3 con argumento real. Invisible para `svelte-check`: ensanchar un tipo no es un error, es una garantía perdida. Medido: 95 props en 72 componentes. `<Avatar color="nonsense">` compilaba. `ComboboxInput.size` chocaba con el `<input size>` numérico y era inusable. Migrado con codemod sobre AST (nunca regex) a `Own & Omit<Nativos, keyof Own>`: 92 tipos en 73 ficheros + carousel a mano. `check` no se movió. Garantía nueva: `eidos/prop-surface.test.ts` (PROP-1) compara los literales de la anotación del autor contra los de la propiedad pública. Verificado que falla reintroduciendo el defecto. ## Los cuatro guards que fallaban abiertos - `translations:check` crasheaba en CADA ejecución de su historia — un stripper de comentarios borraba `//` dentro de strings. Sustituido por import dinámico. Al arrancar destapó 8 slots `texts` sin traducción. - `soma-attr-audit` agotaba el timeout de 5 s: sin veredicto, verde por omisión. - `component-audit` D-7.4 hacía `continue` mudo cuando el tipo no resolvía. Ahora resuelve con el checker de TypeScript (`scripts/prop-unions.ts`): puntos ciegos de 124 → 3. - `component-audit` R-1.1: el regex casaba `[data-motion='reduce']` y daba PASS por el motivo equivocado. Regla adoptada: un guard que no puede evaluar TIENE que decirlo. El informe lleva ahora bloque «Not verified» y recuento en el resumen. ## D-1 · tooltip y D-2 · card, cableados `tooltip` declaraba 3 eventos `emerge` que nadie emitía. Ahora emiten; `present` pasa a `sequence: 'post'` — con `'pre'` el hold de ~240 ms gateaba el montaje del propio overlay. `card` declaraba `commit-select` sin emisor posible (scope sin soma). Puente headless en `soma/components/card/` con la forma ya establecida por `menu-dial` / `onion-menu`: eidos posee estado y render, soma posee sólo el `SomaRuntime` que emite. ## Documentación: 22 mentiras corregidas `docs/` afirmaba guards inexistentes (`NO_MISSING_PROVIDER_TESTS`), APIs con firma equivocada y un modelo de Motion que el código no implementa. Corregido en CANON, arquitectura, glosario, theming/motion, checklist y los README de `motion` / `callout` / `arts/motion`. ## Además - CardGroup: la descripción se metía en la primera celda del grid. - Motion: `data-state` siempre estampado, salida real en `leave()`, token fantasma `--motion-stagger-each-default` eliminado. - `engine-motion`: `handoffState` Map → WeakMap (fuga por nodo). - `mockup`: primitivo crudo → token de rol (R-4.6). - 5 catálogos de traducción que faltaban. Handoff: `docs/process/CONTINUE-audit-2026-07-29.md`. Batería: check 0 errores en src · vitest server 3701/3701 · docs:check 0/0 · component:audit 161 PASS / 2 NEEDS-WORK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### The `runtime.trigger(eventName)` sequence
```
1. imperative prewrite (transient markers like data-last-action)
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
2. events.emit(event) → { id, settled }, dispatched, NOT awaited
3. the provider's synchronous handler mutates state — same tick
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
4. the render bag re-derives structural attrs (data-state, aria-*) and
Svelte renders them
```
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
The signal precedes the commit; it does not gate it (D-full, 2026-09-15). Step 2
used to be awaited, which meant the state changed only after the ~240 ms hold.
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
The bag reads rune-backed sources, so every spread re-derives when state
changes — server-rendered included. ADom's one attr write left is the
prewrite (step 1).
### Operational rules
- `partProps(part)` emits static identity only. Mutables are written by ADom.
- What `dom.apply` writes, Svelte does not render from `partProps`.
- Event handlers are synchronous. Async goes before the trigger.
- Guards (`if (disabled) return`) live at the call-site, not inside the
handler.
- `events`/`VisualChannel` may use `ActiveDom` through the injected
projector; `ActiveDom` does not know `events`.
- `morfo.events.commits` is descriptive, not executable. Smoke validates it.
### Piloting (the incremental order)
1. **Toggle** — the first case, `partProps` only.
2. **Collapsible** — adds `keydown`.
3. **Toast** — the first real test of `trigger()` with `intent`.
4. **Dialog** — last, once layers + portal are validated.
See also:
- [`architecture/morfo.md`](./morfo.md) — declaration, archetypes, the 2-of-3 rule
- [`architecture/sema.md`](./sema.md) — the `emit` contract, verb vocabulary
- [`src/arts/adom/README.md`](../../src/arts/adom/README.md) — `dom.apply`
- [`architecture/eidos.md`](./eidos.md) — what eidos consumes from the DOM
- [`architecture/overview.md`](./overview.md) §2.bis — the cross-layer view
### Cross-layer hooks soma emits per the 2-of-3 rule
Soma writes to the DOM not only what it needs; it also writes what sema and
eidos will consume. The "2-of-3" rule decides what enters the morfo and
therefore what the runtime emits:
- **`data-archetype`** — emitted by `partProps` when the part declares an
archetype. Eidos uses it for transversal selectors
(`[data-archetype=trigger] { ... }`); sema can associate verbs by
archetype.
- **`data-event*`** — emitted by `events.emit` (through the VisualChannel)
during a configurable hold. Eidos uses it to tint event transitions
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
(`[data-event^=emerge-dismiss]`). Per-family hold values live in
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`SEMA_MAP.families[*].hold`.
- **`data-{component}` / `data-{component}-{part}`** — the classic structural
markers. Eidos uses them for per-component selectors.
## 4. Component model
Soma's base shape is `Component.Part`:
```ts
import { Dialog } from '$soma/components';
Dialog.Provider; // root — creates context
Dialog.Trigger; // action — opens/closes
Dialog.Content; // content — integrated layers
Dialog.Overlay; // backdrop — presence
Dialog.Title; // ARIA metadata
Dialog.Description; // ARIA metadata
Dialog.Close; // action — closes
```
### Provider (root)
Creates the central state, registers it in context, manages presence for
content and overlay. It may or may not render DOM:
- **With DOM** (Collapsible, Accordion): uses `WithRefOpts`, renders a `<div>`
- **Without DOM** (Dialog, Popover): uses `ProviderOpts`, renders only
children
### Subcomponents
They read the root's state via `.require()`. They don't reimplement logic —
they derive props, ARIA, data-\*, events from the parent's state.
### Portal
An internal component (`components/internal/portal.svelte`). Renders children
into another DOM node. Svelte context is preserved.
### Picker composition (shared state across providers)
Pickers (`DatePicker`, `DateRangePicker`, `TimePicker`, `TimeRangePicker`) do
not reimplement Popover / Field / Calendar — they **compose** them with
shared state. The root wrapper creates three (or more) Providers pointing at
the same `writableActive` refs:
```ts
// Root wrapper
const sharedValue = writableActive(() => value, (v) => (value = v));
const sharedPlaceholder = writableActive(() => placeholder, (v) => (placeholder = v));
const sharedOpen = writableActive(() => open, (v) => (open = v));
{Name}PickerProvider.create({ value: sharedValue, placeholder: sharedPlaceholder, open: sharedOpen, ...config });
PopoverProvider.create({ open: sharedOpen, ... });
{Base}FieldProvider.create({ value: sharedValue, placeholder: sharedPlaceholder, ...config });
// Calendar/RangeCalendar/slider providers are created in their own wrapper (DatePicker.Calendar, TimePicker.HourSlider, …)
```
The picker exposes **only** unique wrappers for `Provider`, `Trigger` and the
calendar/slider bridge. The remaining exports re-export from the composed
components — their native `data-*` (`data-popover-*`, `data-date-field-*`,
`data-calendar-*`, `data-slider-*`) stay the authoritative styling API. The
picker only adds identity attributes (`data-{picker}-trigger`,
`data-{picker}-calendar`) on its own wrappers.
**Auto-close / auto-anchor**: the PickerProvider exposes `handleSelect()`,
which the calendar wrapper calls when a selection completes. Range pickers
re-anchor `placeholder` so the final month lands in the rightmost visible
column, never showing the user a month that doesn't contain their selection.
See A27 in
[`component-guide.md`](../guides/component-guide.md) for the full
checklist.
## 5. Runtime parts
```ts
readonly soma = Soma.require();
readonly runtime = this.soma.runtime(accordionMorfo, {});
const runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
context: AccordionProvider.ctx
});
readonly props = $derived.by(() =>
runtimePart.assert({
...runtimePart.props,
'data-state': this.state
})
);
```
`SomaRuntime.part()` centralizes each part's mechanics:
```ts
interface SomaRuntimePart {
readonly attachment: RefAttachment | undefined;
readonly props: Record<string, unknown>;
resolveProps(bindings?): Record<string, unknown>;
assert<P extends Record<string, unknown>>(props: P): P;
}
```
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
There is no imperative attr path per part: the bag (`props`) resolves the
whole contract and Svelte renders it. The one sanctioned imperative writer
left is `trigger`'s prewrite (a commit the morfo declares), which goes
through `dom.apply` on its own step.
Two opts interfaces:
- `ProviderOpts` — `{ id: Active<string>; ref?: State<HTMLElement | null> }` —
for DOM-less roots
- `WithRefOpts` — `{ id: Active<string>; ref: State<HTMLElement | null> }` —
for parts with DOM
## 6. Layers
`layers/` contains behavior classes only (`.svelte.ts`). They are
infrastructure consumed by Providers, never directly by the consumer.
### Inventory
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| Layer | API | Responsibility |
| ----------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `Presence` | `new Presence(opts)` | Animation-aware mount/unmount. `isPresent`, `transitionAttrs`, `onComplete`. |
| `FocusScope` | `FocusScope.use(opts)` | Focus trap, loop, auto-focus, restore. Singleton manager with a stack. |
| `Dismissal` | `Dismissal.use(opts)` | Escape + click-outside. Global registry. Behaviors: close, ignore, defer. |
| `TextSelection` | `TextSelection.use(opts)` | Prevents selection overflow during drag. |
| `ScrollLock` | `new ScrollLock(initial?, delay?)` | Body scroll lock with refcount. Supports a delay for animations. |
| `ResizeObserver$` | `new ResizeObserver$(getter, cb)` | ResizeObserver with Svelte lifecycle. |
| `Floating*` | `FloatingProvider.create()`, `FloatingContent.create(opts)`, etc. | Anchor-relative positioning — the in-house engine (`layers/floating` + `$ethereal`; `@floating-ui` is a parity-tests devDep). |
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| `Gesture.base` | `Gesture.base(opts)` | Pointer tracking + axis lock + velocity. |
| `Gesture.drag` | `Gesture.drag(opts)` | Base + progress + snap points + dismiss. |
| `Gesture.resize` | `Gesture.resize(opts)` | Base + delta + min/max constraints. |
| `SafePolygon` | `new SafePolygon(opts)` (`floating/safe-polygon.ts`) | Hover-gap corridor between trigger↔content. |
| `Stacking` | module-level registry (`stacking.svelte.ts`) | Shared z-order of movable surfaces (FloatPanel): `bringToFront`, `data-topmost`/`data-behind`. |
| `AxialDrag` | `new AxialDrag(opts)` (`manipulation/`) | Single-axis drag with snap points + release state. |
| `ZoomPan` | `new ZoomPan(config)` | Scale + pan of content inside a fixed viewport (Cropper); pure state + math. |
| `ImageProvider` | `new ImageProvider(opts)` | Image load state (`idle/loading/loaded/error`) with delay. |
| `ListSelection` | pure functions (`list-selection.ts`) | The single/multi selection machine + `allowDeselect`, shared by Select/Combobox. |
`layers/floating/placement.ts` is the single source for `Side`, `Align`,
`Boundary`, `SIDE_OPTIONS` and `ALIGN_OPTIONS`. `floating/types.ts` consumes
that source and does not import from the `floating.svelte.ts` runtime,
avoiding cycles between types and classes.
### Provider test coverage
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Every active Soma provider has a direct `*-provider.svelte.test.ts` — the
guard returns `NO_MISSING_PROVIDER_TESTS` when a provider ships without one,
so the live inventory is the test tree itself (one test file next to each
provider). The reusable engines live outside Soma and carry their own tests:
`$libs/datagrid` (table core), `$libs/forms` (form core + Standard Schema)
and `$libs/strings` (Command's scorer).
### Convention
- `.use(opts)` → self-managed lifecycle (internal watch/$effect). Private
constructor.
- `new X(opts)` → manual lifecycle. The consumer controls it.
- `.props` → an object to spread into the Provider.
### Animations (Presence)
Lifecycle:
```
OPENING:
open=true → shouldRender=true + data-starting-style
→ next rAF: data-starting-style removed (triggers CSS transition)
→ getAnimations().finished → onComplete(true)
CLOSING:
open=false → data-ending-style (element stays in DOM!)
→ getAnimations().finished
→ shouldRender=false + data-ending-style removed → onComplete(false)
```
- `forceMount` keeps the element in the DOM always (for CSS transitions)
- `onComplete` uses the `getAnimations()` API, not
`transitionend`/`animationend` events
- Run-ID cancellation prevents stale callbacks on fast toggles
- **JS-driver gating** (the `motion` option): a `spring` (pure rAF) does not
appear in `getAnimations()`. `Presence` receives `motion: EngineMotion`
(= `soma.motion`, relocated to `arts/motion`) and calls
`motion.run(node, phase)`; it awaits its `finished` ALONGSIDE
`getAnimations()` before unmounting. For CSS presets (or nodes without
`data-animation-style`), `run` returns an already-settled handle → the
declarative path is unchanged. (Replaces the old
`runMotion`/`eidos.motionRunner` hook.)
## 7. The Soma class (the component runtime scope)
Soma reads `ActiveUix` from context and exposes services to components.
Components import Soma internals through relative paths, never from
`$active-app` and never through their own `$soma/*` public alias. Nestable: a
child `<Soma portalTo="#modals">` overrides the parent.
```ts
class Soma {
static create(opts?: SomaOptions): Soma; // factory + context set
static get(): Soma | undefined; // safe read
static require(): Soma; // throws if not found
readonly uix: ActiveUix;
readonly portalTo: string | HTMLElement | undefined;
// Service accessors (delegate to ActiveUix)
get langs(): ActiveLangs;
get nums(): ActiveNumbers | undefined;
get money(): ActiveCurrency | undefined;
get dates(): ActiveDates | undefined;
get units(): ActiveUnits | undefined;
get prefs(): ActiveUixPrefsView;
get logger(): EngineLogger;
get motion(): EngineMotion; // arts/motion — Presence's JS-driver gating
}
```
### Service access from components
Components access services through Soma, never through App directly:
```ts
const soma = Soma.get();
soma?.langs.ts('#?common.buttons.close|Close'); // translation via idlangref
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
soma?.prefs.getDir(); // the app's direction — one link of the chain, see below
soma?.money?.format(1099); // currency formatting
soma?.dates?.getDateOrder(); // DMY / MDY / YMD
soma?.dates?.getHourCycle(); // 12 | 24 (numeric — not '12h' / '24h')
soma?.portalTo; // portal target
```
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en `CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`. Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon; por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que `:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las trampas que el guard no ve; que espeja y que no; y la mitad global de prefs. EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal: - `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones, mandando al wrapper a leer prefs directamente. Eso excluye la prop. - `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()` dentro del provider) como LA forma de obtener la direccion — justo lo que el eje retiro del catalogo. - `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una clave HTML) es correcto y sobrevive; solo cambia el ejemplo. - `active-architecture.md:416` no listaba `lang` en la proyeccion, contra `contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`. Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`, `data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`, `building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura, que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`. El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de estampar `data-dir`, que `:dir()` no puede ver. DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse. Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos: RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca lee al padre» era absoluto y borraba la composicion sancionada en el punto de llamada · el estampado se afirmaba incondicional en un sitio y condicional en otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni `component-audit.ts` conocen. `docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
A component never takes its own direction from that accessor: the wrapper runs
`activeDir(() => dir, soma)` and the provider defaults once in `resolvedDir`
(§3.4) — [`canon/direction-contract.md`](../canon/direction-contract.md).
### Date / time types and formatting
Soma imports date-related symbols from `$libs/days`, the canonical date
library. Components **never** import from `$lib/util/dates` (legacy) or
`@internationalized/date` directly. There is no Soma re-export façade for the
date domain.
- Value types: `CalendarDate`, `CalendarDateTime`, `Time`, `ZonedDateTime`
- Types: `DateValue`, `TimeValue`, `DateRange`, `DateMatcher`, `Month`,
`WeekStartsOn`, `HourCycle`, `TimeGranularity`, `DateOrder`, `Granularity`,
`SegmentPart`, `EditableTimeSegmentPart`, `TimeSegmentObj`,
`SegmentValueObj`, `DayPeriod`, …
- Queries: `isSameDay`, `hasTime`, `isZonedDateTime`, `isTimeBefore`,
`isTimeAfter`, `today`, `now`, `startOfMonth`, `endOfMonth`,
`getLastFirstDayOfWeek`, `getNextLastDayOfWeek`, …
- Operations: `dateValueToDate`, `convertTimeValueToDateValue`,
`convertTimeValueToTime`, `toCalendarDate`, `toZoned`, …
- Parsing: `parseDate`, `parseDateTime`, `parseTime`
- Formatting: `DateFormatter`, `getCachedDateFormat`, `getPlaceholder`,
`getDefaultDate`, `getDefaultTime`, `inferGranularity`,
`inferTimeGranularity`, `getDefaultHourCycle`, `resolveDateOrder(locale)`,
`resolveHourCycle(locale)`
- **Segments (dias/segments.ts)**: constants (`DATE_SEGMENT_PARTS`,
`EDITABLE_TIME_SEGMENT_PARTS`, …), type guards (`isDateSegmentPart`,
`isEditableTimeSegmentPart`, `isDateAndTimeSegmentObj`, …), pure helpers
(`initializeSegmentValues`, `initializeTimeSegmentValues`,
`getValueFromSegments`, `getTimeValueFromSegments`,
`areAllSegmentsFilled`, `createSegmentContent`, `createTimeSegmentContent`,
`getOptsByGranularity`, `getOptsByTimeGranularity`).
`HourCycle` is canonically the numeric form `12 | 24` across the whole
framework, matching `Intl.DateTimeFormat`'s `hour12` resolved option. String
forms like `'12h'`/`'24h'` are legacy and must not appear in new code.
**`soma/datetime/` holds only UI-level helpers** (the screen-reader
announcer, DOM segment navigation, the `SegmentState` shape with
`lastKeyZero`/`hasLeftFocus`/`updating`, `isAcceptableSegmentKey` using KEYS,
description-element DOM writers). It must not re-export `$libs/days`
symbols — consumers import from `$libs/days` directly. Extending `$libs/days`
is the default for new date/time helpers; adding to `soma/datetime/` is only
correct when the helper is genuinely UI-specific.
### The static-method convention (project-wide)
All classes using Svelte context follow the same pattern:
| Method | Returns | Use when |
| ---------------- | ----------------------- | --------------------------------- |
| `X.create(opts)` | instance | Creating + registering in context |
| `X.get()` | instance or `undefined` | Parent/context is optional |
| `X.require()` | instance (throws) | Parent/context is required |
This applies to `App`, `Soma`, and every state class using context. No
standalone functions. No `from()`. No exposed `ctx`.
### Functional text
The component's own text slots are declared in the morfo as idlangrefs (the
multilingual catalog lives in `src/uix/langs/components/{kebab}.ts`):
```ts
export const drawerMorfo = {
name: 'Drawer',
kebab: 'drawer',
texts: {
trigger: '#?components.drawer.trigger|Open drawer'
},
parts: [
{
name: 'Trigger',
kebab: 'trigger',
aria: [{ attr: 'aria-label', value: v.translationRef('trigger', 'Open drawer') }]
}
]
} as const satisfies Morfo;
```
Shared translations are not duplicated per component:
```ts
value: v.commonRef('buttons.close', 'Close'); // #?common.buttons.close|Close
```
`ActiveUix` registers the `src/uix/langs/components/*` catalogs into
`ActiveLangs` under `components.{kebab}.*`. When the provider creates
`createSomaRuntime(morfo, sources)` or `soma.runtime(morfo, sources)`,
`registerMorfo(morfo)` compiles and registers the `data-*` contract; the
morfo only declares its slots (`morfo.texts`), not the catalog.
`commonLangs` in `src/uix/langs.ts` supplies the `common.*` defaults.
`ActiveUix` registers them without overwriting existing leaves, so the
integrator can pass their own translations and UIX only fills what is
missing.
There is no global per-component catalog. `ActiveUix` wires the morfo
registry; each component publishes its texts when its morfo registers.
## 8. The reactive system
A thin layer over Svelte 5 runes that lets reactive state be passed by
reference between classes.
- `state<T>(initial)` → `State<T>` (mutable, `.current`)
- `readableActive(() => value)` → `Active<T>` (readonly derived)
- `writableActive(getter, setter)` → `State<T>` (two-way binding)
Types:
```ts
type Active<T> = { readonly current: T }; // readonly container
type State<T> = { current: T }; // mutable container
type ActiveProps<T> = { [K in keyof T]: Active<T[K]> };
type StateProps<T> = { [K in keyof T]: State<T[K]> };
```
The `.svelte` wrappers convert plain props into `Active`/`State` with these
functions. That conversion is the boundary between Svelte's prop world and
soma's reactive-class world. Providers receive their options typed as
`StateProps<…>` / `ActiveProps<…>`.
## 8.bis Internal helpers
Infrastructure modules providers consume. Not consumer API; imported by
relative path inside soma.
### props — `mergeProps`
```ts
const merged = mergeProps(restProps, state.props);
```
- handlers (`onclick`, `onfocus`, …) → composed with `composeHandlers`
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `class` → merged with clsx
- `style` → merged (object + string)
feat(morfo,soma): el attr de nombrado es un DEFAULT del contrato, no una imposicion A-85 estaba confirmado desde el ledger de blocks: la demo de site-header escribia `aria-label` en el trigger del cajon y el canon lo pisaba. El censo dio la magnitud real — 132 declaraciones en 59 morfos — y la disposicion escrita (replicar el `prop-truthy` de button en las 132) resulto ser la direccion inversa a la buena: obligaba a 264 entradas de boilerplate y hacia que el texto del consumidor diese una vuelta entera por el grafo reactivo para aterrizar donde ya estaba. Las 132 declaraciones eran CORRECTAS. El defecto era la precedencia. Asi que el contrato gana dos clases, por VOCABULARIO y no por declaracion: contract (role, data-*, aria-expanded, aria-controls, roledescription...) el runtime SIEMPRE gana; un consumidor que los pisara haria mentir al componente, y esa garantia es algo que las librerias user-props-last no tienen. naming (`ARIA_NAMING_ATTRS`, hoy `aria-label`) el morfo da un DEFAULT y el attr explicito del consumidor gana — espejo de la propia cadena de nombre accesible de la plataforma. Tres puntos, uno por capa, y ninguno de los 132 morfos tocado: `compileMorfo` clasifica el plan (`consumerWins`) y deja de hoistear esos literales a staticAttrs; el runtime los saca del efecto `syncAttrs` —el `dom.apply` post-render ERA el pisado— y los embarca en el bag de la parte, el mismo vehiculo que `dir`; y `mergeProps` los resuelve consumer-first (`a ?? b`, para que un `aria-label=""` explicito sobreviva). Efecto medido que no se buscaba: los labels salen ahora en el HTML de SSR. Antes los escribia un efecto, que en servidor no corre — el propio ledger lo tenia medido y nadie habia atado los dos cabos. `aria-labelledby` se queda en contract A PROPOSITO: es cableado `partRef`, y la plataforma ya hace que un labelledby del consumidor gane a cualquier label sin que tengamos que arbitrarlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- ARIA naming attrs (`ARIA_NAMING_ATTRS` — `aria-label`) → FIRST wins. The
framework-wide call shape puts the consumer's `restProps` first, so the
consumer's explicit label beats the morfo's default (two-class precedence,
A-85 — see `architecture/morfo.md` Step 4). Contract attrs keep last-wins:
the runtime must win on state/wiring or the component lies.
- `hidden: false` / `disabled: false` → removed (a Svelte fix)
- the rest → last wins
### provider — `context()`
`context<T>(name)` wraps `$libs/reactive`'s `Context` with descriptive errors. Providers
don't touch it directly: they expose the static `create()` / `get()` /
`require()` methods (see §7).
```ts
const ctx = context<AccordionProvider>('Accordion');
ctx.set(instance); // registers in Svelte context
ctx.get(); // reads — throws if missing
ctx.getOr(fallback); // reads with a fallback
```
### keyboard — `KEYS`, `getDirectionalKeys`
```ts
KEYS.ENTER; // 'Enter'
KEYS.ESCAPE; // 'Escape'
KEYS.ARROW_DOWN; // 'ArrowDown'
KEYS.SPACE; // ' '
const { nextKey, prevKey } = getDirectionalKeys('ltr', 'horizontal');
// nextKey: 'ArrowRight', prevKey: 'ArrowLeft'
IsUsingKeyboard.current; // boolean — keyboard vs pointer
```
### dom — focus, roving, scroll lock
```ts
focusWithoutScroll(element);
focusFirst(candidates);
getTabbableCandidates(container);
getTabbableEdges(container);
const roving = new RovingFocusGroup({ candidateAttr, rootNode, loop, orientation });
roving.handleKeydown(currentElement, event);
const lock = new ScrollLock();
lock.locked.current = true; // locks body scroll
```
### attrs — boolean helpers
```ts
boolToStr(true); // 'true'
boolToEmptyStrOrUndef(true); // ''
boolToEmptyStrOrUndef(false); // undefined
boolToTrueOrUndef(true); // true
boolToTrueOrUndef(false); // undefined
```
## 9. data-\* contracts
The `data-*` attrs are formal public API, validated with `assertContract()`.
Convention:
```
data-dialog → provider (no -provider, no -root)
data-dialog-trigger → part
data-dialog-content → part
data-state="open|closed" → state
data-disabled → flag
data-side="top|right|bottom|left" → floating position
data-align="start|center|end" → alignment
data-starting-style → enter animation (1 frame)
data-ending-style → exit animation (persists)
data-nested → is a child of another of the same type
data-nested-open → has an open child
data-dragging → gesture drag active
data-highlighted → item with virtual focus (aria-activedescendant)
data-resizing → splitter resize active
```
feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar Firma SS16, quinta aplicacion de la doctrina "la capa sostiene la pluma, no es la dueña". El censo, el ledger y la ley del espacio cerrado auditaban SOLO quien LEE (recetas de eidos); quien ESCRIBE la custom property por instancia vive en soma/arts y ningun instrumento del eje lo habia enumerado jamas. EL BUG VIVO QUE ESTO DESTAPO — y no se parcheo, se diagnostico --drawer-overlay-opacity era DOS especies bajo un nombre: el knob de tema (el contrato dice 62%) y el canal del arrastre (0..1). soma escribia el canal SIN UNIDAD sobre el nombre del knob; color-mix() exige porcentaje, la funcion caia invalida y el velo computaba rgba(0,0,0,0): NO PINTABA. Y el inline dejaba el knob inalcanzable para cualquier tema. Se separan: el publico se queda como knob (nadie lo escribe en runtime), el arrastre pasa a --_drawer-overlay-progress (sin unidad, que para un multiplicador es lo correcto) y la receta los COMPONE - calc(knob * progress) -, asi que el arrastre ATENUA el tema en vez de destruirlo. Medido en Chrome: reposo 0.408471 (antes rgba 0,0,0,0) - el tema LLEGA (20% -> 0.1318, 62% -> 0.4085, 100% -> 0.6588; antes ninguno movia nada) - el arrastre sigue (Escape real -> alpha 0; 0/0.25/0.5/1 lineal) - control negativo: reinyectando la escritura vieja el velo vuelve a caer, o sea que la unidad era el SINTOMA y las dos especies la enfermedad. ⚠ Estaba adjudicado EN PROSA en theming-sentinel-exceptions.ts:815 desde hacia dias: la excepcion se trago el bug. LOS 18 NOMBRES A SU SITIO Clase B (11 en command/dialog/scroll-area/toast + 4 del drawer): forma publica sin contrato y sin UN SOLO lector en el repo - una API publicada que el framework no consume. A --_{c}-*, y los README de soma dejan de enseñarlos como API del consumidor: ahora enseñan LA PARTE, con la formulacion verbatim que SS15 ya habia verificado. Clase D (tree-view, gradient-picker): tenian lector, verificados en navegador. --scrollbar-width NO se renombro por inercia: es una escritura sobre el unico <body>, la doctrina no le llega, y queda REGISTRADA con su razon en vez de inventarle un dueño. LA AGUJA - src/uix/value-channels.test.ts (fichero propio, 6 tests) Vitest y no script, porque el gate termina en la suite y eso es lo que convierte la doctrina en ley. Deriva el vocabulario de sistema RESTANDO el contrato a lo que emite el generador (lista derivada, nunca a mano). Barre NUEVE raices - las siete nuevas verificadas a cero ANTES de asertarlas - y el quinto test asserta que cada raiz declarada se anduvo de verdad: una raiz que resuelve a cero ficheros es la puerta que nadie habria visto. Dos correcciones al dimensionado, por medida: el arbol tiene SEIS formas de escribir, no cuatro, y una de las que faltaban era LA CANONICA (la --_${component}-... que SS14 y SS15 firmaron) - un guard ciego a ella habria dado verde sobre su propio destino. Y un barrido mas ancho marcaba en rojo un anchor-name, que en gramatica es identico a un nombre de propiedad: probado y REVERTIDO. El instrumento miente primero. Mutaciones: cinco, con el arbol byte a byte identico. Incluyen las dos que prueban lo que las correcciones añaden (una clave _ DEL contrato pasa de verde a rojo; un --_ legitimo en blocks pasa de rojo a verde). LO QUE EL ADVERSARIAL CORRIGIO DE MI PROPIA LEY Dictamen: "es LEY sobre la mitad que barre, y sigue siendo PROSA sobre el absoluto que enuncia". Cierto: decia "toda escritura por instancia" y la aguja no mira eidos, donde viven ONCE escrituras de nombres contratados. Corregido - el enunciado nombra sus nueve raices y DECLARA sus dos fronteras (eidos, con su expediente abierto; web/routes, congelado); las salidas son CUATRO y no tres (la cuarta, sistema, es verde); y el registro se queda con cuatro campos por entrada (since, reason, destination, heldBecause) con un test que exige los cuatro - la diferencia entre un registro y un cajon. Y la relectura con ojo de abogado encontro DOS absolutos mas, en direccion contraria, escritos bajo "WHAT THE GUARD DOES NOT CHECK": "por instancia" no es decidible estaticamente (el antecedente del guard es MAS ANCHO que el de la doctrina), y el {c} de --_{c}-* no lo comprueba nadie, solo el guion bajo. SS18 ABIERTO, y es la respuesta medida a "¿puede volver a nacer un nombre sin dueño sin que nadie se entere?": SI, desde eidos. 11 contratadas + 18 sin dueño, partidas en dos especies (la fundacion generando su vocabulario, que es legitima, y las props ergonomicas del consumidor, que no es lo mismo). dialog-overlay-opacity es el GEMELO EXACTO del velo del drawer y sigue vivo. No es "añadir el root": distinguir las dos especies EXIGE FIRMA. Guards: value-channels 6/6 - recipe-css-contract + reach-floor + generated-css 55/55 - docs:check 0/0 sobre 819 - --debt 1148/0/0 - tsc 0 propios - prettier limpio. BREAKING: los 18 nombres publicos ya no existen; el canal se lee --_{c}-* y sigue sin ser de nadie para fijarlo. Y consumir un --{c}-* del contrato desde un provider es ROJO desde hoy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
Value channels soma writes per instance. **There is only ONE form**: `--_{c}-*`.
This list carried both spellings until 2026-08-27 — it was the acta of a
migration half done — and the law that ended it is R-5.5 in
[`canon/recipe-contract.md`](../canon/recipe-contract.md) §4: a per-instance
write outside `--_{c}-*` is a public form nobody owns, or, if the contract
declares the name, a token that LIES (a theme can never win an inline write).
The guard is `src/uix/value-channels.test.ts`.
```
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
--_{host}-floating-transform-origin → §15: the positioner writes its
--_{host}-floating-available-width per-instance geometry as a value
--_{host}-floating-available-height channel in the HOST's namespace
--_{host}-floating-anchor-width (host = the component that owns the
--_{host}-floating-anchor-height floating composition)
feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar Firma SS16, quinta aplicacion de la doctrina "la capa sostiene la pluma, no es la dueña". El censo, el ledger y la ley del espacio cerrado auditaban SOLO quien LEE (recetas de eidos); quien ESCRIBE la custom property por instancia vive en soma/arts y ningun instrumento del eje lo habia enumerado jamas. EL BUG VIVO QUE ESTO DESTAPO — y no se parcheo, se diagnostico --drawer-overlay-opacity era DOS especies bajo un nombre: el knob de tema (el contrato dice 62%) y el canal del arrastre (0..1). soma escribia el canal SIN UNIDAD sobre el nombre del knob; color-mix() exige porcentaje, la funcion caia invalida y el velo computaba rgba(0,0,0,0): NO PINTABA. Y el inline dejaba el knob inalcanzable para cualquier tema. Se separan: el publico se queda como knob (nadie lo escribe en runtime), el arrastre pasa a --_drawer-overlay-progress (sin unidad, que para un multiplicador es lo correcto) y la receta los COMPONE - calc(knob * progress) -, asi que el arrastre ATENUA el tema en vez de destruirlo. Medido en Chrome: reposo 0.408471 (antes rgba 0,0,0,0) - el tema LLEGA (20% -> 0.1318, 62% -> 0.4085, 100% -> 0.6588; antes ninguno movia nada) - el arrastre sigue (Escape real -> alpha 0; 0/0.25/0.5/1 lineal) - control negativo: reinyectando la escritura vieja el velo vuelve a caer, o sea que la unidad era el SINTOMA y las dos especies la enfermedad. ⚠ Estaba adjudicado EN PROSA en theming-sentinel-exceptions.ts:815 desde hacia dias: la excepcion se trago el bug. LOS 18 NOMBRES A SU SITIO Clase B (11 en command/dialog/scroll-area/toast + 4 del drawer): forma publica sin contrato y sin UN SOLO lector en el repo - una API publicada que el framework no consume. A --_{c}-*, y los README de soma dejan de enseñarlos como API del consumidor: ahora enseñan LA PARTE, con la formulacion verbatim que SS15 ya habia verificado. Clase D (tree-view, gradient-picker): tenian lector, verificados en navegador. --scrollbar-width NO se renombro por inercia: es una escritura sobre el unico <body>, la doctrina no le llega, y queda REGISTRADA con su razon en vez de inventarle un dueño. LA AGUJA - src/uix/value-channels.test.ts (fichero propio, 6 tests) Vitest y no script, porque el gate termina en la suite y eso es lo que convierte la doctrina en ley. Deriva el vocabulario de sistema RESTANDO el contrato a lo que emite el generador (lista derivada, nunca a mano). Barre NUEVE raices - las siete nuevas verificadas a cero ANTES de asertarlas - y el quinto test asserta que cada raiz declarada se anduvo de verdad: una raiz que resuelve a cero ficheros es la puerta que nadie habria visto. Dos correcciones al dimensionado, por medida: el arbol tiene SEIS formas de escribir, no cuatro, y una de las que faltaban era LA CANONICA (la --_${component}-... que SS14 y SS15 firmaron) - un guard ciego a ella habria dado verde sobre su propio destino. Y un barrido mas ancho marcaba en rojo un anchor-name, que en gramatica es identico a un nombre de propiedad: probado y REVERTIDO. El instrumento miente primero. Mutaciones: cinco, con el arbol byte a byte identico. Incluyen las dos que prueban lo que las correcciones añaden (una clave _ DEL contrato pasa de verde a rojo; un --_ legitimo en blocks pasa de rojo a verde). LO QUE EL ADVERSARIAL CORRIGIO DE MI PROPIA LEY Dictamen: "es LEY sobre la mitad que barre, y sigue siendo PROSA sobre el absoluto que enuncia". Cierto: decia "toda escritura por instancia" y la aguja no mira eidos, donde viven ONCE escrituras de nombres contratados. Corregido - el enunciado nombra sus nueve raices y DECLARA sus dos fronteras (eidos, con su expediente abierto; web/routes, congelado); las salidas son CUATRO y no tres (la cuarta, sistema, es verde); y el registro se queda con cuatro campos por entrada (since, reason, destination, heldBecause) con un test que exige los cuatro - la diferencia entre un registro y un cajon. Y la relectura con ojo de abogado encontro DOS absolutos mas, en direccion contraria, escritos bajo "WHAT THE GUARD DOES NOT CHECK": "por instancia" no es decidible estaticamente (el antecedente del guard es MAS ANCHO que el de la doctrina), y el {c} de --_{c}-* no lo comprueba nadie, solo el guion bajo. SS18 ABIERTO, y es la respuesta medida a "¿puede volver a nacer un nombre sin dueño sin que nadie se entere?": SI, desde eidos. 11 contratadas + 18 sin dueño, partidas en dos especies (la fundacion generando su vocabulario, que es legitima, y las props ergonomicas del consumidor, que no es lo mismo). dialog-overlay-opacity es el GEMELO EXACTO del velo del drawer y sigue vivo. No es "añadir el root": distinguir las dos especies EXIGE FIRMA. Guards: value-channels 6/6 - recipe-css-contract + reach-floor + generated-css 55/55 - docs:check 0/0 sobre 819 - --debt 1148/0/0 - tsc 0 propios - prettier limpio. BREAKING: los 18 nombres publicos ya no existen; el canal se lee --_{c}-* y sigue sin ser de nadie para fijarlo. Y consumir un --{c}-* del contrato desde un provider es ROJO desde hoy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
--_dialog-depth → §16: nesting depth of this dialog
--_dialog-nested-count → §16: how many dialogs it has above it
--_drawer-progress → §16: 0-1 drag progress
--_drawer-offset-x / y → §16: drag offset in px
--_drawer-overlay-progress → §16: 0-1 snap progress, a MULTIPLIER over
the public --drawer-overlay-opacity knob
--_toast-swipe-move-x / y → §16: toast swipe offset
--_toast-swipe-end-x / y → §16: toast swipe release offset
```
The list is a sample, not the census: soma and arts write **61** names over 72
sites (measured 2026-08-27), and the guard — not this page — is what keeps
them in the namespace.
## 10. IDs
IDs are generated with component context:
```
soma-dialog-c12
soma-dialog-trigger-c13
soma-dialog-content-c14
```
Pattern: `soma-{component}-{part}-{uid}`. Descriptive and inspectable.
## 11. Barrel exports
### Components (hierarchical)
```ts
// $soma/components/index.ts
export * as Collapsible from './collapsible';
export * as Dialog from './dialog';
export * as Popover from './popover';
```
Consumption:
```ts
import { Dialog, Popover } from '$soma/components';
Dialog.Provider; // not SomaDialogProvider, not TerraDialogProvider
Dialog.Trigger;
```
### Internal
```ts
import { Portal, Arrow, VisuallyHidden, Soma } from '$soma/components/internal';
```
## 12. External boundaries
`soma` distinguishes between:
- internal: `layers/`, `reactive/`, `dom/`, `provider/` — its own helpers
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- external: `svelte` only (`clsx` is imported in `props/props.ts` without being
declared — it resolves as a transitive of svelte; a debt pending decision)
Floating positioning stopped being an external dependency: it is the in-house
engine (`layers/floating` + `$ethereal`); `@floating-ui` survives only as a
devDependency for the parity tests. **`runed` and `tabbable` followed the same
path (2026-07)**: they are no longer npm dependencies. `runed`'s reactive runes
were ported into `$libs/reactive` (`Context`, `watch`, `Previous`, `Debounced`,
`FiniteStateMachine`, `resource`, …) and `$adom` (`ElementSize`, rebuilt on the
ActiveDom runtime); `tabbable`'s focus-order engine was ported into
`$libs/dom` (`tabbable-core`, consumed by the existing `tabbable.ts` wrapper and
surfaced through `$adom`). soma now depends on nobody but svelte. If a dependency
has an unstable API or could change, it is accessed through a formal boundary (as
`layers/floating/` does with the positioning engine).
## 13. Directory structure
```
src/uix/soma/
├── SOMA_ARCHITECTURE.md ← stub (this chapter lives in docs/architecture/)
├── COMPONENT_GUIDE.md ← stub (the guide lives in docs/guides/component-guide.md)
├── README.md ← stub (the soma chapter lives in docs/architecture/)
├── runtime.svelte.ts ← SomaRuntime (morfo interpreter)
├── errors.ts ← typed runtime/context errors
├── core/
│ └── soma.svelte.ts ← the Soma class (root instance)
├── reactive/ ← the reactive system
├── provider/ ← context + opts bridge
├── props/ ← mergeProps, composeHandlers
├── keyboard/ ← KEYS, directional
├── dom/ ← DOM utilities, focus
├── css/ ← styleToString, cssToStyleObj
├── id/ ← createId (useId → $active-uix/id)
├── types/ ← shared types + service interfaces
├── layers/ ← behavior layers (classes only)
│ ├── presence.svelte.ts
│ ├── focus-scope.svelte.ts
│ ├── dismissal.svelte.ts
│ ├── text-selection.svelte.ts
│ ├── scroll-lock.svelte.ts
│ ├── resize-observer.svelte.ts
│ └── floating/
├── datetime/ ← UI-only helpers (announcer, segment DOM nav,
│ segment UI-state shapes, segment-key predicates,
│ description-element writers). NO date math,
│ NO re-exports of days — import `$libs/days`
│ directly.
├── components/
│ ├── internal/ ← Portal, Arrow, VisuallyHidden, <Soma>
│ ├── {name}/ ← each headless component
│ │ ├── {name}-provider.svelte.ts ← state classes (NOT {name}.svelte.ts)
│ │ ├── types.ts ← public props + canonical field shapes
│ │ ├── langs.ts ← optional idlangref constants for imperative strings
│ │ ├── exports.ts
│ │ ├── index.ts
│ │ └── components/
│ │ ├── {name}.svelte ← root wrapper
│ │ ├── {name}-trigger.svelte
│ │ └── ...
│ └── index.ts ← hierarchical barrel
└── index.ts ← root scope only (`Soma`)
```
### File naming convention
- State class: `{name}-provider.svelte.ts` — NOT `{name}.svelte.ts`
- Avoids Vite module-resolution ambiguity with the `{name}.svelte` wrapper
- Reflects what's inside: provider/state classes
- Root wrapper: `{name}.svelte` in the `components/` subdirectory
- Export name: always `Provider`, never `Root`
## 14. Anti-patterns
Avoid in soma:
- Complex logic inside the wrapper `.svelte` — it belongs in the Provider
- Props drilling when context is the correct pattern
- `data-*` attrs outside the contract
- Inventing part names without checking the reference-library anatomies
(ark-ui, bits-ui, radix-ui)
- Nesting layers as component wrappers in templates
- Inline `z-index: auto` overriding CSS
- Coupling primitives to app libraries
- Product copy inside the primitive
- Speculative abstractions ("just in case")
- One-line files that only re-export (merge into the parent)
- Redundant naming prefixes (SomaDialog, DialogLayerState)
- Dummy refs to satisfy a type — use `ProviderOpts` for no-DOM roots
- **State class file named like the wrapper** — `select.svelte.ts` +
`components/select.svelte` causes Vite module duplication. Always
`{name}-provider.svelte.ts`
- **Event handlers not in props** — defining onclick as a class method but
not including it in the derived props object
- **getContext in event handlers** — getContext only works during
initialization. Capture references in the constructor
- **Exporting as Root** — always `Provider`, never `Root`
- **Skipping the reference-library comparison** — a mandatory step, no
exceptions
- **Comments in Spanish** — all code comments in English
- **Standalone context functions** — no `createX()`, `getX()`, `useX()` as
loose functions. Use the `X.create()`, `X.get()`, `X.require()` statics
- **Importing from `$lib/ext/app`** in components — components access
services through `Soma`, never App directly
- **`from()` as a factory name** — use `create()` consistently
- **Re-implementing date/time helpers inside soma** — extend `$libs/days`
(A23). Importing from `$lib/util/dates` (legacy vendored) or
`@internationalized/date` directly is forbidden; use `$libs/days`.
- **Re-export façades over days** — a soma module whose only job is to
forward `$libs/days` symbols is dead weight. Consumers import from
`$libs/days` directly.
- **UI-level helpers in dias, or date math in `soma/datetime/`** — dias is
pure (no DOM, no Svelte, no KEYS); `soma/datetime/` is UI-only (the
screen-reader announcer, DOM segment navigation, `SegmentState` shapes,
KEYS-based predicates). No crossover
- **`readonlySegments` without a concrete value anchor** — A24: warn via
`soma?.logger.warn` when `value` is undefined. Range components split into
`startReadonlySegments` / `endReadonlySegments` (A25)
- **`keydown.preventDefault()` as the only guard on contenteditable
segments** — IME/paste/drop bypass keydown. Always add
`onbeforeinput: e => e.preventDefault()` (A26)
- **Time placeholders as `'––'`** — use `createSegmentContent` /
`createTimeSegmentContent` from `dias/segments.ts`; time parts render as
`hh`/`mm`/`ss` (A28)
- **Pickers that reimplement field/calendar/popover** — compose via shared
`writableActive` refs (A27). Only `Provider`, `Trigger` and the
calendar/slider bridge are unique parts
- **Demo pages as galleries of canned snippets** — every soma demo must be an
interactive testbed wiring every public prop to a live control, including a
Field-integration section (A29)
- **`HourCycle` as `'12h' \| '24h'`** — the canonical form is numeric
`12 \| 24` (matches `Intl.DateTimeFormat.hour12`). String forms are legacy
## 15. Current shape (standing decisions)
Soma has gone through several phases. Its current shape (post-2026-05-08):
- **Provider inheritance dropped** — providers no longer inherit from an
abstract base; they are concrete classes. The shared DOM mechanics live in
`SomaRuntime.part(...)`, and cases needing semantic events use the same
`SomaRuntime` for `trigger`/`keydown`.
- **SomaRuntime caches** the morfo compilation (`compileMorfo` by WeakMap)
and registers the `effects` that sync `state → attrs` via `dom.apply`.
- **Naming**: `Provider` (never `Root`); child providers reference the parent
as `provider`, never `root`. A multi-part component's export keeps the
compound shape `Toggle.Provider + Toggle.Trigger + ...`.
- **Data-attr naming**: `data-{component}` (provider) and
`data-{component}-{kebab}` (sub-parts). No `data-soma-*` prefix. The
compiler emits these via `compiled.parts.attrs`.
- **State files**: `{name}-provider.svelte.ts` (explicit, no ambiguity).
- **IDs**: descriptive (`soma-dialog-trigger-c13`).
## 16. The stability rule
A soma component is considered stable when:
- its public API is clear and JSDoc-documented
- its `data-*` are registered and validated with `assertContract`
- the wrapper and the Provider follow the general pattern
- its base accessibility is solved (ARIA, roles, keyboard)
- its props have been compared against ark-ui, bits-ui and radix-ui
- it has a working demo page at `web/routes/uix/components/{component}/`
- it doesn't depend on local hacks, hardcoded z-indexes or demo CSS to stand
- it compiles with 0 errors (`svelte-check`)
## 17. New component checklist
See [`component-guide.md`](../guides/component-guide.md) for the
full step-by-step process (27 general steps + 4 date/time specific, with
rules A1–A29). Summary:
```
[ ] 1. Compare with ark-ui, bits-ui, radix-ui — feature table
[ ] 2. Verify membership criteria
[ ] 3. Define parts + attrs + morfo.texts when the component owns text
[ ] 4. Create types.ts (props + canonical field shapes)
[ ] 5. Create langs.ts only for imperative idlangref constants, not as the catalog
[ ] 6. Create {name}-provider.svelte.ts (concrete state classes, no Provider inheritance)
[ ] 7. Create wrapper .svelte files (thin)
[ ] 8. Create exports.ts + index.ts
[ ] 9. Create interactive demo page + link in index (A29)
[ ] 10. README.md with anatomy, ARIA, data-attrs, comparison table
[ ] 11. svelte-check + test in browser
[ ] 12. Date/time components: only consume date/time domain via `$libs/days`,
`onbeforeinput` on contenteditable, readonly-without-value warning,
picker composition pattern (A23–A28)
```

Powered by TurnKey Linux.