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/theming/guide.md

456 lines
15 KiB

---
title: Eidos Theming — Authoring guides
type: guide
audience: human + agent
status: current
source: migrated from src/uix/eidos/THEMING_GUIDE.md (2026-07-02, docs-book F7.3; originally extracted from THEMING §8 + §9)
---
# Eidos Theming — Authoring guides
> The two theming how-tos: add a new component and define a theme. Extracted
> from [`THEMING.md`](./reference.md) (the E1 reference) to
> live as an E4 guide. The token contract holding all of this together is
> [`canon/tsc.md`](../canon/tsc.md); the transversal systems every recipe
> must consume are [`canon/recipe-contract.md`](../canon/recipe-contract.md).
---
## How to add a new component
Assuming you already have the morfo, the soma and the eidos wrapper
scaffolding (`src/uix/eidos/components/{name}/`):
### Step 1 — Decide which tokens you need
Look at similar components (`button`, `toggle`, `switch`). Identify which
dimensions your component exposes:
- Does it have `data-color`? → palette tokens.
- Does it have `data-variant`? → variant tokens.
- Does it have `data-size`? → size tokens.
- How many parts does it have? → per-part tokens.
### Step 2 — Add the recipe in `lib/recipes/base.ts`
```ts
// Within THEME_BASE_RECIPE_TOKENS:
'my-component': {
// size tokens — scope 'root' (stable)
'height-md': '36px',
'padding-inline-md': 'var(--space-3)',
'gap': 'var(--space-2)',
'radius': 'var(--radius-md)',
// per-color literal definitions — scope 'root'
'primary-solid': 'var(--color-primary-solid)',
'affirm-solid': 'var(--color-affirm-solid)',
'threat-solid': 'var(--color-threat-solid)',
// dynamic palette — 'host' default + per-color overrides
'palette-solid': {
declarations: [
{ value: 'var(--my-component-primary-solid)', scope: 'host' },
{ value: 'var(--my-component-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--my-component-threat-solid)', scope: 'color:threat' }
]
},
// derived tokens — scope 'host' (deps inferred)
'solid-bg': {
value: 'var(--my-component-palette-solid)',
scope: 'host'
}
}
```
### Step 3 — Regenerate
```bash
npm run generate:eidos-css
```
If your config violates the TSC, the regeneration tells you:
```
Eidos recipe scope contract violations:
- my-component.solid-bg (scope root): dependency 'palette-solid' is only
declared at scopes [host], none of which is reachable from the
consumer's scope.
```
### Step 4 — Write the recipe CSS
`src/uix/eidos/components/my-component/my-component.css`:
```css
[data-my-component] {
/* Private tokens — only this recipe reads them */
--_my-component-bg: var(--my-component-solid-bg);
--_my-component-radius: var(--my-component-radius);
display: inline-flex;
align-items: center;
padding-inline: var(--my-component-padding-inline-md);
height: var(--my-component-height-md);
border-radius: var(--_my-component-radius);
background: var(--_my-component-bg);
gap: var(--my-component-gap);
}
[data-my-component][data-disabled] {
opacity: var(--opacity-disabled);
pointer-events: none;
}
```
### Step 5 — Wire the CSS
Code-split components import their own CSS from the wrapper `.svelte`;
layout primitives and shared visuals aggregate in `index.css`:
```css
@import './components/my-component/my-component.css';
```
### Step 6 — Verify
```bash
npm run generate:eidos-css
npm test -- src/uix/eidos
npm run morfo:check
node --import tsx/esm scripts/eidos-lint.ts my-component
```
### Common anti-pattern: declaring composed tokens at `:root`
```ts
// ❌ WRONG — the TSC will fail
'my-component': {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'solid-bg': 'var(--my-component-palette-solid)' // ← implicit 'root' scope, deps live in 'host'
}
// ✓ CORRECT
'my-component': {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'solid-bg': {
value: 'var(--my-component-palette-solid)',
scope: 'host'
}
}
```
---
## How to define a theme
feat(eidos)!: un tema DICE lo que es — appearance obligatorio en ThemeDefinition, una sola fuente de verdad 39edbc8db dejaba la apariencia como OPCION del que renderiza (RenderThemeCssOptions.appearance, pasada a mano por generated-css.ts): apply() y renderCss() salian mudos, y habia DOS fuentes de verdad porque la apariencia ya vivia implicita en el sufijo del id (/-(light|dark)$/). Forma firmada por el autor: - ThemeDefinition.appearance: ThemeEffective (el tipo de $libs/theme, sin union nueva), OBLIGATORIO. Tres palabras: mode = la preferencia (data-mode) · theme = lo pintado (data-theme) · appearance = que ES el tema. No `colorScheme`: ese nombre es de la paleta derivada de seed. - renderThemeCss lo lee del TEMA y lo emite SIEMPRE como primera declaracion; la opcion desaparece sin shim. apply(), renderCss() y el generador lo emiten gratis. - El sufijo del id baja a convencion de BUSQUEDA del resolver: el validador exige el campo (gate de forma, el que protege documentos) y cierra la contradiccion sufijo<->apariencia (gate semantico). Un tema nuevo por EidosConfigPatch sin el campo cae en el primero. - Todo bloque que pinta una apariencia la declara: el bloque del seed (applyColorScheme, que admite forzar el donante) emite color-scheme desde la apariencia del tema DONANTE resuelto, no del mode crudo; helper #resolveSchemeDonorThemeId compartido para resolverlo una vez. - Documento persistido v1 -> v2, sin migracion: nadie puede adivinar la apariencia de un tema viejo. generated/ no cambia ni un byte (las dos lineas ya estaban; ahora vienen del tema): renderGeneratedBaseEidosCss() sigue en 5751 nombres unicos / 8034 ocurrencias, contrato y censo quietos. Suite eidos + value-channels 460/460 (8 tests nuevos, 4 mutaciones probadas). check 71 -> 73: los dos son web/routes/alpha/lib/docs-theme.ts sin appearance — el arbol congelado pierde el tipo por diseño y entra en el ledger con la unica excepcion escrita (crece SOLO cuando el framework se mueve bajo el arbol provisional); grafito.ts y theme-variants.ts ya fallaban por literal y no suben. src/ a cero. check:gate OK, eidos:lint 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
Every theme declares **what it is** — `appearance: 'light' | 'dark'`. It is
mandatory, never inferred: it governs `color-scheme`, the first declaration
of the theme block, which rules the surface the UA paints and eidos cannot
style (the native `<select>` option popup, form controls, scrollbars).
Three words, three meanings, no overlap:
| Word | Means | Lives in |
| ------------ | --------------------------- | ---------------------------- |
| `mode` | the PREFERENCE the user set | `data-mode` |
| `theme` | what is PAINTED | `data-theme` |
| `appearance` | what the theme IS | `ThemeDefinition.appearance` |
A `-light` / `-dark` suffix on a theme id is the default resolver's **lookup**
convention (`${theme}-${mode}`), not a source of truth; the config validator
refuses a theme whose suffix contradicts its declared `appearance`. (The name
`colorScheme` is taken in eidos by the seed-derived palette,
`applyColorScheme` — hence `appearance`.)
Eidos supports **3 modes** of theme definition:
### Mode 1 — A patch over the base theme (recommended)
Change only what you need; everything else follows the base:
```ts
import { ActiveEidos } from '$uix/eidos';
ActiveEidos.create({
themeBase: {
semantics: {
color: {
roles: {
primary: 'blue', // primary uses the Radix blue scale
secondary: 'plum'
}
}
},
primitives: {
typography: {
families: {
primary: { family: 'Inter' }
}
}
}
},
applyDom: true
});
```
### Mode 2 — A full config
For authoring from scratch:
```ts
import { ActiveEidos, defineEidosConfig } from '$uix/eidos';
const config = defineEidosConfig({
primitives: { /* … */ },
semantics: { color: { /* … */ } },
feat(eidos)!: un tema DICE lo que es — appearance obligatorio en ThemeDefinition, una sola fuente de verdad 39edbc8db dejaba la apariencia como OPCION del que renderiza (RenderThemeCssOptions.appearance, pasada a mano por generated-css.ts): apply() y renderCss() salian mudos, y habia DOS fuentes de verdad porque la apariencia ya vivia implicita en el sufijo del id (/-(light|dark)$/). Forma firmada por el autor: - ThemeDefinition.appearance: ThemeEffective (el tipo de $libs/theme, sin union nueva), OBLIGATORIO. Tres palabras: mode = la preferencia (data-mode) · theme = lo pintado (data-theme) · appearance = que ES el tema. No `colorScheme`: ese nombre es de la paleta derivada de seed. - renderThemeCss lo lee del TEMA y lo emite SIEMPRE como primera declaracion; la opcion desaparece sin shim. apply(), renderCss() y el generador lo emiten gratis. - El sufijo del id baja a convencion de BUSQUEDA del resolver: el validador exige el campo (gate de forma, el que protege documentos) y cierra la contradiccion sufijo<->apariencia (gate semantico). Un tema nuevo por EidosConfigPatch sin el campo cae en el primero. - Todo bloque que pinta una apariencia la declara: el bloque del seed (applyColorScheme, que admite forzar el donante) emite color-scheme desde la apariencia del tema DONANTE resuelto, no del mode crudo; helper #resolveSchemeDonorThemeId compartido para resolverlo una vez. - Documento persistido v1 -> v2, sin migracion: nadie puede adivinar la apariencia de un tema viejo. generated/ no cambia ni un byte (las dos lineas ya estaban; ahora vienen del tema): renderGeneratedBaseEidosCss() sigue en 5751 nombres unicos / 8034 ocurrencias, contrato y censo quietos. Suite eidos + value-channels 460/460 (8 tests nuevos, 4 mutaciones probadas). check 71 -> 73: los dos son web/routes/alpha/lib/docs-theme.ts sin appearance — el arbol congelado pierde el tipo por diseño y entra en el ledger con la unica excepcion escrita (crece SOLO cuando el framework se mueve bajo el arbol provisional); grafito.ts y theme-variants.ts ya fallaban por literal y no suben. src/ a cero. check:gate OK, eidos:lint 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
themes: {
'acme-light': { appearance: 'light', color: { /* … */ } },
'acme-dark': { appearance: 'dark', color: { /* … */ } }
}
});
ActiveEidos.create({ config, applyDom: true });
```
### Mode 3 — A CSS-only theme (no TypeScript)
Eidos publishes the contract as empty CSS for externals to fill:
```ts
const contract = activeEidos.renderContractCss({
themeSelector: "[data-theme='acme-light']"
});
// Output:
// [data-theme='acme-light'] {
// --scale-blue-9: ;
// --color-primary-solid: ;
// --size-md-control-height: ;
// ...
// }
```
The consumer fills in the values:
```css
[data-theme='acme-light'] {
--scale-blue-9: #006adc;
--color-primary-solid: var(--scale-blue-9);
--size-md-control-height: 38px;
}
```
And loads that CSS alongside Eidos's. With `themeSource: 'css'`,
`ActiveEidos` generates no theme of its own.
### Versioned persistence
```ts
const document = activeEidos.toDocument();
feat(eidos)!: un tema DICE lo que es — appearance obligatorio en ThemeDefinition, una sola fuente de verdad 39edbc8db dejaba la apariencia como OPCION del que renderiza (RenderThemeCssOptions.appearance, pasada a mano por generated-css.ts): apply() y renderCss() salian mudos, y habia DOS fuentes de verdad porque la apariencia ya vivia implicita en el sufijo del id (/-(light|dark)$/). Forma firmada por el autor: - ThemeDefinition.appearance: ThemeEffective (el tipo de $libs/theme, sin union nueva), OBLIGATORIO. Tres palabras: mode = la preferencia (data-mode) · theme = lo pintado (data-theme) · appearance = que ES el tema. No `colorScheme`: ese nombre es de la paleta derivada de seed. - renderThemeCss lo lee del TEMA y lo emite SIEMPRE como primera declaracion; la opcion desaparece sin shim. apply(), renderCss() y el generador lo emiten gratis. - El sufijo del id baja a convencion de BUSQUEDA del resolver: el validador exige el campo (gate de forma, el que protege documentos) y cierra la contradiccion sufijo<->apariencia (gate semantico). Un tema nuevo por EidosConfigPatch sin el campo cae en el primero. - Todo bloque que pinta una apariencia la declara: el bloque del seed (applyColorScheme, que admite forzar el donante) emite color-scheme desde la apariencia del tema DONANTE resuelto, no del mode crudo; helper #resolveSchemeDonorThemeId compartido para resolverlo una vez. - Documento persistido v1 -> v2, sin migracion: nadie puede adivinar la apariencia de un tema viejo. generated/ no cambia ni un byte (las dos lineas ya estaban; ahora vienen del tema): renderGeneratedBaseEidosCss() sigue en 5751 nombres unicos / 8034 ocurrencias, contrato y censo quietos. Suite eidos + value-channels 460/460 (8 tests nuevos, 4 mutaciones probadas). check 71 -> 73: los dos son web/routes/alpha/lib/docs-theme.ts sin appearance — el arbol congelado pierde el tipo por diseño y entra en el ledger con la unica excepcion escrita (crece SOLO cuando el framework se mueve bajo el arbol provisional); grafito.ts y theme-variants.ts ya fallaban por literal y no suben. src/ a cero. check:gate OK, eidos:lint 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
// → { kind: 'uix.eidos-config', version: 2, options: {...} }
const json = activeEidos.serialize();
localStorage.setItem('user-theme', json);
// Later
const hydrated = createActiveEidos({
config: JSON.parse(localStorage.getItem('user-theme')!),
prefs, dom
});
```
feat(eidos)!: un tema DICE lo que es — appearance obligatorio en ThemeDefinition, una sola fuente de verdad 39edbc8db dejaba la apariencia como OPCION del que renderiza (RenderThemeCssOptions.appearance, pasada a mano por generated-css.ts): apply() y renderCss() salian mudos, y habia DOS fuentes de verdad porque la apariencia ya vivia implicita en el sufijo del id (/-(light|dark)$/). Forma firmada por el autor: - ThemeDefinition.appearance: ThemeEffective (el tipo de $libs/theme, sin union nueva), OBLIGATORIO. Tres palabras: mode = la preferencia (data-mode) · theme = lo pintado (data-theme) · appearance = que ES el tema. No `colorScheme`: ese nombre es de la paleta derivada de seed. - renderThemeCss lo lee del TEMA y lo emite SIEMPRE como primera declaracion; la opcion desaparece sin shim. apply(), renderCss() y el generador lo emiten gratis. - El sufijo del id baja a convencion de BUSQUEDA del resolver: el validador exige el campo (gate de forma, el que protege documentos) y cierra la contradiccion sufijo<->apariencia (gate semantico). Un tema nuevo por EidosConfigPatch sin el campo cae en el primero. - Todo bloque que pinta una apariencia la declara: el bloque del seed (applyColorScheme, que admite forzar el donante) emite color-scheme desde la apariencia del tema DONANTE resuelto, no del mode crudo; helper #resolveSchemeDonorThemeId compartido para resolverlo una vez. - Documento persistido v1 -> v2, sin migracion: nadie puede adivinar la apariencia de un tema viejo. generated/ no cambia ni un byte (las dos lineas ya estaban; ahora vienen del tema): renderGeneratedBaseEidosCss() sigue en 5751 nombres unicos / 8034 ocurrencias, contrato y censo quietos. Suite eidos + value-channels 460/460 (8 tests nuevos, 4 mutaciones probadas). check 71 -> 73: los dos son web/routes/alpha/lib/docs-theme.ts sin appearance — el arbol congelado pierde el tipo por diseño y entra en el ledger con la unica excepcion escrita (crece SOLO cuando el framework se mueve bajo el arbol provisional); grafito.ts y theme-variants.ts ya fallaban por literal y no suben. src/ a cero. check:gate OK, eidos:lint 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
The document envelope carries a `version` for future migrations. A v1
document (written before `ThemeDefinition.appearance` became mandatory) is
rejected, not migrated — nobody can guess whether an old theme was light or
dark.
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
---
## How to kill the dark-mode flash
`data-theme` / `data-mode` and the rest of the preference attributes are
written by `ActiveEidos.apply()` and by the prefs DOM projection — that is,
after hydration. On a static build (no server, no hooks, `src/app.html` with
no script) that is several frames after first paint, and a dark-mode user
watches a light page turn dark.
The fix is a tiny script in `<head>` that stamps the SAME attributes before
paint. It must not be hand-written: a second, hand-rolled copy of the
resolution cascade drifts from the real one on the first rename. UIX compiles
it instead, from the runtime's own modules.
feat(active-uix)!: el boot viaja como CUERPO CONSTANTE con los parámetros en un atributo — un hash de CSP fijo, sin node:fs, y la delta deja de confirmarse a sí misma (fila 1) La receta documentada del boot pre-hidratación no funcionaba en ningún despliegue real, y el panel de diseño de nueve agentes que el autor pidió lo confirmó midiendo: con el `render.ts` anterior, un build REAL de SvelteKit con adapter-static revienta el prerender con `ENOENT …/output/server/generated/boot.js` y devuelve un 500. La causa: `renderUixBootScript` leía el bundle DEL DISCO junto a su propio módulo, y una vez empaquetado ese fichero no está ahí. FORMA NUEVA. El generador compila una entrada propia (`boot/entry.ts`), sin `globalName`, y emite el MISMO fichero de siempre (`generated/boot.js`, sin borrar nada) como módulo con dos constantes: el texto exacto del script y su `sha256-…`. `renderUixBootScript` pierde `node:fs`, `node:path`, `node:url`, la caché y el envoltorio que estaba ESCRITO A MANO: al no haber `globalName`, la llamada la hace la propia entrada compilada. «Compilado, no escrito» sale reforzado, no rebajado. LOS PARÁMETROS VIAJAN EN UN ATRIBUTO del mismo `<script>`, no dentro del cuerpo. Consecuencia, que es el punto entero de la fila: el cuerpo es CONSTANTE, así que su hash es constante por versión del framework, igual para todos los sitios y todos los `themeIds`. La guía pasa a tener UNA sola receta, por hash, que cubre prerender, SSR y hasta `mode: 'nonce'`; el nonce baja a salida de emergencia. Un bloque `application/json` hermano se descartó con razón medida: rompería la invariante de que el tag parsea a exactamente un `<script>` y un minificador de HTML podría borrarlo sin que nadie se entere. TRES MENTIRAS DE LA GUÍA, CORREGIDAS. `event.locals.nonce` no existe en Kit 2.55.0 (cero ocurrencias en su código; `respond.js:168` es `locals: {}`), así que la receta anterior pasaba `undefined`, el navegador bloqueaba el script y volvía el flash sin un solo error. El placeholder estaba ENCIMA de `%sveltekit.head%`, donde la política de prerender de Kit no lo alcanza: eso es evadir la CSP, no cumplirla, y no vale cuando la política llega por cabecera. Y el `html.replace` con cadena interpreta `$'`, así que ahora la receta usa reemplazo FUNCIÓN. Se añade el aviso que puede tumbarle la página al consumidor: añadir un hash a un `script-src` que lleva `'unsafe-inline'` hace que el navegador IGNORE `unsafe-inline` y bloquee los demás scripts inline del sitio, incluido el arranque de Kit. EL CRITERIO SE CORRIGE. `boot-delta.test.ts` gana un tercer snapshot contra un documento LIMPIO. El anterior arrancaba el runtime sobre el documento ya sellado, y como `directionDimension.derive` devuelve `env.direction` antes de derivar del idioma y el entorno se siembra leyendo el `<html dir>` que el boot acaba de escribir, la columna `dir` se confirmaba a sí misma. Medido con el boot clavado a `rtl`: los cinco rojos caen todos en la lectura nueva y ninguno en la anterior. AUDIT DEV. La raíz compara en desarrollo su primera proyección con lo que ya hay en `<html>` y NOMBRA los ejes que discrepan. Convierte en ruidosos cinco fallos que hoy son idénticos y mudos: pin olvidado, `themeIds` incompleto, `storageKey` distinto, artefacto rancio y script bloqueado por la CSP. Artefacto 14 666 B, `sha256-5fulJ/2Q6DttR258KqIhWSwfm5g7973BEzzzq14fapY=`, generación determinista. Verificación: suite entera 461 ficheros / 5404 tests, exit 0 · check con 0 errores bajo `src/` y bajo `scripts/` (89 en `web/`, ledger intacto) · docs:check 0/0/819 · `npx vitest run src/uix/active-uix` 9 ficheros / 97 tests. El test nuevo `boot/pack.test.ts` empaqueta `render.ts` y lo ejecuta: sale ROJO contra el código anterior con el ENOENT literal, y es lo único que impide que esta clase vuelva. Adversarial Opus independiente (árbol byte-idéntico al terminar, 14 mutaciones revertidas): midió el hash bajo CSP real contra un servidor HTTP en Chromium 145, Firefox 146 y WebKit 26, con control negativo bloqueado; montó una app SvelteKit real siguiendo la guía al pie de la letra y la vio construir, prerenderizar y arrancar sin violaciones; y probó el ENOENT con un build real. Declaró 5 defectos del lote, ninguno bloqueante, cerrados en una segunda ronda y re-verificados uno a uno con su reproducción original: un universal falso en un comentario, la puerta del artefacto sin validar el cuerpo (ahora con guard propio importable), la constante del hash sin comillas para quien escriba su propia cabecera, el audit que SÍ viaja al bundle de producción (1 428 B medidos, no un supuesto; no CORRE, gracias a un ternario que el bundler no pliega) y el eje que se nombraba a profundidad 2 y ahora va en el mensaje. La redacción del guard del artefacto, último defecto de la segunda pasada, la corregí yo: corre en cada render, 76 us con artefacto frente a 1,49 us sin él, medido. Filas que deja para el autor: los 1 428 B del audit en producción, si se gatean con una bandera que el bundler pliegue · el audit queda mudo si el runtime se desecha en el mismo turno · la fila 2 debe decidir si la puerta verifica que el hash describa al cuerpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
### Step 1 — Generate the artifact (once, and after touching the schema)
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
```bash
npm run generate:boot
```
feat(active-uix)!: el boot viaja como CUERPO CONSTANTE con los parámetros en un atributo — un hash de CSP fijo, sin node:fs, y la delta deja de confirmarse a sí misma (fila 1) La receta documentada del boot pre-hidratación no funcionaba en ningún despliegue real, y el panel de diseño de nueve agentes que el autor pidió lo confirmó midiendo: con el `render.ts` anterior, un build REAL de SvelteKit con adapter-static revienta el prerender con `ENOENT …/output/server/generated/boot.js` y devuelve un 500. La causa: `renderUixBootScript` leía el bundle DEL DISCO junto a su propio módulo, y una vez empaquetado ese fichero no está ahí. FORMA NUEVA. El generador compila una entrada propia (`boot/entry.ts`), sin `globalName`, y emite el MISMO fichero de siempre (`generated/boot.js`, sin borrar nada) como módulo con dos constantes: el texto exacto del script y su `sha256-…`. `renderUixBootScript` pierde `node:fs`, `node:path`, `node:url`, la caché y el envoltorio que estaba ESCRITO A MANO: al no haber `globalName`, la llamada la hace la propia entrada compilada. «Compilado, no escrito» sale reforzado, no rebajado. LOS PARÁMETROS VIAJAN EN UN ATRIBUTO del mismo `<script>`, no dentro del cuerpo. Consecuencia, que es el punto entero de la fila: el cuerpo es CONSTANTE, así que su hash es constante por versión del framework, igual para todos los sitios y todos los `themeIds`. La guía pasa a tener UNA sola receta, por hash, que cubre prerender, SSR y hasta `mode: 'nonce'`; el nonce baja a salida de emergencia. Un bloque `application/json` hermano se descartó con razón medida: rompería la invariante de que el tag parsea a exactamente un `<script>` y un minificador de HTML podría borrarlo sin que nadie se entere. TRES MENTIRAS DE LA GUÍA, CORREGIDAS. `event.locals.nonce` no existe en Kit 2.55.0 (cero ocurrencias en su código; `respond.js:168` es `locals: {}`), así que la receta anterior pasaba `undefined`, el navegador bloqueaba el script y volvía el flash sin un solo error. El placeholder estaba ENCIMA de `%sveltekit.head%`, donde la política de prerender de Kit no lo alcanza: eso es evadir la CSP, no cumplirla, y no vale cuando la política llega por cabecera. Y el `html.replace` con cadena interpreta `$'`, así que ahora la receta usa reemplazo FUNCIÓN. Se añade el aviso que puede tumbarle la página al consumidor: añadir un hash a un `script-src` que lleva `'unsafe-inline'` hace que el navegador IGNORE `unsafe-inline` y bloquee los demás scripts inline del sitio, incluido el arranque de Kit. EL CRITERIO SE CORRIGE. `boot-delta.test.ts` gana un tercer snapshot contra un documento LIMPIO. El anterior arrancaba el runtime sobre el documento ya sellado, y como `directionDimension.derive` devuelve `env.direction` antes de derivar del idioma y el entorno se siembra leyendo el `<html dir>` que el boot acaba de escribir, la columna `dir` se confirmaba a sí misma. Medido con el boot clavado a `rtl`: los cinco rojos caen todos en la lectura nueva y ninguno en la anterior. AUDIT DEV. La raíz compara en desarrollo su primera proyección con lo que ya hay en `<html>` y NOMBRA los ejes que discrepan. Convierte en ruidosos cinco fallos que hoy son idénticos y mudos: pin olvidado, `themeIds` incompleto, `storageKey` distinto, artefacto rancio y script bloqueado por la CSP. Artefacto 14 666 B, `sha256-5fulJ/2Q6DttR258KqIhWSwfm5g7973BEzzzq14fapY=`, generación determinista. Verificación: suite entera 461 ficheros / 5404 tests, exit 0 · check con 0 errores bajo `src/` y bajo `scripts/` (89 en `web/`, ledger intacto) · docs:check 0/0/819 · `npx vitest run src/uix/active-uix` 9 ficheros / 97 tests. El test nuevo `boot/pack.test.ts` empaqueta `render.ts` y lo ejecuta: sale ROJO contra el código anterior con el ENOENT literal, y es lo único que impide que esta clase vuelva. Adversarial Opus independiente (árbol byte-idéntico al terminar, 14 mutaciones revertidas): midió el hash bajo CSP real contra un servidor HTTP en Chromium 145, Firefox 146 y WebKit 26, con control negativo bloqueado; montó una app SvelteKit real siguiendo la guía al pie de la letra y la vio construir, prerenderizar y arrancar sin violaciones; y probó el ENOENT con un build real. Declaró 5 defectos del lote, ninguno bloqueante, cerrados en una segunda ronda y re-verificados uno a uno con su reproducción original: un universal falso en un comentario, la puerta del artefacto sin validar el cuerpo (ahora con guard propio importable), la constante del hash sin comillas para quien escriba su propia cabecera, el audit que SÍ viaja al bundle de producción (1 428 B medidos, no un supuesto; no CORRE, gracias a un ternario que el bundler no pliega) y el eje que se nombraba a profundidad 2 y ahora va en el mensaje. La redacción del guard del artefacto, último defecto de la segunda pasada, la corregí yo: corre en cada render, 76 us con artefacto frente a 1,49 us sin él, medido. Filas que deja para el autor: los 1 428 B del audit en producción, si se gatean con una bandera que el bundler pliegue · el audit queda mudo si el runtime se desecha en el mismo turno · la fila 2 debe decidir si la puerta verifica que el hash describa al cuerpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
Writes `src/uix/active-uix/generated/boot.js`, checked in. It is not a bundle
you have to place: it is two constants — `UIX_BOOT_SCRIPT`, the exact text that
goes inside the `<script>` tag, and `UIX_BOOT_CSP_HASH`, the `script-src` hash
of that text. A vitest keeps both in sync with a live compile, so a stale
artifact fails the gate rather than shipping.
The body never carries your parameters — they ride a `data-uix-boot` attribute
of the same tag — so the hash is constant per framework version. That is what
makes step 4 possible.
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
### Step 2 — A placeholder in `src/app.html`
```html
<head>
%sveltekit.head%
feat(active-uix)!: el boot viaja como CUERPO CONSTANTE con los parámetros en un atributo — un hash de CSP fijo, sin node:fs, y la delta deja de confirmarse a sí misma (fila 1) La receta documentada del boot pre-hidratación no funcionaba en ningún despliegue real, y el panel de diseño de nueve agentes que el autor pidió lo confirmó midiendo: con el `render.ts` anterior, un build REAL de SvelteKit con adapter-static revienta el prerender con `ENOENT …/output/server/generated/boot.js` y devuelve un 500. La causa: `renderUixBootScript` leía el bundle DEL DISCO junto a su propio módulo, y una vez empaquetado ese fichero no está ahí. FORMA NUEVA. El generador compila una entrada propia (`boot/entry.ts`), sin `globalName`, y emite el MISMO fichero de siempre (`generated/boot.js`, sin borrar nada) como módulo con dos constantes: el texto exacto del script y su `sha256-…`. `renderUixBootScript` pierde `node:fs`, `node:path`, `node:url`, la caché y el envoltorio que estaba ESCRITO A MANO: al no haber `globalName`, la llamada la hace la propia entrada compilada. «Compilado, no escrito» sale reforzado, no rebajado. LOS PARÁMETROS VIAJAN EN UN ATRIBUTO del mismo `<script>`, no dentro del cuerpo. Consecuencia, que es el punto entero de la fila: el cuerpo es CONSTANTE, así que su hash es constante por versión del framework, igual para todos los sitios y todos los `themeIds`. La guía pasa a tener UNA sola receta, por hash, que cubre prerender, SSR y hasta `mode: 'nonce'`; el nonce baja a salida de emergencia. Un bloque `application/json` hermano se descartó con razón medida: rompería la invariante de que el tag parsea a exactamente un `<script>` y un minificador de HTML podría borrarlo sin que nadie se entere. TRES MENTIRAS DE LA GUÍA, CORREGIDAS. `event.locals.nonce` no existe en Kit 2.55.0 (cero ocurrencias en su código; `respond.js:168` es `locals: {}`), así que la receta anterior pasaba `undefined`, el navegador bloqueaba el script y volvía el flash sin un solo error. El placeholder estaba ENCIMA de `%sveltekit.head%`, donde la política de prerender de Kit no lo alcanza: eso es evadir la CSP, no cumplirla, y no vale cuando la política llega por cabecera. Y el `html.replace` con cadena interpreta `$'`, así que ahora la receta usa reemplazo FUNCIÓN. Se añade el aviso que puede tumbarle la página al consumidor: añadir un hash a un `script-src` que lleva `'unsafe-inline'` hace que el navegador IGNORE `unsafe-inline` y bloquee los demás scripts inline del sitio, incluido el arranque de Kit. EL CRITERIO SE CORRIGE. `boot-delta.test.ts` gana un tercer snapshot contra un documento LIMPIO. El anterior arrancaba el runtime sobre el documento ya sellado, y como `directionDimension.derive` devuelve `env.direction` antes de derivar del idioma y el entorno se siembra leyendo el `<html dir>` que el boot acaba de escribir, la columna `dir` se confirmaba a sí misma. Medido con el boot clavado a `rtl`: los cinco rojos caen todos en la lectura nueva y ninguno en la anterior. AUDIT DEV. La raíz compara en desarrollo su primera proyección con lo que ya hay en `<html>` y NOMBRA los ejes que discrepan. Convierte en ruidosos cinco fallos que hoy son idénticos y mudos: pin olvidado, `themeIds` incompleto, `storageKey` distinto, artefacto rancio y script bloqueado por la CSP. Artefacto 14 666 B, `sha256-5fulJ/2Q6DttR258KqIhWSwfm5g7973BEzzzq14fapY=`, generación determinista. Verificación: suite entera 461 ficheros / 5404 tests, exit 0 · check con 0 errores bajo `src/` y bajo `scripts/` (89 en `web/`, ledger intacto) · docs:check 0/0/819 · `npx vitest run src/uix/active-uix` 9 ficheros / 97 tests. El test nuevo `boot/pack.test.ts` empaqueta `render.ts` y lo ejecuta: sale ROJO contra el código anterior con el ENOENT literal, y es lo único que impide que esta clase vuelva. Adversarial Opus independiente (árbol byte-idéntico al terminar, 14 mutaciones revertidas): midió el hash bajo CSP real contra un servidor HTTP en Chromium 145, Firefox 146 y WebKit 26, con control negativo bloqueado; montó una app SvelteKit real siguiendo la guía al pie de la letra y la vio construir, prerenderizar y arrancar sin violaciones; y probó el ENOENT con un build real. Declaró 5 defectos del lote, ninguno bloqueante, cerrados en una segunda ronda y re-verificados uno a uno con su reproducción original: un universal falso en un comentario, la puerta del artefacto sin validar el cuerpo (ahora con guard propio importable), la constante del hash sin comillas para quien escriba su propia cabecera, el audit que SÍ viaja al bundle de producción (1 428 B medidos, no un supuesto; no CORRE, gracias a un ternario que el bundler no pliega) y el eje que se nombraba a profundidad 2 y ahora va en el mensaje. La redacción del guard del artefacto, último defecto de la segunda pasada, la corregí yo: corre en cada render, 76 us con artefacto frente a 1,49 us sin él, medido. Filas que deja para el autor: los 1 428 B del audit en producción, si se gatean con una bandera que el bundler pliegue · el audit queda mudo si el runtime se desecha en el mismo turno · la fila 2 debe decidir si la puerta verifica que el hash describa al cuerpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
%uix.boot%
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
</head>
```
feat(active-uix)!: el boot viaja como CUERPO CONSTANTE con los parámetros en un atributo — un hash de CSP fijo, sin node:fs, y la delta deja de confirmarse a sí misma (fila 1) La receta documentada del boot pre-hidratación no funcionaba en ningún despliegue real, y el panel de diseño de nueve agentes que el autor pidió lo confirmó midiendo: con el `render.ts` anterior, un build REAL de SvelteKit con adapter-static revienta el prerender con `ENOENT …/output/server/generated/boot.js` y devuelve un 500. La causa: `renderUixBootScript` leía el bundle DEL DISCO junto a su propio módulo, y una vez empaquetado ese fichero no está ahí. FORMA NUEVA. El generador compila una entrada propia (`boot/entry.ts`), sin `globalName`, y emite el MISMO fichero de siempre (`generated/boot.js`, sin borrar nada) como módulo con dos constantes: el texto exacto del script y su `sha256-…`. `renderUixBootScript` pierde `node:fs`, `node:path`, `node:url`, la caché y el envoltorio que estaba ESCRITO A MANO: al no haber `globalName`, la llamada la hace la propia entrada compilada. «Compilado, no escrito» sale reforzado, no rebajado. LOS PARÁMETROS VIAJAN EN UN ATRIBUTO del mismo `<script>`, no dentro del cuerpo. Consecuencia, que es el punto entero de la fila: el cuerpo es CONSTANTE, así que su hash es constante por versión del framework, igual para todos los sitios y todos los `themeIds`. La guía pasa a tener UNA sola receta, por hash, que cubre prerender, SSR y hasta `mode: 'nonce'`; el nonce baja a salida de emergencia. Un bloque `application/json` hermano se descartó con razón medida: rompería la invariante de que el tag parsea a exactamente un `<script>` y un minificador de HTML podría borrarlo sin que nadie se entere. TRES MENTIRAS DE LA GUÍA, CORREGIDAS. `event.locals.nonce` no existe en Kit 2.55.0 (cero ocurrencias en su código; `respond.js:168` es `locals: {}`), así que la receta anterior pasaba `undefined`, el navegador bloqueaba el script y volvía el flash sin un solo error. El placeholder estaba ENCIMA de `%sveltekit.head%`, donde la política de prerender de Kit no lo alcanza: eso es evadir la CSP, no cumplirla, y no vale cuando la política llega por cabecera. Y el `html.replace` con cadena interpreta `$'`, así que ahora la receta usa reemplazo FUNCIÓN. Se añade el aviso que puede tumbarle la página al consumidor: añadir un hash a un `script-src` que lleva `'unsafe-inline'` hace que el navegador IGNORE `unsafe-inline` y bloquee los demás scripts inline del sitio, incluido el arranque de Kit. EL CRITERIO SE CORRIGE. `boot-delta.test.ts` gana un tercer snapshot contra un documento LIMPIO. El anterior arrancaba el runtime sobre el documento ya sellado, y como `directionDimension.derive` devuelve `env.direction` antes de derivar del idioma y el entorno se siembra leyendo el `<html dir>` que el boot acaba de escribir, la columna `dir` se confirmaba a sí misma. Medido con el boot clavado a `rtl`: los cinco rojos caen todos en la lectura nueva y ninguno en la anterior. AUDIT DEV. La raíz compara en desarrollo su primera proyección con lo que ya hay en `<html>` y NOMBRA los ejes que discrepan. Convierte en ruidosos cinco fallos que hoy son idénticos y mudos: pin olvidado, `themeIds` incompleto, `storageKey` distinto, artefacto rancio y script bloqueado por la CSP. Artefacto 14 666 B, `sha256-5fulJ/2Q6DttR258KqIhWSwfm5g7973BEzzzq14fapY=`, generación determinista. Verificación: suite entera 461 ficheros / 5404 tests, exit 0 · check con 0 errores bajo `src/` y bajo `scripts/` (89 en `web/`, ledger intacto) · docs:check 0/0/819 · `npx vitest run src/uix/active-uix` 9 ficheros / 97 tests. El test nuevo `boot/pack.test.ts` empaqueta `render.ts` y lo ejecuta: sale ROJO contra el código anterior con el ENOENT literal, y es lo único que impide que esta clase vuelva. Adversarial Opus independiente (árbol byte-idéntico al terminar, 14 mutaciones revertidas): midió el hash bajo CSP real contra un servidor HTTP en Chromium 145, Firefox 146 y WebKit 26, con control negativo bloqueado; montó una app SvelteKit real siguiendo la guía al pie de la letra y la vio construir, prerenderizar y arrancar sin violaciones; y probó el ENOENT con un build real. Declaró 5 defectos del lote, ninguno bloqueante, cerrados en una segunda ronda y re-verificados uno a uno con su reproducción original: un universal falso en un comentario, la puerta del artefacto sin validar el cuerpo (ahora con guard propio importable), la constante del hash sin comillas para quien escriba su propia cabecera, el audit que SÍ viaja al bundle de producción (1 428 B medidos, no un supuesto; no CORRE, gracias a un ternario que el bundler no pliega) y el eje que se nombraba a profundidad 2 y ahora va en el mensaje. La redacción del guard del artefacto, último defecto de la segunda pasada, la corregí yo: corre en cada render, 76 us con artefacto frente a 1,49 us sin él, medido. Filas que deja para el autor: los 1 428 B del audit en producción, si se gatean con una bandera que el bundler pliegue · el audit queda mudo si el runtime se desecha en el mismo turno · la fila 2 debe decidir si la puerta verifica que el hash describa al cuerpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
**Below `%sveltekit.head%`**, and the order is load-bearing. On a prerendered
site Kit publishes the policy as a `<meta http-equiv="content-security-policy">`
inside `%sveltekit.head%`, and a `<meta>` policy governs only what comes after
it. A boot placed above it is outside the policy — which is evading the CSP,
not satisfying it — and the trick buys nothing the moment the same policy
arrives as a header.
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
### Step 3 — Three lines in `src/hooks.server.ts`
```ts
import { renderUixBootScript } from '$active-uix/boot/render';
export const handle = ({ event, resolve }) =>
resolve(event, {
transformPageChunk: ({ html }) =>
feat(active-uix)!: el boot viaja como CUERPO CONSTANTE con los parámetros en un atributo — un hash de CSP fijo, sin node:fs, y la delta deja de confirmarse a sí misma (fila 1) La receta documentada del boot pre-hidratación no funcionaba en ningún despliegue real, y el panel de diseño de nueve agentes que el autor pidió lo confirmó midiendo: con el `render.ts` anterior, un build REAL de SvelteKit con adapter-static revienta el prerender con `ENOENT …/output/server/generated/boot.js` y devuelve un 500. La causa: `renderUixBootScript` leía el bundle DEL DISCO junto a su propio módulo, y una vez empaquetado ese fichero no está ahí. FORMA NUEVA. El generador compila una entrada propia (`boot/entry.ts`), sin `globalName`, y emite el MISMO fichero de siempre (`generated/boot.js`, sin borrar nada) como módulo con dos constantes: el texto exacto del script y su `sha256-…`. `renderUixBootScript` pierde `node:fs`, `node:path`, `node:url`, la caché y el envoltorio que estaba ESCRITO A MANO: al no haber `globalName`, la llamada la hace la propia entrada compilada. «Compilado, no escrito» sale reforzado, no rebajado. LOS PARÁMETROS VIAJAN EN UN ATRIBUTO del mismo `<script>`, no dentro del cuerpo. Consecuencia, que es el punto entero de la fila: el cuerpo es CONSTANTE, así que su hash es constante por versión del framework, igual para todos los sitios y todos los `themeIds`. La guía pasa a tener UNA sola receta, por hash, que cubre prerender, SSR y hasta `mode: 'nonce'`; el nonce baja a salida de emergencia. Un bloque `application/json` hermano se descartó con razón medida: rompería la invariante de que el tag parsea a exactamente un `<script>` y un minificador de HTML podría borrarlo sin que nadie se entere. TRES MENTIRAS DE LA GUÍA, CORREGIDAS. `event.locals.nonce` no existe en Kit 2.55.0 (cero ocurrencias en su código; `respond.js:168` es `locals: {}`), así que la receta anterior pasaba `undefined`, el navegador bloqueaba el script y volvía el flash sin un solo error. El placeholder estaba ENCIMA de `%sveltekit.head%`, donde la política de prerender de Kit no lo alcanza: eso es evadir la CSP, no cumplirla, y no vale cuando la política llega por cabecera. Y el `html.replace` con cadena interpreta `$'`, así que ahora la receta usa reemplazo FUNCIÓN. Se añade el aviso que puede tumbarle la página al consumidor: añadir un hash a un `script-src` que lleva `'unsafe-inline'` hace que el navegador IGNORE `unsafe-inline` y bloquee los demás scripts inline del sitio, incluido el arranque de Kit. EL CRITERIO SE CORRIGE. `boot-delta.test.ts` gana un tercer snapshot contra un documento LIMPIO. El anterior arrancaba el runtime sobre el documento ya sellado, y como `directionDimension.derive` devuelve `env.direction` antes de derivar del idioma y el entorno se siembra leyendo el `<html dir>` que el boot acaba de escribir, la columna `dir` se confirmaba a sí misma. Medido con el boot clavado a `rtl`: los cinco rojos caen todos en la lectura nueva y ninguno en la anterior. AUDIT DEV. La raíz compara en desarrollo su primera proyección con lo que ya hay en `<html>` y NOMBRA los ejes que discrepan. Convierte en ruidosos cinco fallos que hoy son idénticos y mudos: pin olvidado, `themeIds` incompleto, `storageKey` distinto, artefacto rancio y script bloqueado por la CSP. Artefacto 14 666 B, `sha256-5fulJ/2Q6DttR258KqIhWSwfm5g7973BEzzzq14fapY=`, generación determinista. Verificación: suite entera 461 ficheros / 5404 tests, exit 0 · check con 0 errores bajo `src/` y bajo `scripts/` (89 en `web/`, ledger intacto) · docs:check 0/0/819 · `npx vitest run src/uix/active-uix` 9 ficheros / 97 tests. El test nuevo `boot/pack.test.ts` empaqueta `render.ts` y lo ejecuta: sale ROJO contra el código anterior con el ENOENT literal, y es lo único que impide que esta clase vuelva. Adversarial Opus independiente (árbol byte-idéntico al terminar, 14 mutaciones revertidas): midió el hash bajo CSP real contra un servidor HTTP en Chromium 145, Firefox 146 y WebKit 26, con control negativo bloqueado; montó una app SvelteKit real siguiendo la guía al pie de la letra y la vio construir, prerenderizar y arrancar sin violaciones; y probó el ENOENT con un build real. Declaró 5 defectos del lote, ninguno bloqueante, cerrados en una segunda ronda y re-verificados uno a uno con su reproducción original: un universal falso en un comentario, la puerta del artefacto sin validar el cuerpo (ahora con guard propio importable), la constante del hash sin comillas para quien escriba su propia cabecera, el audit que SÍ viaja al bundle de producción (1 428 B medidos, no un supuesto; no CORRE, gracias a un ternario que el bundler no pliega) y el eje que se nombraba a profundidad 2 y ahora va en el mensaje. La redacción del guard del artefacto, último defecto de la segunda pasada, la corregí yo: corre en cada render, 76 us con artefacto frente a 1,49 us sin él, medido. Filas que deja para el autor: los 1 428 B del audit en producción, si se gatean con una bandera que el bundler pliegue · el audit queda mudo si el runtime se desecha en el mismo turno · la fila 2 debe decidir si la puerta verifica que el hash describa al cuerpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
html.replace('%uix.boot%', () =>
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
renderUixBootScript({
defaultLocale: 'es',
feat(active-uix)!: el boot viaja como CUERPO CONSTANTE con los parámetros en un atributo — un hash de CSP fijo, sin node:fs, y la delta deja de confirmarse a sí misma (fila 1) La receta documentada del boot pre-hidratación no funcionaba en ningún despliegue real, y el panel de diseño de nueve agentes que el autor pidió lo confirmó midiendo: con el `render.ts` anterior, un build REAL de SvelteKit con adapter-static revienta el prerender con `ENOENT …/output/server/generated/boot.js` y devuelve un 500. La causa: `renderUixBootScript` leía el bundle DEL DISCO junto a su propio módulo, y una vez empaquetado ese fichero no está ahí. FORMA NUEVA. El generador compila una entrada propia (`boot/entry.ts`), sin `globalName`, y emite el MISMO fichero de siempre (`generated/boot.js`, sin borrar nada) como módulo con dos constantes: el texto exacto del script y su `sha256-…`. `renderUixBootScript` pierde `node:fs`, `node:path`, `node:url`, la caché y el envoltorio que estaba ESCRITO A MANO: al no haber `globalName`, la llamada la hace la propia entrada compilada. «Compilado, no escrito» sale reforzado, no rebajado. LOS PARÁMETROS VIAJAN EN UN ATRIBUTO del mismo `<script>`, no dentro del cuerpo. Consecuencia, que es el punto entero de la fila: el cuerpo es CONSTANTE, así que su hash es constante por versión del framework, igual para todos los sitios y todos los `themeIds`. La guía pasa a tener UNA sola receta, por hash, que cubre prerender, SSR y hasta `mode: 'nonce'`; el nonce baja a salida de emergencia. Un bloque `application/json` hermano se descartó con razón medida: rompería la invariante de que el tag parsea a exactamente un `<script>` y un minificador de HTML podría borrarlo sin que nadie se entere. TRES MENTIRAS DE LA GUÍA, CORREGIDAS. `event.locals.nonce` no existe en Kit 2.55.0 (cero ocurrencias en su código; `respond.js:168` es `locals: {}`), así que la receta anterior pasaba `undefined`, el navegador bloqueaba el script y volvía el flash sin un solo error. El placeholder estaba ENCIMA de `%sveltekit.head%`, donde la política de prerender de Kit no lo alcanza: eso es evadir la CSP, no cumplirla, y no vale cuando la política llega por cabecera. Y el `html.replace` con cadena interpreta `$'`, así que ahora la receta usa reemplazo FUNCIÓN. Se añade el aviso que puede tumbarle la página al consumidor: añadir un hash a un `script-src` que lleva `'unsafe-inline'` hace que el navegador IGNORE `unsafe-inline` y bloquee los demás scripts inline del sitio, incluido el arranque de Kit. EL CRITERIO SE CORRIGE. `boot-delta.test.ts` gana un tercer snapshot contra un documento LIMPIO. El anterior arrancaba el runtime sobre el documento ya sellado, y como `directionDimension.derive` devuelve `env.direction` antes de derivar del idioma y el entorno se siembra leyendo el `<html dir>` que el boot acaba de escribir, la columna `dir` se confirmaba a sí misma. Medido con el boot clavado a `rtl`: los cinco rojos caen todos en la lectura nueva y ninguno en la anterior. AUDIT DEV. La raíz compara en desarrollo su primera proyección con lo que ya hay en `<html>` y NOMBRA los ejes que discrepan. Convierte en ruidosos cinco fallos que hoy son idénticos y mudos: pin olvidado, `themeIds` incompleto, `storageKey` distinto, artefacto rancio y script bloqueado por la CSP. Artefacto 14 666 B, `sha256-5fulJ/2Q6DttR258KqIhWSwfm5g7973BEzzzq14fapY=`, generación determinista. Verificación: suite entera 461 ficheros / 5404 tests, exit 0 · check con 0 errores bajo `src/` y bajo `scripts/` (89 en `web/`, ledger intacto) · docs:check 0/0/819 · `npx vitest run src/uix/active-uix` 9 ficheros / 97 tests. El test nuevo `boot/pack.test.ts` empaqueta `render.ts` y lo ejecuta: sale ROJO contra el código anterior con el ENOENT literal, y es lo único que impide que esta clase vuelva. Adversarial Opus independiente (árbol byte-idéntico al terminar, 14 mutaciones revertidas): midió el hash bajo CSP real contra un servidor HTTP en Chromium 145, Firefox 146 y WebKit 26, con control negativo bloqueado; montó una app SvelteKit real siguiendo la guía al pie de la letra y la vio construir, prerenderizar y arrancar sin violaciones; y probó el ENOENT con un build real. Declaró 5 defectos del lote, ninguno bloqueante, cerrados en una segunda ronda y re-verificados uno a uno con su reproducción original: un universal falso en un comentario, la puerta del artefacto sin validar el cuerpo (ahora con guard propio importable), la constante del hash sin comillas para quien escriba su propia cabecera, el audit que SÍ viaja al bundle de producción (1 428 B medidos, no un supuesto; no CORRE, gracias a un ternario que el bundler no pliega) y el eje que se nombraba a profundidad 2 y ahora va en el mensaje. La redacción del guard del artefacto, último defecto de la segunda pasada, la corregí yo: corre en cada render, 76 us con artefacto frente a 1,49 us sin él, medido. Filas que deja para el autor: los 1 428 B del audit en producción, si se gatean con una bandera que el bundler pliegue · el audit queda mudo si el runtime se desecha en el mismo turno · la fila 2 debe decidir si la puerta verifica que el hash describa al cuerpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
themeIds: eidos.listThemes()
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
})
)
});
```
`renderUixBootScript` returns a string and nothing else — the framework does
feat(active-uix)!: el boot viaja como CUERPO CONSTANTE con los parámetros en un atributo — un hash de CSP fijo, sin node:fs, y la delta deja de confirmarse a sí misma (fila 1) La receta documentada del boot pre-hidratación no funcionaba en ningún despliegue real, y el panel de diseño de nueve agentes que el autor pidió lo confirmó midiendo: con el `render.ts` anterior, un build REAL de SvelteKit con adapter-static revienta el prerender con `ENOENT …/output/server/generated/boot.js` y devuelve un 500. La causa: `renderUixBootScript` leía el bundle DEL DISCO junto a su propio módulo, y una vez empaquetado ese fichero no está ahí. FORMA NUEVA. El generador compila una entrada propia (`boot/entry.ts`), sin `globalName`, y emite el MISMO fichero de siempre (`generated/boot.js`, sin borrar nada) como módulo con dos constantes: el texto exacto del script y su `sha256-…`. `renderUixBootScript` pierde `node:fs`, `node:path`, `node:url`, la caché y el envoltorio que estaba ESCRITO A MANO: al no haber `globalName`, la llamada la hace la propia entrada compilada. «Compilado, no escrito» sale reforzado, no rebajado. LOS PARÁMETROS VIAJAN EN UN ATRIBUTO del mismo `<script>`, no dentro del cuerpo. Consecuencia, que es el punto entero de la fila: el cuerpo es CONSTANTE, así que su hash es constante por versión del framework, igual para todos los sitios y todos los `themeIds`. La guía pasa a tener UNA sola receta, por hash, que cubre prerender, SSR y hasta `mode: 'nonce'`; el nonce baja a salida de emergencia. Un bloque `application/json` hermano se descartó con razón medida: rompería la invariante de que el tag parsea a exactamente un `<script>` y un minificador de HTML podría borrarlo sin que nadie se entere. TRES MENTIRAS DE LA GUÍA, CORREGIDAS. `event.locals.nonce` no existe en Kit 2.55.0 (cero ocurrencias en su código; `respond.js:168` es `locals: {}`), así que la receta anterior pasaba `undefined`, el navegador bloqueaba el script y volvía el flash sin un solo error. El placeholder estaba ENCIMA de `%sveltekit.head%`, donde la política de prerender de Kit no lo alcanza: eso es evadir la CSP, no cumplirla, y no vale cuando la política llega por cabecera. Y el `html.replace` con cadena interpreta `$'`, así que ahora la receta usa reemplazo FUNCIÓN. Se añade el aviso que puede tumbarle la página al consumidor: añadir un hash a un `script-src` que lleva `'unsafe-inline'` hace que el navegador IGNORE `unsafe-inline` y bloquee los demás scripts inline del sitio, incluido el arranque de Kit. EL CRITERIO SE CORRIGE. `boot-delta.test.ts` gana un tercer snapshot contra un documento LIMPIO. El anterior arrancaba el runtime sobre el documento ya sellado, y como `directionDimension.derive` devuelve `env.direction` antes de derivar del idioma y el entorno se siembra leyendo el `<html dir>` que el boot acaba de escribir, la columna `dir` se confirmaba a sí misma. Medido con el boot clavado a `rtl`: los cinco rojos caen todos en la lectura nueva y ninguno en la anterior. AUDIT DEV. La raíz compara en desarrollo su primera proyección con lo que ya hay en `<html>` y NOMBRA los ejes que discrepan. Convierte en ruidosos cinco fallos que hoy son idénticos y mudos: pin olvidado, `themeIds` incompleto, `storageKey` distinto, artefacto rancio y script bloqueado por la CSP. Artefacto 14 666 B, `sha256-5fulJ/2Q6DttR258KqIhWSwfm5g7973BEzzzq14fapY=`, generación determinista. Verificación: suite entera 461 ficheros / 5404 tests, exit 0 · check con 0 errores bajo `src/` y bajo `scripts/` (89 en `web/`, ledger intacto) · docs:check 0/0/819 · `npx vitest run src/uix/active-uix` 9 ficheros / 97 tests. El test nuevo `boot/pack.test.ts` empaqueta `render.ts` y lo ejecuta: sale ROJO contra el código anterior con el ENOENT literal, y es lo único que impide que esta clase vuelva. Adversarial Opus independiente (árbol byte-idéntico al terminar, 14 mutaciones revertidas): midió el hash bajo CSP real contra un servidor HTTP en Chromium 145, Firefox 146 y WebKit 26, con control negativo bloqueado; montó una app SvelteKit real siguiendo la guía al pie de la letra y la vio construir, prerenderizar y arrancar sin violaciones; y probó el ENOENT con un build real. Declaró 5 defectos del lote, ninguno bloqueante, cerrados en una segunda ronda y re-verificados uno a uno con su reproducción original: un universal falso en un comentario, la puerta del artefacto sin validar el cuerpo (ahora con guard propio importable), la constante del hash sin comillas para quien escriba su propia cabecera, el audit que SÍ viaja al bundle de producción (1 428 B medidos, no un supuesto; no CORRE, gracias a un ternario que el bundler no pliega) y el eje que se nombraba a profundidad 2 y ahora va en el mensaje. La redacción del guard del artefacto, último defecto de la segunda pasada, la corregí yo: corre en cada render, 76 us con artefacto frente a 1,49 us sin él, medido. Filas que deja para el autor: los 1 428 B del audit en producción, si se gatean con una bandera que el bundler pliegue · el audit queda mudo si el runtime se desecha en el mismo turno · la fila 2 debe decidir si la puerta verifica que el hash describa al cuerpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
not own `app.html` and does not install a hook.
**The replacement is a FUNCTION, always.** Given a string,
`String.prototype.replace` expands `$&`, `` $` `` and `$'` inside it, and those
two characters can appear in a theme id or a storage key: `$'` splices in
everything that followed the placeholder and the `<script>` ends early. A
function replacement is inserted verbatim, which is the only form that keeps
the guarantee the tag's own tests make.
One hook covers a fully static build as well: `adapter-static` renders its
prerendered pages and its `200.html` fallback through `server.respond`, so the
transform runs for both.
### Step 4 — Name the hash in `svelte.config.js`
```js
import { UIX_BOOT_CSP_HASH } from './src/uix/active-uix/generated/boot.js';
const config = {
kit: {
csp: {
mode: 'hash',
directives: { 'script-src': ['self', UIX_BOOT_CSP_HASH] }
}
}
};
```
This is the canonical recipe. Kit computes hashes for the scripts IT emits and
fixes the policy before `transformPageChunk` runs, so the boot's own hash is
yours to declare — and it never changes until you upgrade UIX.
**The constant carries no quotes, because Kit adds them.** It recognises a
`sha256-…` source and writes `'sha256-…'` into the policy itself. A site that
sends its own `Content-Security-Policy` header instead of using `kit.csp` has
to quote it: `script-src 'self' 'sha256-…'`. Unquoted, the token is not a valid
source expression at all — the browser ignores it and blocks the script, in
every engine.
> **Adding a hash to a `script-src` that already carries `'unsafe-inline'`
> makes the browser IGNORE `'unsafe-inline'` and block every other inline
> script on the site, including Kit's own start-up.** That is how CSP Level 2
> and later are specified, not a UIX rule, and it is the one thing here that
> can take a page down while it fixes a flash. If your policy relies on
> `'unsafe-inline'` today, move every inline script to a hash (or a nonce) in
> the same change, or do not add this one.
Skip this step entirely when the site has no CSP.
### The nonce — an emergency exit, not the recipe
`renderUixBootScript({ …, nonce })` exists for a site that runs its OWN policy
and issues its own nonces. That site writes the header by hand, so if it stays
with the hash it also writes the quotes by hand — `'sha256-…'`, step 4. Two
things to know before reaching for the nonce:
- **`event.locals.nonce` does not exist.** Kit starts `locals` as `{}` and
never writes a nonce into it. Passing it renders no attribute at all, the
browser blocks the script, and the flash comes back — silently. The only
nonce Kit exposes is the `%sveltekit.nonce%` substitution, which happens
across the whole template BEFORE `transformPageChunk`; a site that wants it
carries it through a placeholder of its own (`%uix.boot:%sveltekit.nonce%%`
in `app.html`, a regex replacement in the hook).
- **A prerendered page has no nonce at all.** Kit rejects `%sveltekit.nonce%`
in the template and `mode: 'nonce'` when prerendering, because the value
would be baked into a file served to everyone. On `adapter-static` the hash
is the only option.
The nonce must fit the CSP Level 3 nonce grammar (base64 or base64url
characters, at most two trailing `=`), and Kit's always does: it is base64.
Outside that grammar the value is not a valid `'nonce-…'` source — engines that
implement the grammar never match it, and the tolerance some engines show for a
stray `=` is nothing a portable policy can rely on — so `renderUixBootScript`
throws `ActiveUixInvalidBootNonceError` instead of writing it into the tag.
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
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
### The three parameters, and why they are yours
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
- `defaultLocale` — the locale you passed to `createActiveUix({ langs })`. The
preference schema is built around it, so a boot given a different one
resolves a different `lang` than the runtime will.
- `themeIds` — `eidos.listThemes()`. It feeds the "is this family already a
complete theme id?" branch; without it every family gets a `-light` /
`-dark` suffix appended and `data-theme` disagrees with the runtime.
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
- `pins` — the axes your ROOT `ActiveEidos` nailed, if any
(`ActiveEidos.create({ mode: 'dark' })` is a nailed instance, not a
preference). A pin wins over the resolved preference on both sides, so a
site that nails an axis and does not repeat it here paints the user's
preference before hydration and swaps to the pin after it — this section's
flash, inverted. Nothing pinned, nothing to pass.
```ts
renderUixBootScript({
defaultLocale: 'es',
themeIds: eidos.listThemes(),
pins: { mode: 'dark' }
});
```
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
Pass `storageKey` as well if you replaced the canonical persistence adapter
(`prefs: { storage }`) with one that writes somewhere else. Key and adapter
move together or the two readers stop agreeing.
feat(active-uix)!: el boot viaja como CUERPO CONSTANTE con los parámetros en un atributo — un hash de CSP fijo, sin node:fs, y la delta deja de confirmarse a sí misma (fila 1) La receta documentada del boot pre-hidratación no funcionaba en ningún despliegue real, y el panel de diseño de nueve agentes que el autor pidió lo confirmó midiendo: con el `render.ts` anterior, un build REAL de SvelteKit con adapter-static revienta el prerender con `ENOENT …/output/server/generated/boot.js` y devuelve un 500. La causa: `renderUixBootScript` leía el bundle DEL DISCO junto a su propio módulo, y una vez empaquetado ese fichero no está ahí. FORMA NUEVA. El generador compila una entrada propia (`boot/entry.ts`), sin `globalName`, y emite el MISMO fichero de siempre (`generated/boot.js`, sin borrar nada) como módulo con dos constantes: el texto exacto del script y su `sha256-…`. `renderUixBootScript` pierde `node:fs`, `node:path`, `node:url`, la caché y el envoltorio que estaba ESCRITO A MANO: al no haber `globalName`, la llamada la hace la propia entrada compilada. «Compilado, no escrito» sale reforzado, no rebajado. LOS PARÁMETROS VIAJAN EN UN ATRIBUTO del mismo `<script>`, no dentro del cuerpo. Consecuencia, que es el punto entero de la fila: el cuerpo es CONSTANTE, así que su hash es constante por versión del framework, igual para todos los sitios y todos los `themeIds`. La guía pasa a tener UNA sola receta, por hash, que cubre prerender, SSR y hasta `mode: 'nonce'`; el nonce baja a salida de emergencia. Un bloque `application/json` hermano se descartó con razón medida: rompería la invariante de que el tag parsea a exactamente un `<script>` y un minificador de HTML podría borrarlo sin que nadie se entere. TRES MENTIRAS DE LA GUÍA, CORREGIDAS. `event.locals.nonce` no existe en Kit 2.55.0 (cero ocurrencias en su código; `respond.js:168` es `locals: {}`), así que la receta anterior pasaba `undefined`, el navegador bloqueaba el script y volvía el flash sin un solo error. El placeholder estaba ENCIMA de `%sveltekit.head%`, donde la política de prerender de Kit no lo alcanza: eso es evadir la CSP, no cumplirla, y no vale cuando la política llega por cabecera. Y el `html.replace` con cadena interpreta `$'`, así que ahora la receta usa reemplazo FUNCIÓN. Se añade el aviso que puede tumbarle la página al consumidor: añadir un hash a un `script-src` que lleva `'unsafe-inline'` hace que el navegador IGNORE `unsafe-inline` y bloquee los demás scripts inline del sitio, incluido el arranque de Kit. EL CRITERIO SE CORRIGE. `boot-delta.test.ts` gana un tercer snapshot contra un documento LIMPIO. El anterior arrancaba el runtime sobre el documento ya sellado, y como `directionDimension.derive` devuelve `env.direction` antes de derivar del idioma y el entorno se siembra leyendo el `<html dir>` que el boot acaba de escribir, la columna `dir` se confirmaba a sí misma. Medido con el boot clavado a `rtl`: los cinco rojos caen todos en la lectura nueva y ninguno en la anterior. AUDIT DEV. La raíz compara en desarrollo su primera proyección con lo que ya hay en `<html>` y NOMBRA los ejes que discrepan. Convierte en ruidosos cinco fallos que hoy son idénticos y mudos: pin olvidado, `themeIds` incompleto, `storageKey` distinto, artefacto rancio y script bloqueado por la CSP. Artefacto 14 666 B, `sha256-5fulJ/2Q6DttR258KqIhWSwfm5g7973BEzzzq14fapY=`, generación determinista. Verificación: suite entera 461 ficheros / 5404 tests, exit 0 · check con 0 errores bajo `src/` y bajo `scripts/` (89 en `web/`, ledger intacto) · docs:check 0/0/819 · `npx vitest run src/uix/active-uix` 9 ficheros / 97 tests. El test nuevo `boot/pack.test.ts` empaqueta `render.ts` y lo ejecuta: sale ROJO contra el código anterior con el ENOENT literal, y es lo único que impide que esta clase vuelva. Adversarial Opus independiente (árbol byte-idéntico al terminar, 14 mutaciones revertidas): midió el hash bajo CSP real contra un servidor HTTP en Chromium 145, Firefox 146 y WebKit 26, con control negativo bloqueado; montó una app SvelteKit real siguiendo la guía al pie de la letra y la vio construir, prerenderizar y arrancar sin violaciones; y probó el ENOENT con un build real. Declaró 5 defectos del lote, ninguno bloqueante, cerrados en una segunda ronda y re-verificados uno a uno con su reproducción original: un universal falso en un comentario, la puerta del artefacto sin validar el cuerpo (ahora con guard propio importable), la constante del hash sin comillas para quien escriba su propia cabecera, el audit que SÍ viaja al bundle de producción (1 428 B medidos, no un supuesto; no CORRE, gracias a un ternario que el bundler no pliega) y el eje que se nombraba a profundidad 2 y ahora va en el mensaje. La redacción del guard del artefacto, último defecto de la segunda pasada, la corregí yo: corre en cada render, 76 us con artefacto frente a 1,49 us sin él, medido. Filas que deja para el autor: los 1 428 B del audit en producción, si se gatean con una bandera que el bundler pliegue · el audit queda mudo si el runtime se desecha en el mismo turno · la fila 2 debe decidir si la puerta verifica que el hash describa al cuerpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
`nonce` has its own section above, and `artifact` is for a site that compiles a
narrower boot of its own: it replaces the framework's `{ script, hash }` pair,
through the same function, so the hash in your CSP and the body in your page can
never come from two places.
**In development, the root tells you when the two disagree.** `createActiveUix`
reads what the boot stamped on `<html>` before its own projection writes, reads
it again when the turn is over, and names the axis that moved through the
logger. A forgotten `pin`, a `themeIds` missing a family, a `storageKey` that
does not match the adapter, an artifact that lags the schema and a tag the CSP
blocked all produce the same mute flash otherwise. It measures and changes
nothing, and it is silent on a site that never added the tag.
test(active-uix): el boot se prueba por el tag REAL que inyecta el sitio, y el nonce se valida contra la gramática de CSP `renderUixBootScript` es el único camino de producción al boot pre-hidratación (guía de theming, §59) y ningún test lo importaba. La suite de delta cero no lo llamaba: ejecutaba una COPIA de su envoltorio con `new Function`, la segunda implementación que la doctrina del boot prohíbe («compilado, no escrito»). Nadie vigilaba que `pins` llegase al boot, que el escape de `<` impidiera cerrar el tag antes de tiempo, ni el atributo `nonce`. Tres decisiones firmadas por el autor: 1. `boot/render.test.ts` (nuevo, jsdom): forma del tag (un `<script>`, un solo cierre, ningún `<!--`); nonce ausente, válido (base64 y base64url) e inválido; reenvío de TODOS los parámetros medido por comportamiento en `<html>` (`defaultLocale`, `pins`, `themeIds`, `storageKey`); ida y vuelta byte a byte de una clave y un themeId hostiles con `</script>` y `<!--` sin inyectar markup; ningún global tras ejecutar; higiene del bundle generado. 2. La suite de delta ejecuta el tag real: muere `runGeneratedBoot` con su `new Function` y `BOOT_SOURCE`. El único ejecutor vive en `test/boot-script-tag.ts` (DOMParser inerte + `vm.runInThisContext`, script clásico en ámbito global). No eval indirecto: el bundle abre con `"use strict"` y el eval estricto guarda sus `var` en un entorno propio, así que una fuga de `__uixBoot` habría sido invisible (medido en Node y Chromium; un control positivo lo pone en rojo si vuelve). 3. El nonce se valida contra CSP Level 3 `base64-value` y fuera de ella lanza `ActiveUixInvalidBootNonceError` (`uix::boot.invalid_nonce`, con la longitud, nunca el valor). `JSON.stringify` escapaba para JavaScript y no para un atributo HTML: `a"b` salía como `nonce="a\"` más un atributo basura, y `&lt;` llegaba decodificado. La validación ES el escape. Razón precisa (corregida por el adversarial antes de anclar): fuera de la gramática el valor no es un `nonce-source` válido; Chromium 145 y Firefox 146 no lo casan, WebKit 26 tolera `=` de más, así que ninguna política portable puede apoyarse en él. El nonce de SvelteKit 2.55.0 (`btoa` sobre bytes aleatorios) siempre la cumple. `render.ts` localiza el bundle con una ruta de disco resuelta desde el módulo en vez del idioma `new URL(…, import.meta.url)`, que el entorno jsdom de Vitest reescribe a `self.location`. Medido por el adversarial con un build REAL de Vite 7.3.1: neutro en producción (build SSR suelto y con disposición tipo Kit: las dos formas se comportan igual), funciona en dev SSR, y solo la nueva funciona bajo Vitest jsdom. El barrel exporta la clase de error junto a las otras seis. Verificación: suite de `boot` + `test` 5 ficheros / 29 tests → 6 / 52 · check con 0 errores bajo src/ (89 en web/, ledger intacto) · docs:check 0/0 · suite entera 459 ficheros / 5389 tests, exit 0. Mutaciones del constructor, cada una restaurada byte a byte: `pins` fuera del reenvío 4 rojos (2 de ellos en la suite de delta: prueba de que ya pasa por el camino real) · sin escape de `<` 2 · sin envoltorio 1 · sin validación 10 · ejecutor vuelto a eval 1 · gramática sin `^` 6 · `storageKey` sin reenviar 2. Adversarial Opus independiente (árbol intacto al terminar): gramática = el ABNF exacto, 0 discrepancias en ~600 000 entradas contra un oráculo propio; suite sin dependencia de orden en 7 semillas; 3 defectos del lote cerrados en una segunda ronda — la frase absoluta sobre CSP en cuatro sitios, y dos mutaciones que sobrevivían (clave en minúsculas; regex con flag `g` según el orden), re-ejecutadas después por el coordinador: rojo en orden por defecto y con las semillas 42 y 777. Filas PREEXISTENTES que destapó el adversarial, no tocadas aquí (van al handoff para el autor): en cualquier build de servidor empaquetado `renderUixBootScript` lanza ENOENT al leer el bundle; la receta de la guía pasa `event.locals.nonce`, que Kit nunca rellena (el navegador bloquea y vuelve el flash, sin error); y el `html.replace` con cadena interpreta `$'` y puede cerrar el script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
feat(prefs)!: un motor, un sobre, un codigo — mode/theme/density/scaling son prefs, sobre canonico y boot compilado Auditoria P2 #2: los attrs de preferencias se estampaban solo en cliente tras hidratar (app.html sin script, build estatico sin servidor) — flash claro para todo usuario dark — y no podia existir un script de arranque porque no habia contrato de persistencia (PrefsIntentStorage lo aporta cada app) y eidos llevaba un SEGUNDO motor de preferencias (theme / modeSource / densitySource / scalingSource por opciones, sin persistir), por doctrina escrita en arts/prefs/README.md («UIX does not turn it into prefs.theme»). El autor REVOCA esa doctrina. UN MOTOR. `mode` es una dimension de prefs junto a `motion` (mismo patron, `resolveTheme` de $libs/theme, que ya existia como gemelo de `resolveMotion`); `theme` (id de familia), `density` y `scaling` las declara la RAIZ en un modulo PURO (src/uix/active-uix/prefs-schema.ts) — arts/prefs no importa src/uix. Eidos lee `uix.prefs` por su puerto `ActiveEidosPreferenceSource` (src/uix/eidos/prefs-source.ts), como ya consume uix.langs / uix.dom / uix.motion. El resolver de tema baja a una funcion pura `resolveThemeId(theme, mode, isRegistered)` (src/uix/eidos/lib/theme-id.ts); los cuatro nombres de attr de eidos a src/uix/eidos/lib/attrs.ts y los de la proyeccion a src/arts/prefs/dom-attrs.ts: modulos sin Svelte, porque el boot los comparte. UN SOBRE. src/libs/prefs/document.ts: `uix.prefs-intent` v1 bajo la clave `uix.prefs`, espejo del documento de eidos (kind + version estrictos, version desconocida se RECHAZA). Solo intencion — nunca efectivo ni entorno. Adaptador localStorage canonico creado en la raiz standalone (`prefs.storage`: propio gana, `false` desactiva). HIDRATACION SINCRONA: el bridge awaitea `load()` aunque sea sincrono y produciria una primera resolucion con defaults y un salto un tick despues — el flash movido de sitio; la raiz lee el sobre ANTES de crear el motor y lo pasa como `intent`; el bridge queda solo para persistir (skipHydrate). UN CODIGO. El boot no se escribe: se COMPILA con esbuild desde los mismos modulos puros (src/uix/active-uix/boot/boot.ts → generated/boot.js, 15.009 bytes / 5.625 gzip, IIFE sin globales, cero Svelte; test de sincronia, precedente generated/base.css). `renderUixBootScript(params)` (boot/render.ts) es Kit-agnostico: el framework entrega el string; el hook de Kit son tres lineas de la app (receta en docs/theming/guide.md, NO cableada: el layout congelado lleva persistencia propia). Estampa los NUEVE attrs (dir lang data-motion data-sound data-haptic data-theme data-mode data-density data-scaling) → DELTA CERO al hidratar, probado por integracion con la raiz REAL (boot/boot-delta.test.ts, tres casos) y por tres mutaciones (sin hidratacion sincrona → rojo; constante de attr cambiada en un solo lado → rojo; boot compilado con `data-modo` → los tres casos rojos). Hallazgo del test de delta cero: la raiz no aplicaba el entorno del navegador (esperaba que el app llamara detectBrowserEnvironment en onMount) — ahora lo siembra ella (guardado por `document`) y observa sus cambios (watchBrowserEnvironment → patchEnvironment), sustituyendo el listener que eidos tenia en createSystemColorSchemeSource. Cifras: vitest prefs+libs/prefs+active-uix+eidos+value-channels 57/593 · suite completa 453/5306 · check 73 (src/ a cero; ledger intacto) · check:gate OK · eidos:lint 0 · docs:check 0/0 · morfo:vocabulary, arts/blocks/packs/rtl/translations/agent:check OK. ESTADO INTERINO, firmado para manana (acta en CONTINUE-audit-p0.md): `resolvePreferences` LANZA con `uix` + escalar/source (forma firmada originalmente); medido despues, `ActiveEidos.create()` inyecta `uix` SIEMPRE y todas las demos pasan escalares o sources (>=12 sitios, incluido web/routes/uix/+layout@.svelte:511) → el sitio de demos lanza al arrancar hasta la forma (c'): pines si (escalar explicito gana, precedente `options.dom ?? options.uix?.dom`), sources reactivas por eje FUERA, sin throw, pines en el boot. Pendientes anotados: contracts.test.ts:1270-1276 codifica la doctrina revocada; esbuild no es dependencia declarada (transitiva de vite) — decision del autor con servidores parados. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
### The declared limit
The boot reproduces the DEFAULT theme resolution (`resolveThemeId`: registered
id → as written; already mode-qualified → as written; otherwise
`${theme}-${mode}`). An app that passes its own `themeResolver` to
`ActiveEidos` has replaced that function, and the compiled boot cannot know
it: `data-theme` will differ until hydration. Such an app owns its own
pre-paint stamp.

Powered by TurnKey Linux.