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/active-uix.md

112 lines
5.8 KiB

---
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:
- creates `logger`, `timers`, `bus` and `prefs`;
- 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;
- **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>
4 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>
4 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.

Powered by TurnKey Linux.