---
title: ActiveUix — the composition root
type: reference
audience: human + agent
authority: E1 architecture — how UIX boots, which services it owns, and how it degrades
status: current
source: migrated from src/uix/active-uix/README.md (2026-07-02, docs-book F7.1)
---
# ActiveUix
`active-uix` is UIX's composition root. Its responsibility is not to be another
behavior layer, but to hand `morfo` , `soma` , `sema` and `eidos` the minimal
services they need — without components ever knowing `ActiveApp` directly.
> **Whole-system architecture**: [`architecture/active-architecture.md`](./active-architecture.md).
> **Executable contract**: [`src/uix/contracts.ts`](../../src/uix/contracts.ts), validated by
> [`contracts.test.ts`](../../src/uix/contracts.test.ts).
## Two boot modes
`createActiveUix(options)` — **standalone** . Composes its own runtime:
feat(motion)!: una fuente de motion, y los cuatro ejes visuales SIEMPRE en prefs — eidos no lee medios, nadie en el árbol uix pregunta al SO, ningún esquema del app deja a eidos sin motor (P2 #5)
La preferencia efectiva de motion la decide prefs UNA vez (`resolveMotion`: la
intención `allow|reduce` gana al hint del SO, `system` deriva) y la proyección la
estampa en `<html data-motion>` — desde e3c0899dd antes del primer pintado. Aun
así el árbol la re-derivaba por su cuenta leyendo el SO en crudo en dos capas:
A) CSS de eidos (§61 del changelog): 65 at-rules `@media (prefers-reduced-motion:
reduce)` en 63 ficheros + 29 emitidas por el generador, conviviendo con 14 + 52
selectores `[data-motion='reduce']` («with or without the JS projection»). Dos
fuentes ⇒ un `allow` explícito no llegaba al CSS. Cada bloque pasa a
`[data-motion='reduce'] SEL` con la MISMA declaración y orden (paridad a
máquina: 63 ficheros, 65 bloques, 89 reglas, 178 pares selector-declaraciones,
0 discrepancias); los 3 ficheros con gemelo idéntico se unifican; ningún bloque
usaba `:root`. Los 4 `!important` dentro de bloques migrados QUEDAN con su rival
nombrado (navigation-menu ×2 contra el swap direccional (0,3,0); text-focus
contra un `style:transition` inline; events.css empate (0,3,0) con las firmas
generadas en otra hoja). Generador: los 4 emisores dejan de emitir el media, ink
marks gana su gemelo; `generated/base.css` 29 → 0 media, `--nombres` 8034 /
5751 únicos INTACTOS. GUARD nuevo `reduced-motion-media.test.ts` (at-rule, no
prosa; anti-vacío > 60 ficheros; mordido por mutación). Cambio de veredicto de
cascada MEDIDO en Chrome real por el adversarial: el prefijo suma (0,1,0), una
veintena de variantes más específicas que ganaban al media hoy pierden — y bajo
la media del SO la forma de HEAD NO paraba el anillo del spinner. `data-motion`
tiene TRES dueños (reduce · navigation-menu from/to · tabs fade|slide), valores
disjuntos, forma ancestro obligatoria. Prosa que afirmaba «both» barrida por la
AFIRMACIÓN (tooltip.css, card.css, recipes/base.ts, skin-media-player, events,
float-panel).
B) JS (§62): el motor de motion (`engine-motion.ts:131`), el motor de escena
(`engine-scene.ts:99`), la háptica de sema (`chans/haptic.ts:129`) y OCHO
lecturas en componentes de eidos preguntaban a `ActiveDom.prefersReducedMotion`
(el media en crudo). El puerto correcto YA EXISTÍA sin consumidores:
`MotionSource = Source<MotionEffective>` (`$libs/motion`). La fuente nace en
prefs (`createMotionSourceFromPrefs`, `src/arts/prefs/motion-source.ts`,
precedente `createLocaleSourceFromPrefs`) y la construyen UNA vez las tres
raíces: `createActiveUix`, `attachActiveUix` (fallbacks desde `app.prefs`) y las
fábricas `defineEngineMotion` / `defineEngineScene` / `defineEngineSemantic`
(`coreDependencies: ['prefs']`, forma de format y langs). MUEREN de los puertos
`MotionDom.prefersReducedMotion`, `SceneDom.prefersReducedMotion` y el interface
`HapticChannelDom` entero (un puerto que contesta política es la puerta por
donde vuelve el defecto); `MotionRunOptions.reduced` se queda como PIN por
ejecución. `ActiveEidos.reducedMotion` con dos puertas: con prefs el efectivo;
standalone sigue al SO con acta (sin primer motor no hay segundo, §60). Los
ocho sitios de componentes leen `eidos.reducedMotion`. Hallazgo del lote: DOS
motores de escena por superficie DOM (aura-indicator, pack Ambient) que se
habrían quedado ciegos a la policy `reduce` obligatoria (P-1) EN SILENCIO —
reciben la fuente vía `eidos.reducedMotion`. Deuda nombrada: escena lee la
fuente en el montaje (paridad con la lectura del media que sustituye).
`ActiveDom.prefersReducedMotion` no se toca: es un hecho del SO que alimenta
el ENTORNO de prefs y nada más.
Soma (adenda firmada «a en B»): el último lector de política que preguntaba al
SO, `soma/runtime.svelte.ts:1280` (migración a11y del morfo: `'state'` ⇒
`channels: []` por S5, `'text'` ⇒ región viva, `'focus'` ⇒ foco), lee la
MISMA fuente: `SomaRuntimeBaseSources.motion: MotionSource` OBLIGATORIO como
`dom` (un runtime que no puede responder «¿reducido?» no puede honrar S5 — lo
garantiza el tipo), llenado por `Soma.runtime()` desde `uix.prefs` (esquema sin
`motion` ⇒ permitir, como los motores). La decisión mayor (b) — que el MOTOR
aplique `'state'` en su pasada de reducción y que un `emit()` directo reciba
tratamiento a11y — queda ABIERTA como fila §3.9 de CONTINUE-sema-audit.md, a
ejecutar junto a D-full. `ActiveEidos.reducedMotion` distingue «sin prefs»
(standalone ⇒ SO, §60) de «prefs sin ranura motion» (⇒ permitir, como los
motores): las dos mitades de una UI ya no discrepan bajo un esquema sin la
dimensión. Hallazgo de la adenda: la bolsa `sources` se construye en 96
sitios (3 de producción — menu-dial, metrics, onion-menu — que reciben la
fuente vía `eidos.reducedMotion`, y 93 harnesses cuyo `as unknown as Soma`
CEGABA la comprobación de miembros: 60 tests rojos hasta declarar el `Omit`
real; el tipo hizo su trabajo en el código de producción y era ciego justo
donde se suponía que bastaba). Test real nuevo
`soma/test/reduced-motion-source.svelte.test.ts` (chromium: `Soma.create()`
lee contexto Svelte) con `createActiveUix` + `EngineSemantic` reales;
mutación (el trigger vuelve al dom) ROJA 3/3. Adversarial dirigido de la
adenda: siete defectos cerrados — `src/uix/contracts.ts` declaraba
`requires: ['dom']` para la bolsa de soma y el test no lo asertaba (un guard
que no inspecciona nada pasa) → `['dom', 'motion']` + aserto; la bolsa #97
(`bag-census.test.ts`) sin `motion` bajo un casteo; el camino `'text'` —el
ÚNICO que declaran 9 morfos de producción— sin test (añadido); dos snippets
de docs que ya no compilaban (component-guide A1, morfo.md); cifras del §62.
C) La raíz GARANTIZA los cuatro ejes visuales (cazado por el autor en el docs
site: texto invisible en oscuro). Cuatro layouts congelados componen su propio
esquema de prefs SIN `mode/theme/density/scaling`; un esquema del app
SUSTITUÍA al de la raíz entero y eidos «degradaba defensivamente» a
`mode = 'light'` CONSTANTE ignorando el SO, mientras el shell estampaba su
wrapper en oscuro: tinta de tema claro sobre superficies oscuras. Forma:
`uixVisualPrefsDimensions()` (pura, en `prefs-schema.ts`; el boot compila la
misma) y `createActiveUix` fusiona SIEMPRE `{ ...ejesVisuales, ...esquemaDelApp }`
— el app puede REDEFINIR un eje, nunca omitirlo; sin un solo cast. Eidos deja
de degradar en silencio: `createPrefsPreferenceSource(prefs, fallbacks,
onMissing)` avisa por `uix.logger.warn` por cada ranura ausente (queda solo
para attach con prefs ajenas; docs de attach: el app compone
`uixVisualPrefsDimensions()`). Tests reales (esquema sin ejes + SO oscuro ⇒
`data-mode="dark"`; app que redefine `theme` gana; prefs ajenas ⇒ 4 avisos por
el logger REAL); el viejo test «degrades to the fallbacks» estaba verde POR
COINCIDENCIA (defaults = fallbacks) y se sustituye; mutación (sin fusión) 2
rojos y restaurada. Boot regenerado (15134 B), delta cero ×5 intacto.
`web/routes/uix/+layout@.svelte` (descongelado por orden del autor, +14/−29):
muere el `modeSource` muerto y el `$state` local; el shell LEE la ranura
`mode` y el toggle escribe `uix.prefs.setIntent('mode', …)`; migración única
de `uix-docs-theme` al sobre de prefs (clave borrada). Ledger 95 → 93 (la
entrada de ese fichero desaparece). VERIFICADO EN CHROME por el coordinador:
SO oscuro sin clave ⇒ `<html data-mode="dark">`, wrapper dark, tinta
`oklch(0.95)`; clave vieja `dark` ⇒ intent `dark`; toggle mueve html, wrapper,
tinta e intent en los dos sentidos. Nombrado, no arreglado: el boot compila
siempre el esquema por defecto (un app que REDEFINE un eje resuelve distinto
que el boot; hoy nadie en web/ usa el boot) · los otros tres layouts congelados
recuperan los ejes pero su wrapper sigue en `$state` local · SSR del docs sirve
`light` y la hidratación corrige (previo).
Verificación: A — vitest eidos+value-channels 43/483, eidos:lint 0, paridad 0
discrepancias, mutación del guard 3/3 y 4/4 (adversarial), prettier solo avisos
preexistentes; B — vitest del scope 90/990 y, con la adenda, 242/2395; +26 tests
(motor 5 · escena 2 · háptica 2 · raíz real jsdom 7 · ActiveEidos 7 · fábrica 1 ·
soma 3 — cifras verificadas una a una por el adversarial), 11 dobles re-firmados,
93 harnesses de soma tipados con el `Omit` real, cuatro mutaciones ROJAS (3/2/3 +
la de soma 3/3) y restauradas por sha256, más la mutación de TIPO del adversarial
(la fuente devuelve la intención ⇒ 2 errores nuevos en src/: el puerto es gate de
tipo, no prosa); C — vitest active-uix+eidos+prefs 57/591, boot 8/8 con delta
cero, mutación 2 rojos, verificación en Chrome por el coordinador; todos —
check src/ 0 (ledger 95 → 93: MENGUA), check:gate OK, docs:check OK,
arts:check OK, packs:check OK. Suite completa 457/5336 verde (adversarial A). Adversariales Opus
independientes por lote (informes en el handoff). Constructores + adversariales
Opus 5; la sesión coordina. ⚠ Lección: la cuenta del brief de A («94 en fuentes»)
era un fallo de medida del coordinador (`grep -rh | grep -v generated` filtra
LÍNEAS con esa palabra, no el directorio); el constructor la re-midió porque el
brief lo exigía. ⚠ Dos constructores cortados por límite de sesión y reanudados
tras releer su diff entero (ley de la casa).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
- creates `logger` , `timers` , `bus` and `prefs` — and the prefs schema ALWAYS
carries the four visual axes: `uixVisualPrefsDimensions()` is merged under
whatever `prefs.schema` the app passes, so the app may redefine `mode` /
`theme` / `density` / `scaling` but never omit them;
- the `bus` uses `createSvelteEngineBus({ logger, clock: timers.clock })` , same
as `ActiveApp` , so listeners run under `untrack` and never create accidental
reactive dependencies;
- creates `langs` , `dom` (or `disabledDom` when `dom:false` ), `clipboard` ,
`format` and `events` according to the options;
- keeps `portal` as a generic portal target, so each layer adapts it to its own
API (`portalTo`) without coupling `active-uix` to that layer;
- registers the common translations and the per-component catalogs from
`src/uix/langs/components/*` .
`attachActiveUix(app, options)` — **attach** to an external `ActiveApp` :
- reuses `app.prefs` , `app.langs` , `app.dom` , `app.clipboard` and `app.format`
when they exist;
feat(motion)!: una fuente de motion, y los cuatro ejes visuales SIEMPRE en prefs — eidos no lee medios, nadie en el árbol uix pregunta al SO, ningún esquema del app deja a eidos sin motor (P2 #5)
La preferencia efectiva de motion la decide prefs UNA vez (`resolveMotion`: la
intención `allow|reduce` gana al hint del SO, `system` deriva) y la proyección la
estampa en `<html data-motion>` — desde e3c0899dd antes del primer pintado. Aun
así el árbol la re-derivaba por su cuenta leyendo el SO en crudo en dos capas:
A) CSS de eidos (§61 del changelog): 65 at-rules `@media (prefers-reduced-motion:
reduce)` en 63 ficheros + 29 emitidas por el generador, conviviendo con 14 + 52
selectores `[data-motion='reduce']` («with or without the JS projection»). Dos
fuentes ⇒ un `allow` explícito no llegaba al CSS. Cada bloque pasa a
`[data-motion='reduce'] SEL` con la MISMA declaración y orden (paridad a
máquina: 63 ficheros, 65 bloques, 89 reglas, 178 pares selector-declaraciones,
0 discrepancias); los 3 ficheros con gemelo idéntico se unifican; ningún bloque
usaba `:root`. Los 4 `!important` dentro de bloques migrados QUEDAN con su rival
nombrado (navigation-menu ×2 contra el swap direccional (0,3,0); text-focus
contra un `style:transition` inline; events.css empate (0,3,0) con las firmas
generadas en otra hoja). Generador: los 4 emisores dejan de emitir el media, ink
marks gana su gemelo; `generated/base.css` 29 → 0 media, `--nombres` 8034 /
5751 únicos INTACTOS. GUARD nuevo `reduced-motion-media.test.ts` (at-rule, no
prosa; anti-vacío > 60 ficheros; mordido por mutación). Cambio de veredicto de
cascada MEDIDO en Chrome real por el adversarial: el prefijo suma (0,1,0), una
veintena de variantes más específicas que ganaban al media hoy pierden — y bajo
la media del SO la forma de HEAD NO paraba el anillo del spinner. `data-motion`
tiene TRES dueños (reduce · navigation-menu from/to · tabs fade|slide), valores
disjuntos, forma ancestro obligatoria. Prosa que afirmaba «both» barrida por la
AFIRMACIÓN (tooltip.css, card.css, recipes/base.ts, skin-media-player, events,
float-panel).
B) JS (§62): el motor de motion (`engine-motion.ts:131`), el motor de escena
(`engine-scene.ts:99`), la háptica de sema (`chans/haptic.ts:129`) y OCHO
lecturas en componentes de eidos preguntaban a `ActiveDom.prefersReducedMotion`
(el media en crudo). El puerto correcto YA EXISTÍA sin consumidores:
`MotionSource = Source<MotionEffective>` (`$libs/motion`). La fuente nace en
prefs (`createMotionSourceFromPrefs`, `src/arts/prefs/motion-source.ts`,
precedente `createLocaleSourceFromPrefs`) y la construyen UNA vez las tres
raíces: `createActiveUix`, `attachActiveUix` (fallbacks desde `app.prefs`) y las
fábricas `defineEngineMotion` / `defineEngineScene` / `defineEngineSemantic`
(`coreDependencies: ['prefs']`, forma de format y langs). MUEREN de los puertos
`MotionDom.prefersReducedMotion`, `SceneDom.prefersReducedMotion` y el interface
`HapticChannelDom` entero (un puerto que contesta política es la puerta por
donde vuelve el defecto); `MotionRunOptions.reduced` se queda como PIN por
ejecución. `ActiveEidos.reducedMotion` con dos puertas: con prefs el efectivo;
standalone sigue al SO con acta (sin primer motor no hay segundo, §60). Los
ocho sitios de componentes leen `eidos.reducedMotion`. Hallazgo del lote: DOS
motores de escena por superficie DOM (aura-indicator, pack Ambient) que se
habrían quedado ciegos a la policy `reduce` obligatoria (P-1) EN SILENCIO —
reciben la fuente vía `eidos.reducedMotion`. Deuda nombrada: escena lee la
fuente en el montaje (paridad con la lectura del media que sustituye).
`ActiveDom.prefersReducedMotion` no se toca: es un hecho del SO que alimenta
el ENTORNO de prefs y nada más.
Soma (adenda firmada «a en B»): el último lector de política que preguntaba al
SO, `soma/runtime.svelte.ts:1280` (migración a11y del morfo: `'state'` ⇒
`channels: []` por S5, `'text'` ⇒ región viva, `'focus'` ⇒ foco), lee la
MISMA fuente: `SomaRuntimeBaseSources.motion: MotionSource` OBLIGATORIO como
`dom` (un runtime que no puede responder «¿reducido?» no puede honrar S5 — lo
garantiza el tipo), llenado por `Soma.runtime()` desde `uix.prefs` (esquema sin
`motion` ⇒ permitir, como los motores). La decisión mayor (b) — que el MOTOR
aplique `'state'` en su pasada de reducción y que un `emit()` directo reciba
tratamiento a11y — queda ABIERTA como fila §3.9 de CONTINUE-sema-audit.md, a
ejecutar junto a D-full. `ActiveEidos.reducedMotion` distingue «sin prefs»
(standalone ⇒ SO, §60) de «prefs sin ranura motion» (⇒ permitir, como los
motores): las dos mitades de una UI ya no discrepan bajo un esquema sin la
dimensión. Hallazgo de la adenda: la bolsa `sources` se construye en 96
sitios (3 de producción — menu-dial, metrics, onion-menu — que reciben la
fuente vía `eidos.reducedMotion`, y 93 harnesses cuyo `as unknown as Soma`
CEGABA la comprobación de miembros: 60 tests rojos hasta declarar el `Omit`
real; el tipo hizo su trabajo en el código de producción y era ciego justo
donde se suponía que bastaba). Test real nuevo
`soma/test/reduced-motion-source.svelte.test.ts` (chromium: `Soma.create()`
lee contexto Svelte) con `createActiveUix` + `EngineSemantic` reales;
mutación (el trigger vuelve al dom) ROJA 3/3. Adversarial dirigido de la
adenda: siete defectos cerrados — `src/uix/contracts.ts` declaraba
`requires: ['dom']` para la bolsa de soma y el test no lo asertaba (un guard
que no inspecciona nada pasa) → `['dom', 'motion']` + aserto; la bolsa #97
(`bag-census.test.ts`) sin `motion` bajo un casteo; el camino `'text'` —el
ÚNICO que declaran 9 morfos de producción— sin test (añadido); dos snippets
de docs que ya no compilaban (component-guide A1, morfo.md); cifras del §62.
C) La raíz GARANTIZA los cuatro ejes visuales (cazado por el autor en el docs
site: texto invisible en oscuro). Cuatro layouts congelados componen su propio
esquema de prefs SIN `mode/theme/density/scaling`; un esquema del app
SUSTITUÍA al de la raíz entero y eidos «degradaba defensivamente» a
`mode = 'light'` CONSTANTE ignorando el SO, mientras el shell estampaba su
wrapper en oscuro: tinta de tema claro sobre superficies oscuras. Forma:
`uixVisualPrefsDimensions()` (pura, en `prefs-schema.ts`; el boot compila la
misma) y `createActiveUix` fusiona SIEMPRE `{ ...ejesVisuales, ...esquemaDelApp }`
— el app puede REDEFINIR un eje, nunca omitirlo; sin un solo cast. Eidos deja
de degradar en silencio: `createPrefsPreferenceSource(prefs, fallbacks,
onMissing)` avisa por `uix.logger.warn` por cada ranura ausente (queda solo
para attach con prefs ajenas; docs de attach: el app compone
`uixVisualPrefsDimensions()`). Tests reales (esquema sin ejes + SO oscuro ⇒
`data-mode="dark"`; app que redefine `theme` gana; prefs ajenas ⇒ 4 avisos por
el logger REAL); el viejo test «degrades to the fallbacks» estaba verde POR
COINCIDENCIA (defaults = fallbacks) y se sustituye; mutación (sin fusión) 2
rojos y restaurada. Boot regenerado (15134 B), delta cero ×5 intacto.
`web/routes/uix/+layout@.svelte` (descongelado por orden del autor, +14/−29):
muere el `modeSource` muerto y el `$state` local; el shell LEE la ranura
`mode` y el toggle escribe `uix.prefs.setIntent('mode', …)`; migración única
de `uix-docs-theme` al sobre de prefs (clave borrada). Ledger 95 → 93 (la
entrada de ese fichero desaparece). VERIFICADO EN CHROME por el coordinador:
SO oscuro sin clave ⇒ `<html data-mode="dark">`, wrapper dark, tinta
`oklch(0.95)`; clave vieja `dark` ⇒ intent `dark`; toggle mueve html, wrapper,
tinta e intent en los dos sentidos. Nombrado, no arreglado: el boot compila
siempre el esquema por defecto (un app que REDEFINE un eje resuelve distinto
que el boot; hoy nadie en web/ usa el boot) · los otros tres layouts congelados
recuperan los ejes pero su wrapper sigue en `$state` local · SSR del docs sirve
`light` y la hidratación corrige (previo).
Verificación: A — vitest eidos+value-channels 43/483, eidos:lint 0, paridad 0
discrepancias, mutación del guard 3/3 y 4/4 (adversarial), prettier solo avisos
preexistentes; B — vitest del scope 90/990 y, con la adenda, 242/2395; +26 tests
(motor 5 · escena 2 · háptica 2 · raíz real jsdom 7 · ActiveEidos 7 · fábrica 1 ·
soma 3 — cifras verificadas una a una por el adversarial), 11 dobles re-firmados,
93 harnesses de soma tipados con el `Omit` real, cuatro mutaciones ROJAS (3/2/3 +
la de soma 3/3) y restauradas por sha256, más la mutación de TIPO del adversarial
(la fuente devuelve la intención ⇒ 2 errores nuevos en src/: el puerto es gate de
tipo, no prosa); C — vitest active-uix+eidos+prefs 57/591, boot 8/8 con delta
cero, mutación 2 rojos, verificación en Chrome por el coordinador; todos —
check src/ 0 (ledger 95 → 93: MENGUA), check:gate OK, docs:check OK,
arts:check OK, packs:check OK. Suite completa 457/5336 verde (adversarial A). Adversariales Opus
independientes por lote (informes en el handoff). Constructores + adversariales
Opus 5; la sesión coordina. ⚠ Lección: la cuenta del brief de A («94 en fuentes»)
era un fallo de medida del coordinador (`grep -rh | grep -v generated` filtra
LÍNEAS con esa palabra, no el directorio); el constructor la re-midió porque el
brief lo exigía. ⚠ Dos constructores cortados por límite de sesión y reanudados
tras releer su diff entero (ley de la casa).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
- **does not merge the visual axes.** The prefs engine already exists and belongs
to the app, so nothing can be composed into it after the fact: an attaching app
spreads `uixVisualPrefsDimensions()` (from `$active-uix` ) into its own prefs
schema. Otherwise eidos warns once per absent slot (`eidos.prefs`) and falls
back to its own default — `mode: 'light'` on a dark-mode machine included;
- **requires** `app.langs` and `app.dom` ; if either is missing it throws a
configuration error;
- does not re-subscribe `langs` to `prefs.language` (that connection belongs to
`defineActiveLangs` inside `ActiveApp` );
- registers the common translations and the per-component catalogs;
- does not own the `app` 's lifecycle.
## Minimum contracts per module
> Executable source: [`src/uix/contracts.ts`](../../src/uix/contracts.ts).
| Module | Minimum required | Optional | Fallback when missing | Error when missing |
| --- | --- | --- | --- | --- |
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
| `ActiveUix` standalone | `langs` config | `clipboard:false` , `format` , `events` , `portal` , `dom:false` | creates `prefs` , core services and, with `dom:false` , a local `disabledDom` | missing `langs` config |
| `ActiveUix` attach | `ActiveApp` core + `langs` , `dom` services | `app.clipboard` , `app.format` , event engine, `portal` | none for required services | missing `langs` or `dom` on the app; the getter of an absent optional service fails explicitly |
| `SomaRuntime` | `dom` from `ActiveUix` | event engine, `langs` , `format` | none of its own | nonexistent morfo/event/part |
| `Sema` direct | `dom` or `projector` when `visual` is active | `sound` , `haptic` , `visual:false` | none of its own for UIX services | `SemaConfigError` without `dom/projector` while visual is active |
| `Eidos` | `dom` when `applyDom` | `langs` , `format` , `prefs` , mode/density sources | `applyDom:false` allows render/serialize without DOM | missing `dom` with `applyDom` active |
| `ADom` direct | caller's target/window/document | breakpoints/window | `disabledDom` only when the caller asks for it | ADom's own errors without a real DOM |
## Ownership and degradation rules
1. **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` .
2. ** `dom:false` only degrades in standalone.** `ActiveUix` exposes a local
`disabledDom` and passes it to `EngineSemantic` too; Sema/events never fall
back to direct DOM writes. In attach there is no compensating creation: if
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
the app has no `dom` , `attachActiveUix(app)` fails. ** `disabledDom` is
deliberately MIXED** (AUX-2, 2026-07-11): write-shaped calls (`apply`,
`listen` , …) no-op silently, but reads that MUST return a value —
`measure` , `raf` , `getDocument` / `getWindow` — **throw
`ActiveUixDomDisabledError` **; a component that measures under
`dom:false` dies loudly instead of computing from a phantom layout.
3. ** `langs` is not `locale` .** `prefs.language` feeds translations;
`prefs.locale` feeds formats. They never mix.
4. ** `clipboard` is a capability service, not visual DOM.** Standalone creates
it unless `clipboard:false` ; attach consumes it from `app.clipboard` when a
layer asks for it, and fails with an explicit error if it was not declared.
5. ** `prefs.direction` is the effective direction preference**; `html[dir]` is
only its DOM projection.
6. ** `ActiveUix` does not auto-project preferences onto the DOM.** `arts/prefs`
projects the cross-modal attrs (`dir`, `data-motion` , `data-sound` ,
`data-haptic` ) via `createActivePrefsDomProjection` ; `ActiveEidos` projects
the visual ones (`data-theme`, `data-mode` , `data-density` ). The `frontend`
artifact was retired and must not reappear as a `locale` source.
7. ** `ActiveUix` neither creates nor knows `Soma` or `Eidos` .** `Soma.create(...)`
creates its own scope and `Soma.runtime(...)` ; `ActiveEidos.create(...)`
creates the visual scope when the app needs runtime CSS.
## Booting a UIX shell
A shell that uses visual components wires three pieces explicitly:
```ts
const uix = createActiveUix({ langs, prefs: { schema } });
setActiveUix(uix);
Soma.create();
const prefsProjection = createActivePrefsDomProjection({
prefs: uix.prefs,
dom: uix.dom
});
feat(eidos)!: un escalar es una instancia CLAVADA — mueren el throw de la puerta 2 y las fuentes por eje (c')
Cierra el estado INTERINO de e3c0899dd. `resolvePreferences` lanzaba con `uix` +
escalar y `ActiveEidos.create()` inyecta `uix` SIEMPRE, así que la regla no decía
«no mezcles dos motores»: decía «ningún escalar, nunca», y toda demo que clava un
panel en oscuro arrancaba con excepción. Forma firmada por el autor (c'):
- Pines SÍ: `theme` / `mode` / `density` / `scaling` en `ActiveEidosOptions` son
PINES — el eje queda clavado en la instancia y gana sobre la fuente, prefs
incluido (precedente en el mismo constructor: `options.dom ?? options.uix?.dom`).
Un eje clavado sigue suscrito: `apply()` corre y lo encuentra quieto.
- Sources por eje FUERA sin shim: `modeSource` / `densitySource` / `scalingSource`
eran la API del segundo motor. La puerta de sustitución ENTERA sigue siendo
`preferences`. Mueren `createComposedPreferenceSource`, `createStaticValueSource`
y `PREFERENCE_OPTION_KEYS`; nace `createStandalonePreferenceSource(dom)`:
sin primer motor no hay segundo — standalone sigue al SO EN VIVO para `mode`.
- Sin throw. UNA precedencia por UN envoltorio (`createPinnedPreferenceSource`)
sobre la puerta que responda (`preferences` · `uix.prefs` · standalone):
`pin ?? fuente ?? fallback` en las tres.
- Una función pura, dos lectores: `src/uix/eidos/lib/visual-preference.ts`
(`VisualPreferencePins` + `resolveVisualPreference(pin, value)`), importable por
el boot compilado como `lib/theme-id.ts`. `ActiveEidosOptions extends
VisualPreferencePins`; `UixBootParams.pins` lo toma entero;
`renderUixBootScript({ pins })` los embarca. DOS parámetros y no tres: el
fallback no es compartido (el boot resuelve siempre los cuatro ejes; en runtime
lo aporta cada fuente) — acta en changelog §60.
- Delta cero a CINCO casos: instancia clavada a dark con sobre en light (ni un
attr se mueve al hidratar) + familia clavada `acme` con modo de prefs
(`acme-dark` a los dos lados: el pin atraviesa `resolveThemeId`). Mutaciones
probadas: sin pin en el boot → 2 rojos; sin pin en el envoltorio → 2 rojos;
restauración por sha256.
- `contracts.test.ts`: retirado el guard «guards UIX docs shell from writing
visual prefs through ActivePrefs» — codificaba la doctrina REVOCADA el
2026-09-14 (escribir `theme` en prefs es el camino canónico); no se invierte:
un guard sobre un árbol congelado que se reconstruye no mide nada. Acta en §60.
- Coste en el árbol congelado: diez demos pasan `*Source` y pierden el tipo →
ledger `check-debt.ts` +22 (8 entradas nuevas, 2 subidas), cada una con causa
fechada (excepción firmada 2026-09-13). `src/` a CERO.
- Docs: eidos.md §preferencias · prefs README §Eidos Boundary · guide.md
(`pins`, tercer parámetro del boot) · changelog §60 · active-uix / overview /
active-architecture / blocks (snippets con `modeSource` al flujo canónico).
Verificación: vitest eidos+active-uix+prefs+contracts+value-channels 59/640 ·
check 95 = 73 + 22 exacto, src/ 0 · check:gate OK · docs:check 819 OK ·
generate:boot 15104 bytes con sync verde · adversarial Opus independiente
(informe en el handoff). Constructor + adversarial Opus 5; la sesión coordina.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
const eidos = ActiveEidos.create({ applyDom: true });
```
`prefsProjection` owns `dir` , `data-motion` , `data-sound` and `data-haptic` .
`ActiveEidos` owns `data-theme` , `data-mode` and `data-density` . Light/dark
feat(eidos)!: un escalar es una instancia CLAVADA — mueren el throw de la puerta 2 y las fuentes por eje (c')
Cierra el estado INTERINO de e3c0899dd. `resolvePreferences` lanzaba con `uix` +
escalar y `ActiveEidos.create()` inyecta `uix` SIEMPRE, así que la regla no decía
«no mezcles dos motores»: decía «ningún escalar, nunca», y toda demo que clava un
panel en oscuro arrancaba con excepción. Forma firmada por el autor (c'):
- Pines SÍ: `theme` / `mode` / `density` / `scaling` en `ActiveEidosOptions` son
PINES — el eje queda clavado en la instancia y gana sobre la fuente, prefs
incluido (precedente en el mismo constructor: `options.dom ?? options.uix?.dom`).
Un eje clavado sigue suscrito: `apply()` corre y lo encuentra quieto.
- Sources por eje FUERA sin shim: `modeSource` / `densitySource` / `scalingSource`
eran la API del segundo motor. La puerta de sustitución ENTERA sigue siendo
`preferences`. Mueren `createComposedPreferenceSource`, `createStaticValueSource`
y `PREFERENCE_OPTION_KEYS`; nace `createStandalonePreferenceSource(dom)`:
sin primer motor no hay segundo — standalone sigue al SO EN VIVO para `mode`.
- Sin throw. UNA precedencia por UN envoltorio (`createPinnedPreferenceSource`)
sobre la puerta que responda (`preferences` · `uix.prefs` · standalone):
`pin ?? fuente ?? fallback` en las tres.
- Una función pura, dos lectores: `src/uix/eidos/lib/visual-preference.ts`
(`VisualPreferencePins` + `resolveVisualPreference(pin, value)`), importable por
el boot compilado como `lib/theme-id.ts`. `ActiveEidosOptions extends
VisualPreferencePins`; `UixBootParams.pins` lo toma entero;
`renderUixBootScript({ pins })` los embarca. DOS parámetros y no tres: el
fallback no es compartido (el boot resuelve siempre los cuatro ejes; en runtime
lo aporta cada fuente) — acta en changelog §60.
- Delta cero a CINCO casos: instancia clavada a dark con sobre en light (ni un
attr se mueve al hidratar) + familia clavada `acme` con modo de prefs
(`acme-dark` a los dos lados: el pin atraviesa `resolveThemeId`). Mutaciones
probadas: sin pin en el boot → 2 rojos; sin pin en el envoltorio → 2 rojos;
restauración por sha256.
- `contracts.test.ts`: retirado el guard «guards UIX docs shell from writing
visual prefs through ActivePrefs» — codificaba la doctrina REVOCADA el
2026-09-14 (escribir `theme` en prefs es el camino canónico); no se invierte:
un guard sobre un árbol congelado que se reconstruye no mide nada. Acta en §60.
- Coste en el árbol congelado: diez demos pasan `*Source` y pierden el tipo →
ledger `check-debt.ts` +22 (8 entradas nuevas, 2 subidas), cada una con causa
fechada (excepción firmada 2026-09-13). `src/` a CERO.
- Docs: eidos.md §preferencias · prefs README §Eidos Boundary · guide.md
(`pins`, tercer parámetro del boot) · changelog §60 · active-uix / overview /
active-architecture / blocks (snippets con `modeSource` al flujo canónico).
Verificación: vitest eidos+active-uix+prefs+contracts+value-channels 59/640 ·
check 95 = 73 + 22 exacto, src/ 0 · check:gate OK · docs:check 819 OK ·
generate:boot 15104 bytes con sync verde · adversarial Opus independiente
(informe en el handoff). Constructor + adversarial Opus 5; la sesión coordina.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
mode is a prefs dimension like the rest (2026-09-14): the toggle writes
`uix.prefs.setIntent('mode', 'dark')` and eidos, which reads the slot,
re-applies the attributes.