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

705 lines
26 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: {
/* … */
}
},
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% %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';
docs: el repositorio, el formato y los guards de texto entran en el corpus; cuatro afirmaciones caducas mueren (documentación del cierre) El plan de cierre cambió cosas de un nivel que el corpus no cubría: la forma del repositorio, la política de formato, el inventario de lo generado y la clase de guard que lee TEXTO fuente. Un inventario previo de todo `docs/**` (más los README de raíz, `src/**` y `apps/**`) midió qué había: los temas de capa estaban cubiertos, y este nivel no. NUEVO - `docs/repository.md` (E0): las zonas y quién escribe en cada una; la LEY de `web/routes/` congelado y sus dos consecuencias (los validadores de navegador siguen manuales; el formato no llega ahí); un solo install y un solo workspace (el argumento de la copia única de Svelte); un solo mapa de importación con el orden como contrato; qué debe cero y qué debe un ledger que solo mengua. Enlazada desde el mapa, el README de la raíz, getting-started y AGENTS.md. - `docs/testing-and-tooling.md` §Format policy: `.prettierignore` enumerado y justificado (cinco clases), el commit único de formato, el `git config blame.ignoreRevsFile` que hay que ejecutar a mano y que Gitea no lo lee. - `docs/testing-and-tooling.md` §Guards that read source text: la doctrina que faltaba. Un guard de texto está acoplado al formateador; el positivo se pone ROJO y te enteras, el NEGATIVO pasa en VERDE sin inspeccionar nada. Los cinco síntomas medidos en el formateo de una sola vez, seis reglas para escribir uno que no dependa del formato, y los cuatro pasos antes de commitear un formateo masivo (neutralidad compilada, la vista de los guards, los validadores que NO están en el gate, un commit puro). CORREGIDO (afirmaciones vivas y falsas) - `README.md` de la raíz: era una plantilla vacía que mandaba `npm install vicen` con el repositorio `private: true` y sin paquete. Ahora es una puerta. - `AGENTS.md`: su pre-flight INVIOLABLE mandaba leer dos guías del árbol congelado (el canónico está migrado), su ejemplo de test apuntaba a `src/lib/ling/`, borrado en el refactor, y describía cuatro librerías que no existen. Además decía que los comentarios en castellano valen, contra CLAUDE.md. - `docs/theming/guide.md`: los cuatro sitios que llamaban `eidos.listThemes()` DENTRO de `hooks.server.ts`, donde no hay instancia; ahora `THEME_IDS` derivado con `listEidosThemes(config)` del módulo que la raíz también importa. - `docs/architecture/active-uix.md`: la regla 6 decía que la raíz no proyecta preferencias; hoy standalone proyecta por defecto (`projectPrefs`, `@default true`) y attach es opt-in. Su ejemplo de arranque montaba una SEGUNDA proyección a mano. - `announce`: el opt-in queda calificado (motor desnudo) frente al cableado por defecto de las raíces, en `book-deviations.md`, `channels.md:58` y el docblock del canal. - `docs/getting-started.md` y `docs/architecture/morfo.md`: la política de `check:gate` también cubre `scripts/`. - `docs/canon/direction-contract.md`: los dueños literales de la marca (`boot`, `projection-<n>`) pasan a la prosa, citables por un guard. - `src/uix/eidos/components/README.md`: regla 8 — un bindable se reenvía con `bind:`, nunca por el spread del resto (el proxy de rest props no lleva `set`, así que el tipo promete lo que no ata). Con el `ref` en la superficie del `Button` y el gap RESUELTO en `cookie-consent`. `docs/canon/vocabularies.md` y `src/libs/emoji/data.ts` aparecen por fin como artefactos generados, con su comando. Ledger: L-133 · L-142 · L-143 · L-154 a ARREGLADO; L-152 conserva los 532 ficheros pero ya con doctrina escrita; nuevas L-161…L-165 (la última, DIFERIDA: nadie obliga aún a que un guard de texto falle con el corpus vacío). Verificación: `npm run gate` exit 0 en 542 s — lint limpio · check:gate OK (89 de web/ en el ledger) · docs:check 0/0 en 822 docs · suite 466/466 ficheros, 5445/5445 tests · apps:check verde. `component:audit` exit 0 con PASS 161 / NEEDS-WORK 5, las cifras de antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
import { THEME_IDS } from './lib/eidos-config';
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
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',
docs: el repositorio, el formato y los guards de texto entran en el corpus; cuatro afirmaciones caducas mueren (documentación del cierre) El plan de cierre cambió cosas de un nivel que el corpus no cubría: la forma del repositorio, la política de formato, el inventario de lo generado y la clase de guard que lee TEXTO fuente. Un inventario previo de todo `docs/**` (más los README de raíz, `src/**` y `apps/**`) midió qué había: los temas de capa estaban cubiertos, y este nivel no. NUEVO - `docs/repository.md` (E0): las zonas y quién escribe en cada una; la LEY de `web/routes/` congelado y sus dos consecuencias (los validadores de navegador siguen manuales; el formato no llega ahí); un solo install y un solo workspace (el argumento de la copia única de Svelte); un solo mapa de importación con el orden como contrato; qué debe cero y qué debe un ledger que solo mengua. Enlazada desde el mapa, el README de la raíz, getting-started y AGENTS.md. - `docs/testing-and-tooling.md` §Format policy: `.prettierignore` enumerado y justificado (cinco clases), el commit único de formato, el `git config blame.ignoreRevsFile` que hay que ejecutar a mano y que Gitea no lo lee. - `docs/testing-and-tooling.md` §Guards that read source text: la doctrina que faltaba. Un guard de texto está acoplado al formateador; el positivo se pone ROJO y te enteras, el NEGATIVO pasa en VERDE sin inspeccionar nada. Los cinco síntomas medidos en el formateo de una sola vez, seis reglas para escribir uno que no dependa del formato, y los cuatro pasos antes de commitear un formateo masivo (neutralidad compilada, la vista de los guards, los validadores que NO están en el gate, un commit puro). CORREGIDO (afirmaciones vivas y falsas) - `README.md` de la raíz: era una plantilla vacía que mandaba `npm install vicen` con el repositorio `private: true` y sin paquete. Ahora es una puerta. - `AGENTS.md`: su pre-flight INVIOLABLE mandaba leer dos guías del árbol congelado (el canónico está migrado), su ejemplo de test apuntaba a `src/lib/ling/`, borrado en el refactor, y describía cuatro librerías que no existen. Además decía que los comentarios en castellano valen, contra CLAUDE.md. - `docs/theming/guide.md`: los cuatro sitios que llamaban `eidos.listThemes()` DENTRO de `hooks.server.ts`, donde no hay instancia; ahora `THEME_IDS` derivado con `listEidosThemes(config)` del módulo que la raíz también importa. - `docs/architecture/active-uix.md`: la regla 6 decía que la raíz no proyecta preferencias; hoy standalone proyecta por defecto (`projectPrefs`, `@default true`) y attach es opt-in. Su ejemplo de arranque montaba una SEGUNDA proyección a mano. - `announce`: el opt-in queda calificado (motor desnudo) frente al cableado por defecto de las raíces, en `book-deviations.md`, `channels.md:58` y el docblock del canal. - `docs/getting-started.md` y `docs/architecture/morfo.md`: la política de `check:gate` también cubre `scripts/`. - `docs/canon/direction-contract.md`: los dueños literales de la marca (`boot`, `projection-<n>`) pasan a la prosa, citables por un guard. - `src/uix/eidos/components/README.md`: regla 8 — un bindable se reenvía con `bind:`, nunca por el spread del resto (el proxy de rest props no lleva `set`, así que el tipo promete lo que no ata). Con el `ref` en la superficie del `Button` y el gap RESUELTO en `cookie-consent`. `docs/canon/vocabularies.md` y `src/libs/emoji/data.ts` aparecen por fin como artefactos generados, con su comando. Ledger: L-133 · L-142 · L-143 · L-154 a ARREGLADO; L-152 conserva los 532 ficheros pero ya con doctrina escrita; nuevas L-161…L-165 (la última, DIFERIDA: nadie obliga aún a que un guard de texto falle con el corpus vacío). Verificación: `npm run gate` exit 0 en 542 s — lint limpio · check:gate OK (89 de web/ en el ledger) · docs:check 0/0 en 822 docs · suite 466/466 ficheros, 5445/5445 tests · apps:check verde. `component:audit` exit 0 con PASS 161 / NEEDS-WORK 5, las cifras de antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
themeIds: THEME_IDS
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.
docs: el repositorio, el formato y los guards de texto entran en el corpus; cuatro afirmaciones caducas mueren (documentación del cierre) El plan de cierre cambió cosas de un nivel que el corpus no cubría: la forma del repositorio, la política de formato, el inventario de lo generado y la clase de guard que lee TEXTO fuente. Un inventario previo de todo `docs/**` (más los README de raíz, `src/**` y `apps/**`) midió qué había: los temas de capa estaban cubiertos, y este nivel no. NUEVO - `docs/repository.md` (E0): las zonas y quién escribe en cada una; la LEY de `web/routes/` congelado y sus dos consecuencias (los validadores de navegador siguen manuales; el formato no llega ahí); un solo install y un solo workspace (el argumento de la copia única de Svelte); un solo mapa de importación con el orden como contrato; qué debe cero y qué debe un ledger que solo mengua. Enlazada desde el mapa, el README de la raíz, getting-started y AGENTS.md. - `docs/testing-and-tooling.md` §Format policy: `.prettierignore` enumerado y justificado (cinco clases), el commit único de formato, el `git config blame.ignoreRevsFile` que hay que ejecutar a mano y que Gitea no lo lee. - `docs/testing-and-tooling.md` §Guards that read source text: la doctrina que faltaba. Un guard de texto está acoplado al formateador; el positivo se pone ROJO y te enteras, el NEGATIVO pasa en VERDE sin inspeccionar nada. Los cinco síntomas medidos en el formateo de una sola vez, seis reglas para escribir uno que no dependa del formato, y los cuatro pasos antes de commitear un formateo masivo (neutralidad compilada, la vista de los guards, los validadores que NO están en el gate, un commit puro). CORREGIDO (afirmaciones vivas y falsas) - `README.md` de la raíz: era una plantilla vacía que mandaba `npm install vicen` con el repositorio `private: true` y sin paquete. Ahora es una puerta. - `AGENTS.md`: su pre-flight INVIOLABLE mandaba leer dos guías del árbol congelado (el canónico está migrado), su ejemplo de test apuntaba a `src/lib/ling/`, borrado en el refactor, y describía cuatro librerías que no existen. Además decía que los comentarios en castellano valen, contra CLAUDE.md. - `docs/theming/guide.md`: los cuatro sitios que llamaban `eidos.listThemes()` DENTRO de `hooks.server.ts`, donde no hay instancia; ahora `THEME_IDS` derivado con `listEidosThemes(config)` del módulo que la raíz también importa. - `docs/architecture/active-uix.md`: la regla 6 decía que la raíz no proyecta preferencias; hoy standalone proyecta por defecto (`projectPrefs`, `@default true`) y attach es opt-in. Su ejemplo de arranque montaba una SEGUNDA proyección a mano. - `announce`: el opt-in queda calificado (motor desnudo) frente al cableado por defecto de las raíces, en `book-deviations.md`, `channels.md:58` y el docblock del canal. - `docs/getting-started.md` y `docs/architecture/morfo.md`: la política de `check:gate` también cubre `scripts/`. - `docs/canon/direction-contract.md`: los dueños literales de la marca (`boot`, `projection-<n>`) pasan a la prosa, citables por un guard. - `src/uix/eidos/components/README.md`: regla 8 — un bindable se reenvía con `bind:`, nunca por el spread del resto (el proxy de rest props no lleva `set`, así que el tipo promete lo que no ata). Con el `ref` en la superficie del `Button` y el gap RESUELTO en `cookie-consent`. `docs/canon/vocabularies.md` y `src/libs/emoji/data.ts` aparecen por fin como artefactos generados, con su comando. Ledger: L-133 · L-142 · L-143 · L-154 a ARREGLADO; L-152 conserva los 532 ficheros pero ya con doctrina escrita; nuevas L-161…L-165 (la última, DIFERIDA: nadie obliga aún a que un guard de texto falle con el corpus vacío). Verificación: `npm run gate` exit 0 en 542 s — lint limpio · check:gate OK (89 de web/ en el ledger) · docs:check 0/0 en 822 docs · suite 466/466 ficheros, 5445/5445 tests · apps:check verde. `component:audit` exit 0 con PASS 161 / NEEDS-WORK 5, las cifras de antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
**`themeIds` has no instance to ask.** The hook runs on the server, where no
`ActiveEidos` exists, so it cannot call `eidos.listThemes()`. Keep the config in
ONE module both sides import — the root passes it to
`ActiveEidos.create({ config })`, the hook derives the list with
`listEidosThemes(config)` (`$uix/eidos/lib/config`) — and the list the boot
receives is the list eidos registers, by construction. Worked example:
[`consuming.md` §6](../consuming.md).
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
**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.
feat(active-uix)!: el boot lo compila el build del SITIO con su propio esquema, y lo que UIX escribe en <html dir> lleva marca de propiedad (fila 2 del boot, F1 del cierre) F1 del plan de cierre del framework. Cierra el último trabajo del eje boot. EL DEFECTO. El boot por defecto compilaba un catálogo de preferencias de UN solo idioma (createDefaultUixPrefsSchema), y la raíz sembraba su entorno leyendo el <html dir> que el boot acababa de escribir: un usuario árabe de una app multiidioma no veía un parpadeo, se quedaba en LTR TODA la sesión, porque el runtime tomaba la salida del boot como si la hubiera declarado la página. EL COMPILADOR POR SITIO. Una sola fusión, composeUixPrefsSchema, la consumen createActiveUix y el boot: no puede derivar. El generador acepta --schema y --out (el especificador boot/boot-schema.ts apunta al esquema del sitio o a boot/default-schema.ts); los guards (sin runas, techo de tamaño, ASCII, sin </script, valores string por atributo) son errores del COMPILADOR con mensajes para el consumidor, porque ahora el artefacto lo produce el sitio. Un esquema que importa el barrel $prefs se rechaza nombrándolo. scripts/uix-boot-check.ts es el guard de rancidez: plugin de Vite que tumba el build con un artefacto viejo (probado con vite build real) y función para CI. Delta cero por esquema: por defecto, multiidioma con árabe, ejes redefinidos y esquema parcial. LA MARCA DE PROPIEDAD, diseño firmado por el autor. El canon de dirección dice que <html dir> es una proyección y nunca una fuente, con una sola excepción: el dir del AUTOR en la plantilla o el servidor. El boot rompió la premisa de esa excepción. Todo lo que UIX escribe en <html dir> lleva ahora PREFS_DIR_PROJECTED_ATTR (el boot con valor «boot», cada proyección del runtime con un token de instancia); la semilla solo adopta un dir SIN marca. Un dir escrito por script no es una fuente: en ejecución la dirección se afirma con prefs.setIntent('direction') o options.prefs.environment, que ganan a la semilla. Se descartaron, midiendo, la marca con valor (cierra el script y congela la dirección al navegar entre layouts) y la inferencia por valor. dispose solo retira lo que todavía es SUYO: SvelteKit crea la raíz del layout nuevo ANTES de destruir la vieja (medido), y un retiro a ciegas dejaba el <html> de la raíz nueva sin dir, lang ni data-motion/sound/haptic (medido hoy: los nueve atributos a null). Mismo principio que el unstamp de sema, que comprueba data-event-id. esbuild se declara como devDependency EXACTA 0.27.4: los bytes del boot y su hash dependen del minificador, y una subida dentro de un rango rompería la sincronía. Boot 15 198 B, sha256-hsqdGYcrRu3oEc0Q3G/A67ApQT3q9c/vT9zMDgxROg8= (el hash se mueve: firmado por el autor). VERIFICACIÓN. Constructor Opus en cuatro rondas y adversarial Opus en dos pasadas independientes, con sus reproducciones repetidas tras cada cierre: en Chromium, entrar en árabe y pasar a inglés y a español sigue al idioma, y al revés también; el dir de plantilla gana antes y después de hidratar y en una raíz recreada; una raíz recreada sobre una viva sigue al idioma con y sin boot; tras create b → destroy a, <html> conserva lo que proyectó b. Diez defectos declarados por el adversarial, cerrados (D1–D10): marca de propiedad, esquema parcial que estampaba "undefined", vigilante de dev mudo, plugin sin test, receta de CI que no cargaba en jsdom, cifras y prosa, y un vite build real que se cae cuando el boot no compila. Mutaciones en rojo, restauradas byte a byte: la proyección no marca · dispose sin guard de dueño (también repetida por el coordinador: 3 rojos) · la semilla compara valor en vez de presencia · marcar el dir del autor. Suite entera 463 ficheros / 5 437 tests, exit 0 · check 0 errores en src/ y en scripts/ (89 en web/, ledger intacto) · docs:check 0/0 · generate:boot dos veces byte-idéntico. LO QUE NO CIERRA (al ledger de cierre): (1) PREEXISTENTE — en la misma navegación entre layouts, el ActiveEidos.dispose de la raíz vieja sigue retirando data-theme/mode/density/scaling de la raíz nueva; exige cambiar el dispose de eidos, otra capa. (2) La propiedad de los atributos proyectados cuelga de la marca de dir: si la plantilla declara la dirección, dispose deja lang y data-motion puestos cuando la raíz se desmonta sin sustituta (benigno). (3) Sin boot, un script que escribe dir antes de la primera raíz es indistinguible de la plantilla y se adopta. (4) El vigilante de dev no reacciona a ficheros nuevos ni a inputs fuera del root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
**The import must point at the artifact you actually SHIP.** The path above is
the framework's own, which is the right one as long as you ship the framework's
own body. Compile a boot of your own (next section) and this line has to move
with it: a hash that does not describe the body in the tag is a blocked script,
and nothing in the build will tell you.
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
**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.
feat(active-uix)!: el boot lo compila el build del SITIO con su propio esquema, y lo que UIX escribe en <html dir> lleva marca de propiedad (fila 2 del boot, F1 del cierre) F1 del plan de cierre del framework. Cierra el último trabajo del eje boot. EL DEFECTO. El boot por defecto compilaba un catálogo de preferencias de UN solo idioma (createDefaultUixPrefsSchema), y la raíz sembraba su entorno leyendo el <html dir> que el boot acababa de escribir: un usuario árabe de una app multiidioma no veía un parpadeo, se quedaba en LTR TODA la sesión, porque el runtime tomaba la salida del boot como si la hubiera declarado la página. EL COMPILADOR POR SITIO. Una sola fusión, composeUixPrefsSchema, la consumen createActiveUix y el boot: no puede derivar. El generador acepta --schema y --out (el especificador boot/boot-schema.ts apunta al esquema del sitio o a boot/default-schema.ts); los guards (sin runas, techo de tamaño, ASCII, sin </script, valores string por atributo) son errores del COMPILADOR con mensajes para el consumidor, porque ahora el artefacto lo produce el sitio. Un esquema que importa el barrel $prefs se rechaza nombrándolo. scripts/uix-boot-check.ts es el guard de rancidez: plugin de Vite que tumba el build con un artefacto viejo (probado con vite build real) y función para CI. Delta cero por esquema: por defecto, multiidioma con árabe, ejes redefinidos y esquema parcial. LA MARCA DE PROPIEDAD, diseño firmado por el autor. El canon de dirección dice que <html dir> es una proyección y nunca una fuente, con una sola excepción: el dir del AUTOR en la plantilla o el servidor. El boot rompió la premisa de esa excepción. Todo lo que UIX escribe en <html dir> lleva ahora PREFS_DIR_PROJECTED_ATTR (el boot con valor «boot», cada proyección del runtime con un token de instancia); la semilla solo adopta un dir SIN marca. Un dir escrito por script no es una fuente: en ejecución la dirección se afirma con prefs.setIntent('direction') o options.prefs.environment, que ganan a la semilla. Se descartaron, midiendo, la marca con valor (cierra el script y congela la dirección al navegar entre layouts) y la inferencia por valor. dispose solo retira lo que todavía es SUYO: SvelteKit crea la raíz del layout nuevo ANTES de destruir la vieja (medido), y un retiro a ciegas dejaba el <html> de la raíz nueva sin dir, lang ni data-motion/sound/haptic (medido hoy: los nueve atributos a null). Mismo principio que el unstamp de sema, que comprueba data-event-id. esbuild se declara como devDependency EXACTA 0.27.4: los bytes del boot y su hash dependen del minificador, y una subida dentro de un rango rompería la sincronía. Boot 15 198 B, sha256-hsqdGYcrRu3oEc0Q3G/A67ApQT3q9c/vT9zMDgxROg8= (el hash se mueve: firmado por el autor). VERIFICACIÓN. Constructor Opus en cuatro rondas y adversarial Opus en dos pasadas independientes, con sus reproducciones repetidas tras cada cierre: en Chromium, entrar en árabe y pasar a inglés y a español sigue al idioma, y al revés también; el dir de plantilla gana antes y después de hidratar y en una raíz recreada; una raíz recreada sobre una viva sigue al idioma con y sin boot; tras create b → destroy a, <html> conserva lo que proyectó b. Diez defectos declarados por el adversarial, cerrados (D1–D10): marca de propiedad, esquema parcial que estampaba "undefined", vigilante de dev mudo, plugin sin test, receta de CI que no cargaba en jsdom, cifras y prosa, y un vite build real que se cae cuando el boot no compila. Mutaciones en rojo, restauradas byte a byte: la proyección no marca · dispose sin guard de dueño (también repetida por el coordinador: 3 rojos) · la semilla compara valor en vez de presencia · marcar el dir del autor. Suite entera 463 ficheros / 5 437 tests, exit 0 · check 0 errores en src/ y en scripts/ (89 en web/, ledger intacto) · docs:check 0/0 · generate:boot dos veces byte-idéntico. LO QUE NO CIERRA (al ledger de cierre): (1) PREEXISTENTE — en la misma navegación entre layouts, el ActiveEidos.dispose de la raíz vieja sigue retirando data-theme/mode/density/scaling de la raíz nueva; exige cambiar el dispose de eidos, otra capa. (2) La propiedad de los atributos proyectados cuelga de la marca de dir: si la plantilla declara la dirección, dispose deja lang y data-motion puestos cuando la raíz se desmonta sin sustituta (benigno). (3) Sin boot, un script que escribe dir antes de la primera raíz es indistinguible de la plantilla y se adopta. (4) El vigilante de dev no reacciona a ficheros nuevos ni a inputs fuera del root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
### Your schema, your boot — the per-site compiler
Steps 1–4 ship the artifact UIX compiles for itself, and it is built around
UIX's DEFAULT preference schema: **one language**, one currency, the four
visual axes. If your app passes a schema of its own to
`createActiveUix({ prefs: { schema } })`, that boot is resolving somebody
else's preferences — and on a multi-language site the page paints in the wrong
language and the wrong direction.
Measured in Chromium, on a three-language site (`es` / `ar` / `en`) and a
browser asking for Arabic:
| reading | `dir` | `lang` |
| ------------------------------------------------------------- | --------- | -------- |
| first paint, with the boot compiled around the default schema | `ltr` | `es` |
| the same page after hydration, about half a second later | `rtl` | `ar` |
| first paint, with the boot compiled around the SITE's schema | **`rtl`** | **`ar`** |
feat(active-uix)!: el boot lo compila el build del SITIO con su propio esquema, y lo que UIX escribe en <html dir> lleva marca de propiedad (fila 2 del boot, F1 del cierre) F1 del plan de cierre del framework. Cierra el último trabajo del eje boot. EL DEFECTO. El boot por defecto compilaba un catálogo de preferencias de UN solo idioma (createDefaultUixPrefsSchema), y la raíz sembraba su entorno leyendo el <html dir> que el boot acababa de escribir: un usuario árabe de una app multiidioma no veía un parpadeo, se quedaba en LTR TODA la sesión, porque el runtime tomaba la salida del boot como si la hubiera declarado la página. EL COMPILADOR POR SITIO. Una sola fusión, composeUixPrefsSchema, la consumen createActiveUix y el boot: no puede derivar. El generador acepta --schema y --out (el especificador boot/boot-schema.ts apunta al esquema del sitio o a boot/default-schema.ts); los guards (sin runas, techo de tamaño, ASCII, sin </script, valores string por atributo) son errores del COMPILADOR con mensajes para el consumidor, porque ahora el artefacto lo produce el sitio. Un esquema que importa el barrel $prefs se rechaza nombrándolo. scripts/uix-boot-check.ts es el guard de rancidez: plugin de Vite que tumba el build con un artefacto viejo (probado con vite build real) y función para CI. Delta cero por esquema: por defecto, multiidioma con árabe, ejes redefinidos y esquema parcial. LA MARCA DE PROPIEDAD, diseño firmado por el autor. El canon de dirección dice que <html dir> es una proyección y nunca una fuente, con una sola excepción: el dir del AUTOR en la plantilla o el servidor. El boot rompió la premisa de esa excepción. Todo lo que UIX escribe en <html dir> lleva ahora PREFS_DIR_PROJECTED_ATTR (el boot con valor «boot», cada proyección del runtime con un token de instancia); la semilla solo adopta un dir SIN marca. Un dir escrito por script no es una fuente: en ejecución la dirección se afirma con prefs.setIntent('direction') o options.prefs.environment, que ganan a la semilla. Se descartaron, midiendo, la marca con valor (cierra el script y congela la dirección al navegar entre layouts) y la inferencia por valor. dispose solo retira lo que todavía es SUYO: SvelteKit crea la raíz del layout nuevo ANTES de destruir la vieja (medido), y un retiro a ciegas dejaba el <html> de la raíz nueva sin dir, lang ni data-motion/sound/haptic (medido hoy: los nueve atributos a null). Mismo principio que el unstamp de sema, que comprueba data-event-id. esbuild se declara como devDependency EXACTA 0.27.4: los bytes del boot y su hash dependen del minificador, y una subida dentro de un rango rompería la sincronía. Boot 15 198 B, sha256-hsqdGYcrRu3oEc0Q3G/A67ApQT3q9c/vT9zMDgxROg8= (el hash se mueve: firmado por el autor). VERIFICACIÓN. Constructor Opus en cuatro rondas y adversarial Opus en dos pasadas independientes, con sus reproducciones repetidas tras cada cierre: en Chromium, entrar en árabe y pasar a inglés y a español sigue al idioma, y al revés también; el dir de plantilla gana antes y después de hidratar y en una raíz recreada; una raíz recreada sobre una viva sigue al idioma con y sin boot; tras create b → destroy a, <html> conserva lo que proyectó b. Diez defectos declarados por el adversarial, cerrados (D1–D10): marca de propiedad, esquema parcial que estampaba "undefined", vigilante de dev mudo, plugin sin test, receta de CI que no cargaba en jsdom, cifras y prosa, y un vite build real que se cae cuando el boot no compila. Mutaciones en rojo, restauradas byte a byte: la proyección no marca · dispose sin guard de dueño (también repetida por el coordinador: 3 rojos) · la semilla compara valor en vez de presencia · marcar el dir del autor. Suite entera 463 ficheros / 5 437 tests, exit 0 · check 0 errores en src/ y en scripts/ (89 en web/, ledger intacto) · docs:check 0/0 · generate:boot dos veces byte-idéntico. LO QUE NO CIERRA (al ledger de cierre): (1) PREEXISTENTE — en la misma navegación entre layouts, el ActiveEidos.dispose de la raíz vieja sigue retirando data-theme/mode/density/scaling de la raíz nueva; exige cambiar el dispose de eidos, otra capa. (2) La propiedad de los atributos proyectados cuelga de la marca de dir: si la plantilla declara la dirección, dispose deja lang y data-motion puestos cuando la raíz se desmonta sin sustituta (benigno). (3) Sin boot, un script que escribe dir antes de la primera raíz es indistinguible de la plantilla y se adopta. (4) El vigilante de dev no reacciona a ficheros nuevos ni a inputs fuera del root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
The first row is what the user sees: an Arabic page laid out left to right
and announced in Spanish, until the framework hydrates and the whole page
flips. The dev audit names it (`the pre-hydration boot disagrees with the
runtime on dir, lang`); nothing in a production build does.
**Every `dir` UIX writes is marked as UIX's.** The runtime reads `<html dir>`
as an assertion only when it is YOURS — written in your page template or by
your server — and then it beats the language, before paint and after it. The
`dir` the boot derives, and every `dir` a root projects, carries
`data-dir-projected`, so a root skips it and keeps deriving: a language the
user picks later still moves the direction, on this root and on the next one a
layout mounts. A script that writes over it is not a source; at run time,
assert a direction with `prefs.setIntent('direction', …)` or
`prefs.environment`. The rule is the
[direction contract's](../canon/direction-contract.md), §6.
**What you write: one pure module, imported twice.**
```ts
// src/prefs-schema.ts
import { standardPrefsDimensions } from '$prefs/standard';
import type { SupportedLocale } from '$libs/langs';
import type { PrefsSchema } from '$libs/prefs';
export function bootPrefsSchema(defaultLocale: SupportedLocale): PrefsSchema {
return standardPrefsDimensions({
languages: [defaultLocale, 'ar', 'en'],
locales: [defaultLocale, 'ar', 'en'],
currencies: ['EUR', 'USD'],
defaults: { language: defaultLocale, locale: defaultLocale, currency: 'EUR' }
});
}
```
The export name is the contract — the compiler resolves `bootPrefsSchema` by
name. The same function is what your root composes:
```ts
createActiveUix({
langs: { schema: strings, defaultLocale: 'es' },
prefs: { schema: bootPrefsSchema('es') }
});
```
One module, two consumers. The merge with the four visual axes happens inside
both of them (`composeUixPrefsSchema`), so do not add the axes yourself: they
travel with the root and your schema goes ON TOP — it can redefine an axis,
never omit one.
**The same locale on both sides.** `defaultLocale` in the tag is the argument
your schema function receives before paint, and the one your root builds its
own schema around. Two different values are two different catalogues, which is
this section's defect with extra steps.
> **The module must be PURE, and it must import DEEP modules.** No runes, no
> `$app/*`, and never the `$prefs` barrel: the barrel re-exports a
> `.svelte.ts`, so it puts `$state(` in a plain `<script>` and the boot dies
> on line one — measured, and it takes the artifact from 15 KB to 56 KB on
> the way. The compiler refuses to write that artifact and says why.
**Compile it** — from YOUR directory, so the paths mean what they look like.
Until the `bin` entry below exists, the compiler is a script inside the
framework and you name it by path; `tsx` is what runs a `.ts` entry, so add it
to your own devDependencies:
```bash
node --import tsx/esm <uix>/scripts/generate-boot.ts \
--schema src/prefs-schema.ts --out src/generated/boot.js
```
**`--schema` and `--out` are resolved against the CURRENT directory**, and that
is the one thing to get right. Run the framework's own `npm run generate:boot`
and the current directory is the FRAMEWORK's, so `src/prefs-schema.ts` means a
file in UIX and `--out src/generated/boot.js` drops your artifact inside UIX's
checkout. From a framework checkout, pass both as absolute paths. A `--schema`
that resolves to nothing is an error that names the directory it tried.
`--out` decides where YOUR artifact lands; with neither flag the command
reproduces the framework's own, byte for byte. An unknown flag is an error,
never a shrug: a compiler that ignored `--schema` would hand you the default
boot while you believed you had compiled your own. And the compiler RUNS what it
wrote before writing it: a schema that throws, or that exports `bootPrefsSchema`
as anything but a function, fails the compile instead of producing an artifact
that stamps nothing in silence — and so does an attribute the boot would stamp
with something other than a string IN THAT RUN, which a browser would write on
`<html>` as the word `undefined`. It is one run: `defaultLocale` `'en'`, and no
`navigator`, `matchMedia` or `localStorage`. A `density` that answers
`comfortable` there and nothing for an Arabic browser compiles, and that browser
gets `data-density="undefined"`. A preference your schema does not declare at
all (no `language`, say), or one of `dir`, `lang`, `data-motion`, `data-sound`
and `data-haptic` that resolves to nothing, is left off `<html>`, as the runtime
leaves it.
Give it a name in your own `package.json`, because you will run it again on
every schema change and on every UIX upgrade:
```json
{
"scripts": {
"boot": "node --import tsx/esm <uix>/scripts/generate-boot.ts --schema src/prefs-schema.ts --out src/generated/boot.js"
}
}
```
Then step 4 imports `UIX_BOOT_CSP_HASH` from **your** file instead of the
framework's — your body ships, so your hash is the one the policy must name.
> **Move that import, or the browser blocks your boot.** The two artifacts are
> both legitimate and nothing downstream compares them: the staleness guard
> below checks that YOUR artifact is fresh, and `renderUixBootScript` never
> reads `artifact.hash` at all. Ship your body under the framework's hash and
> the page is worse than it was before you started — the tag is blocked, the
> boot writes nothing, and hydration does the whole flash. Measured in
> Chromium: `Executing inline script violates … Content-Security-Policy`, then
> `dir=rtl lang=ar` arriving 400 ms late. One import, one artifact, in both
> places.
**The placeholder, the hook and the tag do not change.** The artifact enters
through the door `renderUixBootScript` already has:
```ts
import { UIX_BOOT_SCRIPT, UIX_BOOT_CSP_HASH } from './generated/boot.js';
renderUixBootScript({
defaultLocale: 'es',
docs: el repositorio, el formato y los guards de texto entran en el corpus; cuatro afirmaciones caducas mueren (documentación del cierre) El plan de cierre cambió cosas de un nivel que el corpus no cubría: la forma del repositorio, la política de formato, el inventario de lo generado y la clase de guard que lee TEXTO fuente. Un inventario previo de todo `docs/**` (más los README de raíz, `src/**` y `apps/**`) midió qué había: los temas de capa estaban cubiertos, y este nivel no. NUEVO - `docs/repository.md` (E0): las zonas y quién escribe en cada una; la LEY de `web/routes/` congelado y sus dos consecuencias (los validadores de navegador siguen manuales; el formato no llega ahí); un solo install y un solo workspace (el argumento de la copia única de Svelte); un solo mapa de importación con el orden como contrato; qué debe cero y qué debe un ledger que solo mengua. Enlazada desde el mapa, el README de la raíz, getting-started y AGENTS.md. - `docs/testing-and-tooling.md` §Format policy: `.prettierignore` enumerado y justificado (cinco clases), el commit único de formato, el `git config blame.ignoreRevsFile` que hay que ejecutar a mano y que Gitea no lo lee. - `docs/testing-and-tooling.md` §Guards that read source text: la doctrina que faltaba. Un guard de texto está acoplado al formateador; el positivo se pone ROJO y te enteras, el NEGATIVO pasa en VERDE sin inspeccionar nada. Los cinco síntomas medidos en el formateo de una sola vez, seis reglas para escribir uno que no dependa del formato, y los cuatro pasos antes de commitear un formateo masivo (neutralidad compilada, la vista de los guards, los validadores que NO están en el gate, un commit puro). CORREGIDO (afirmaciones vivas y falsas) - `README.md` de la raíz: era una plantilla vacía que mandaba `npm install vicen` con el repositorio `private: true` y sin paquete. Ahora es una puerta. - `AGENTS.md`: su pre-flight INVIOLABLE mandaba leer dos guías del árbol congelado (el canónico está migrado), su ejemplo de test apuntaba a `src/lib/ling/`, borrado en el refactor, y describía cuatro librerías que no existen. Además decía que los comentarios en castellano valen, contra CLAUDE.md. - `docs/theming/guide.md`: los cuatro sitios que llamaban `eidos.listThemes()` DENTRO de `hooks.server.ts`, donde no hay instancia; ahora `THEME_IDS` derivado con `listEidosThemes(config)` del módulo que la raíz también importa. - `docs/architecture/active-uix.md`: la regla 6 decía que la raíz no proyecta preferencias; hoy standalone proyecta por defecto (`projectPrefs`, `@default true`) y attach es opt-in. Su ejemplo de arranque montaba una SEGUNDA proyección a mano. - `announce`: el opt-in queda calificado (motor desnudo) frente al cableado por defecto de las raíces, en `book-deviations.md`, `channels.md:58` y el docblock del canal. - `docs/getting-started.md` y `docs/architecture/morfo.md`: la política de `check:gate` también cubre `scripts/`. - `docs/canon/direction-contract.md`: los dueños literales de la marca (`boot`, `projection-<n>`) pasan a la prosa, citables por un guard. - `src/uix/eidos/components/README.md`: regla 8 — un bindable se reenvía con `bind:`, nunca por el spread del resto (el proxy de rest props no lleva `set`, así que el tipo promete lo que no ata). Con el `ref` en la superficie del `Button` y el gap RESUELTO en `cookie-consent`. `docs/canon/vocabularies.md` y `src/libs/emoji/data.ts` aparecen por fin como artefactos generados, con su comando. Ledger: L-133 · L-142 · L-143 · L-154 a ARREGLADO; L-152 conserva los 532 ficheros pero ya con doctrina escrita; nuevas L-161…L-165 (la última, DIFERIDA: nadie obliga aún a que un guard de texto falle con el corpus vacío). Verificación: `npm run gate` exit 0 en 542 s — lint limpio · check:gate OK (89 de web/ en el ledger) · docs:check 0/0 en 822 docs · suite 466/466 ficheros, 5445/5445 tests · apps:check verde. `component:audit` exit 0 con PASS 161 / NEEDS-WORK 5, las cifras de antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
themeIds: THEME_IDS,
feat(active-uix)!: el boot lo compila el build del SITIO con su propio esquema, y lo que UIX escribe en <html dir> lleva marca de propiedad (fila 2 del boot, F1 del cierre) F1 del plan de cierre del framework. Cierra el último trabajo del eje boot. EL DEFECTO. El boot por defecto compilaba un catálogo de preferencias de UN solo idioma (createDefaultUixPrefsSchema), y la raíz sembraba su entorno leyendo el <html dir> que el boot acababa de escribir: un usuario árabe de una app multiidioma no veía un parpadeo, se quedaba en LTR TODA la sesión, porque el runtime tomaba la salida del boot como si la hubiera declarado la página. EL COMPILADOR POR SITIO. Una sola fusión, composeUixPrefsSchema, la consumen createActiveUix y el boot: no puede derivar. El generador acepta --schema y --out (el especificador boot/boot-schema.ts apunta al esquema del sitio o a boot/default-schema.ts); los guards (sin runas, techo de tamaño, ASCII, sin </script, valores string por atributo) son errores del COMPILADOR con mensajes para el consumidor, porque ahora el artefacto lo produce el sitio. Un esquema que importa el barrel $prefs se rechaza nombrándolo. scripts/uix-boot-check.ts es el guard de rancidez: plugin de Vite que tumba el build con un artefacto viejo (probado con vite build real) y función para CI. Delta cero por esquema: por defecto, multiidioma con árabe, ejes redefinidos y esquema parcial. LA MARCA DE PROPIEDAD, diseño firmado por el autor. El canon de dirección dice que <html dir> es una proyección y nunca una fuente, con una sola excepción: el dir del AUTOR en la plantilla o el servidor. El boot rompió la premisa de esa excepción. Todo lo que UIX escribe en <html dir> lleva ahora PREFS_DIR_PROJECTED_ATTR (el boot con valor «boot», cada proyección del runtime con un token de instancia); la semilla solo adopta un dir SIN marca. Un dir escrito por script no es una fuente: en ejecución la dirección se afirma con prefs.setIntent('direction') o options.prefs.environment, que ganan a la semilla. Se descartaron, midiendo, la marca con valor (cierra el script y congela la dirección al navegar entre layouts) y la inferencia por valor. dispose solo retira lo que todavía es SUYO: SvelteKit crea la raíz del layout nuevo ANTES de destruir la vieja (medido), y un retiro a ciegas dejaba el <html> de la raíz nueva sin dir, lang ni data-motion/sound/haptic (medido hoy: los nueve atributos a null). Mismo principio que el unstamp de sema, que comprueba data-event-id. esbuild se declara como devDependency EXACTA 0.27.4: los bytes del boot y su hash dependen del minificador, y una subida dentro de un rango rompería la sincronía. Boot 15 198 B, sha256-hsqdGYcrRu3oEc0Q3G/A67ApQT3q9c/vT9zMDgxROg8= (el hash se mueve: firmado por el autor). VERIFICACIÓN. Constructor Opus en cuatro rondas y adversarial Opus en dos pasadas independientes, con sus reproducciones repetidas tras cada cierre: en Chromium, entrar en árabe y pasar a inglés y a español sigue al idioma, y al revés también; el dir de plantilla gana antes y después de hidratar y en una raíz recreada; una raíz recreada sobre una viva sigue al idioma con y sin boot; tras create b → destroy a, <html> conserva lo que proyectó b. Diez defectos declarados por el adversarial, cerrados (D1–D10): marca de propiedad, esquema parcial que estampaba "undefined", vigilante de dev mudo, plugin sin test, receta de CI que no cargaba en jsdom, cifras y prosa, y un vite build real que se cae cuando el boot no compila. Mutaciones en rojo, restauradas byte a byte: la proyección no marca · dispose sin guard de dueño (también repetida por el coordinador: 3 rojos) · la semilla compara valor en vez de presencia · marcar el dir del autor. Suite entera 463 ficheros / 5 437 tests, exit 0 · check 0 errores en src/ y en scripts/ (89 en web/, ledger intacto) · docs:check 0/0 · generate:boot dos veces byte-idéntico. LO QUE NO CIERRA (al ledger de cierre): (1) PREEXISTENTE — en la misma navegación entre layouts, el ActiveEidos.dispose de la raíz vieja sigue retirando data-theme/mode/density/scaling de la raíz nueva; exige cambiar el dispose de eidos, otra capa. (2) La propiedad de los atributos proyectados cuelga de la marca de dir: si la plantilla declara la dirección, dispose deja lang y data-motion puestos cuando la raíz se desmonta sin sustituta (benigno). (3) Sin boot, un script que escribe dir antes de la primera raíz es indistinguible de la plantilla y se adopta. (4) El vigilante de dev no reacciona a ficheros nuevos ni a inputs fuera del root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
artifact: { script: UIX_BOOT_SCRIPT, hash: UIX_BOOT_CSP_HASH }
});
```
### The staleness guard — not optional
A compiled artifact goes stale the moment you edit your schema, edit a
dimension, or upgrade UIX, and **every symptom of a stale boot is silent**:
the tag is wrapped in a mute `try`/`catch` by design, the values it stamps
are merely old rather than invalid, and nothing in the page says so. The only
guard worth having is one that stops the build.
```js
// vite.config.js
import { uixBootCheck } from '<uix>/scripts/uix-boot-check.ts';
export default {
plugins: [uixBootCheck({ schema: 'src/prefs-schema.ts', out: 'src/generated/boot.js' })]
};
```
It recompiles in `buildStart` with your own configuration and fails the build
on any difference. In dev it WARNS instead — a dev server that refuses to
start over a stale cosmetic artifact helps nobody — and it registers every
module the compile READ, your schema and the dimensions included, so editing
one re-runs the check instead of waiting for a deploy. A schema module you are
halfway through typing is a warning too, and the next save of any module that
compile read runs the check again: the guard never takes the dev server
down with it.
With no Vite, the same check is a promise and a CI test is the whole wiring. The
first line is part of the recipe: the check runs esbuild, and esbuild refuses to
load in a `jsdom` environment (`Invariant violation: "new
TextEncoder().encode("") instanceof Uint8Array" is incorrectly false`), so in a
project whose tests default to `jsdom` the file fails before its first test.
```ts
// @vitest-environment node
import { expect, it } from 'vitest';
import { checkUixBootArtifact } from '<uix>/scripts/uix-boot-check.ts';
it('the compiled boot is not stale', async () => {
const result = await checkUixBootArtifact({
schema: 'src/prefs-schema.ts',
out: 'src/generated/boot.js'
});
expect(result).toMatchObject({ ok: true });
});
```
> **An app consumes UIX as source, from a workspace in this repository — not as
> a package.** Every `<uix>` above is the path from the app to the repository
> root (`../..` for `apps/<name>`), the compiler runs its `.ts` entry with `tsx`,
> and the aliases your schema module writes (`$prefs/standard`, `$libs/prefs`)
> resolve through `uix.aliases.js`, the one table every tool imports. There is
> no published `bin` and no npm package: the contract is
> [`docs/consuming.md`](../consuming.md).
feat(active-uix)!: el boot lo compila el build del SITIO con su propio esquema, y lo que UIX escribe en <html dir> lleva marca de propiedad (fila 2 del boot, F1 del cierre) F1 del plan de cierre del framework. Cierra el último trabajo del eje boot. EL DEFECTO. El boot por defecto compilaba un catálogo de preferencias de UN solo idioma (createDefaultUixPrefsSchema), y la raíz sembraba su entorno leyendo el <html dir> que el boot acababa de escribir: un usuario árabe de una app multiidioma no veía un parpadeo, se quedaba en LTR TODA la sesión, porque el runtime tomaba la salida del boot como si la hubiera declarado la página. EL COMPILADOR POR SITIO. Una sola fusión, composeUixPrefsSchema, la consumen createActiveUix y el boot: no puede derivar. El generador acepta --schema y --out (el especificador boot/boot-schema.ts apunta al esquema del sitio o a boot/default-schema.ts); los guards (sin runas, techo de tamaño, ASCII, sin </script, valores string por atributo) son errores del COMPILADOR con mensajes para el consumidor, porque ahora el artefacto lo produce el sitio. Un esquema que importa el barrel $prefs se rechaza nombrándolo. scripts/uix-boot-check.ts es el guard de rancidez: plugin de Vite que tumba el build con un artefacto viejo (probado con vite build real) y función para CI. Delta cero por esquema: por defecto, multiidioma con árabe, ejes redefinidos y esquema parcial. LA MARCA DE PROPIEDAD, diseño firmado por el autor. El canon de dirección dice que <html dir> es una proyección y nunca una fuente, con una sola excepción: el dir del AUTOR en la plantilla o el servidor. El boot rompió la premisa de esa excepción. Todo lo que UIX escribe en <html dir> lleva ahora PREFS_DIR_PROJECTED_ATTR (el boot con valor «boot», cada proyección del runtime con un token de instancia); la semilla solo adopta un dir SIN marca. Un dir escrito por script no es una fuente: en ejecución la dirección se afirma con prefs.setIntent('direction') o options.prefs.environment, que ganan a la semilla. Se descartaron, midiendo, la marca con valor (cierra el script y congela la dirección al navegar entre layouts) y la inferencia por valor. dispose solo retira lo que todavía es SUYO: SvelteKit crea la raíz del layout nuevo ANTES de destruir la vieja (medido), y un retiro a ciegas dejaba el <html> de la raíz nueva sin dir, lang ni data-motion/sound/haptic (medido hoy: los nueve atributos a null). Mismo principio que el unstamp de sema, que comprueba data-event-id. esbuild se declara como devDependency EXACTA 0.27.4: los bytes del boot y su hash dependen del minificador, y una subida dentro de un rango rompería la sincronía. Boot 15 198 B, sha256-hsqdGYcrRu3oEc0Q3G/A67ApQT3q9c/vT9zMDgxROg8= (el hash se mueve: firmado por el autor). VERIFICACIÓN. Constructor Opus en cuatro rondas y adversarial Opus en dos pasadas independientes, con sus reproducciones repetidas tras cada cierre: en Chromium, entrar en árabe y pasar a inglés y a español sigue al idioma, y al revés también; el dir de plantilla gana antes y después de hidratar y en una raíz recreada; una raíz recreada sobre una viva sigue al idioma con y sin boot; tras create b → destroy a, <html> conserva lo que proyectó b. Diez defectos declarados por el adversarial, cerrados (D1–D10): marca de propiedad, esquema parcial que estampaba "undefined", vigilante de dev mudo, plugin sin test, receta de CI que no cargaba en jsdom, cifras y prosa, y un vite build real que se cae cuando el boot no compila. Mutaciones en rojo, restauradas byte a byte: la proyección no marca · dispose sin guard de dueño (también repetida por el coordinador: 3 rojos) · la semilla compara valor en vez de presencia · marcar el dir del autor. Suite entera 463 ficheros / 5 437 tests, exit 0 · check 0 errores en src/ y en scripts/ (89 en web/, ledger intacto) · docs:check 0/0 · generate:boot dos veces byte-idéntico. LO QUE NO CIERRA (al ledger de cierre): (1) PREEXISTENTE — en la misma navegación entre layouts, el ActiveEidos.dispose de la raíz vieja sigue retirando data-theme/mode/density/scaling de la raíz nueva; exige cambiar el dispose de eidos, otra capa. (2) La propiedad de los atributos proyectados cuelga de la marca de dir: si la plantilla declara la dirección, dispose deja lang y data-motion puestos cuando la raíz se desmonta sin sustituta (benigno). (3) Sin boot, un script que escribe dir antes de la primera raíz es indistinguible de la plantilla y se adopta. (4) El vigilante de dev no reacciona a ficheros nuevos ni a inputs fuera del root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
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
### 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.
docs: el repositorio, el formato y los guards de texto entran en el corpus; cuatro afirmaciones caducas mueren (documentación del cierre) El plan de cierre cambió cosas de un nivel que el corpus no cubría: la forma del repositorio, la política de formato, el inventario de lo generado y la clase de guard que lee TEXTO fuente. Un inventario previo de todo `docs/**` (más los README de raíz, `src/**` y `apps/**`) midió qué había: los temas de capa estaban cubiertos, y este nivel no. NUEVO - `docs/repository.md` (E0): las zonas y quién escribe en cada una; la LEY de `web/routes/` congelado y sus dos consecuencias (los validadores de navegador siguen manuales; el formato no llega ahí); un solo install y un solo workspace (el argumento de la copia única de Svelte); un solo mapa de importación con el orden como contrato; qué debe cero y qué debe un ledger que solo mengua. Enlazada desde el mapa, el README de la raíz, getting-started y AGENTS.md. - `docs/testing-and-tooling.md` §Format policy: `.prettierignore` enumerado y justificado (cinco clases), el commit único de formato, el `git config blame.ignoreRevsFile` que hay que ejecutar a mano y que Gitea no lo lee. - `docs/testing-and-tooling.md` §Guards that read source text: la doctrina que faltaba. Un guard de texto está acoplado al formateador; el positivo se pone ROJO y te enteras, el NEGATIVO pasa en VERDE sin inspeccionar nada. Los cinco síntomas medidos en el formateo de una sola vez, seis reglas para escribir uno que no dependa del formato, y los cuatro pasos antes de commitear un formateo masivo (neutralidad compilada, la vista de los guards, los validadores que NO están en el gate, un commit puro). CORREGIDO (afirmaciones vivas y falsas) - `README.md` de la raíz: era una plantilla vacía que mandaba `npm install vicen` con el repositorio `private: true` y sin paquete. Ahora es una puerta. - `AGENTS.md`: su pre-flight INVIOLABLE mandaba leer dos guías del árbol congelado (el canónico está migrado), su ejemplo de test apuntaba a `src/lib/ling/`, borrado en el refactor, y describía cuatro librerías que no existen. Además decía que los comentarios en castellano valen, contra CLAUDE.md. - `docs/theming/guide.md`: los cuatro sitios que llamaban `eidos.listThemes()` DENTRO de `hooks.server.ts`, donde no hay instancia; ahora `THEME_IDS` derivado con `listEidosThemes(config)` del módulo que la raíz también importa. - `docs/architecture/active-uix.md`: la regla 6 decía que la raíz no proyecta preferencias; hoy standalone proyecta por defecto (`projectPrefs`, `@default true`) y attach es opt-in. Su ejemplo de arranque montaba una SEGUNDA proyección a mano. - `announce`: el opt-in queda calificado (motor desnudo) frente al cableado por defecto de las raíces, en `book-deviations.md`, `channels.md:58` y el docblock del canal. - `docs/getting-started.md` y `docs/architecture/morfo.md`: la política de `check:gate` también cubre `scripts/`. - `docs/canon/direction-contract.md`: los dueños literales de la marca (`boot`, `projection-<n>`) pasan a la prosa, citables por un guard. - `src/uix/eidos/components/README.md`: regla 8 — un bindable se reenvía con `bind:`, nunca por el spread del resto (el proxy de rest props no lleva `set`, así que el tipo promete lo que no ata). Con el `ref` en la superficie del `Button` y el gap RESUELTO en `cookie-consent`. `docs/canon/vocabularies.md` y `src/libs/emoji/data.ts` aparecen por fin como artefactos generados, con su comando. Ledger: L-133 · L-142 · L-143 · L-154 a ARREGLADO; L-152 conserva los 532 ficheros pero ya con doctrina escrita; nuevas L-161…L-165 (la última, DIFERIDA: nadie obliga aún a que un guard de texto falle con el corpus vacío). Verificación: `npm run gate` exit 0 en 542 s — lint limpio · check:gate OK (89 de web/ en el ledger) · docs:check 0/0 en 822 docs · suite 466/466 ficheros, 5445/5445 tests · apps:check verde. `component:audit` exit 0 con PASS 161 / NEEDS-WORK 5, las cifras de antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
- `themeIds` — the ids eidos registers, derived on the server with
`listEidosThemes(config)` from the config module the root also imports (the
hook has no `ActiveEidos` to ask). It feeds the "is this family already a
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
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',
docs: el repositorio, el formato y los guards de texto entran en el corpus; cuatro afirmaciones caducas mueren (documentación del cierre) El plan de cierre cambió cosas de un nivel que el corpus no cubría: la forma del repositorio, la política de formato, el inventario de lo generado y la clase de guard que lee TEXTO fuente. Un inventario previo de todo `docs/**` (más los README de raíz, `src/**` y `apps/**`) midió qué había: los temas de capa estaban cubiertos, y este nivel no. NUEVO - `docs/repository.md` (E0): las zonas y quién escribe en cada una; la LEY de `web/routes/` congelado y sus dos consecuencias (los validadores de navegador siguen manuales; el formato no llega ahí); un solo install y un solo workspace (el argumento de la copia única de Svelte); un solo mapa de importación con el orden como contrato; qué debe cero y qué debe un ledger que solo mengua. Enlazada desde el mapa, el README de la raíz, getting-started y AGENTS.md. - `docs/testing-and-tooling.md` §Format policy: `.prettierignore` enumerado y justificado (cinco clases), el commit único de formato, el `git config blame.ignoreRevsFile` que hay que ejecutar a mano y que Gitea no lo lee. - `docs/testing-and-tooling.md` §Guards that read source text: la doctrina que faltaba. Un guard de texto está acoplado al formateador; el positivo se pone ROJO y te enteras, el NEGATIVO pasa en VERDE sin inspeccionar nada. Los cinco síntomas medidos en el formateo de una sola vez, seis reglas para escribir uno que no dependa del formato, y los cuatro pasos antes de commitear un formateo masivo (neutralidad compilada, la vista de los guards, los validadores que NO están en el gate, un commit puro). CORREGIDO (afirmaciones vivas y falsas) - `README.md` de la raíz: era una plantilla vacía que mandaba `npm install vicen` con el repositorio `private: true` y sin paquete. Ahora es una puerta. - `AGENTS.md`: su pre-flight INVIOLABLE mandaba leer dos guías del árbol congelado (el canónico está migrado), su ejemplo de test apuntaba a `src/lib/ling/`, borrado en el refactor, y describía cuatro librerías que no existen. Además decía que los comentarios en castellano valen, contra CLAUDE.md. - `docs/theming/guide.md`: los cuatro sitios que llamaban `eidos.listThemes()` DENTRO de `hooks.server.ts`, donde no hay instancia; ahora `THEME_IDS` derivado con `listEidosThemes(config)` del módulo que la raíz también importa. - `docs/architecture/active-uix.md`: la regla 6 decía que la raíz no proyecta preferencias; hoy standalone proyecta por defecto (`projectPrefs`, `@default true`) y attach es opt-in. Su ejemplo de arranque montaba una SEGUNDA proyección a mano. - `announce`: el opt-in queda calificado (motor desnudo) frente al cableado por defecto de las raíces, en `book-deviations.md`, `channels.md:58` y el docblock del canal. - `docs/getting-started.md` y `docs/architecture/morfo.md`: la política de `check:gate` también cubre `scripts/`. - `docs/canon/direction-contract.md`: los dueños literales de la marca (`boot`, `projection-<n>`) pasan a la prosa, citables por un guard. - `src/uix/eidos/components/README.md`: regla 8 — un bindable se reenvía con `bind:`, nunca por el spread del resto (el proxy de rest props no lleva `set`, así que el tipo promete lo que no ata). Con el `ref` en la superficie del `Button` y el gap RESUELTO en `cookie-consent`. `docs/canon/vocabularies.md` y `src/libs/emoji/data.ts` aparecen por fin como artefactos generados, con su comando. Ledger: L-133 · L-142 · L-143 · L-154 a ARREGLADO; L-152 conserva los 532 ficheros pero ya con doctrina escrita; nuevas L-161…L-165 (la última, DIFERIDA: nadie obliga aún a que un guard de texto falle con el corpus vacío). Verificación: `npm run gate` exit 0 en 542 s — lint limpio · check:gate OK (89 de web/ en el ledger) · docs:check 0/0 en 822 docs · suite 466/466 ficheros, 5445/5445 tests · apps:check verde. `component:audit` exit 0 con PASS 161 / NEEDS-WORK 5, las cifras de antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
themeIds: THEME_IDS,
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: { 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(active-uix)!: el boot lo compila el build del SITIO con su propio esquema, y lo que UIX escribe en <html dir> lleva marca de propiedad (fila 2 del boot, F1 del cierre) F1 del plan de cierre del framework. Cierra el último trabajo del eje boot. EL DEFECTO. El boot por defecto compilaba un catálogo de preferencias de UN solo idioma (createDefaultUixPrefsSchema), y la raíz sembraba su entorno leyendo el <html dir> que el boot acababa de escribir: un usuario árabe de una app multiidioma no veía un parpadeo, se quedaba en LTR TODA la sesión, porque el runtime tomaba la salida del boot como si la hubiera declarado la página. EL COMPILADOR POR SITIO. Una sola fusión, composeUixPrefsSchema, la consumen createActiveUix y el boot: no puede derivar. El generador acepta --schema y --out (el especificador boot/boot-schema.ts apunta al esquema del sitio o a boot/default-schema.ts); los guards (sin runas, techo de tamaño, ASCII, sin </script, valores string por atributo) son errores del COMPILADOR con mensajes para el consumidor, porque ahora el artefacto lo produce el sitio. Un esquema que importa el barrel $prefs se rechaza nombrándolo. scripts/uix-boot-check.ts es el guard de rancidez: plugin de Vite que tumba el build con un artefacto viejo (probado con vite build real) y función para CI. Delta cero por esquema: por defecto, multiidioma con árabe, ejes redefinidos y esquema parcial. LA MARCA DE PROPIEDAD, diseño firmado por el autor. El canon de dirección dice que <html dir> es una proyección y nunca una fuente, con una sola excepción: el dir del AUTOR en la plantilla o el servidor. El boot rompió la premisa de esa excepción. Todo lo que UIX escribe en <html dir> lleva ahora PREFS_DIR_PROJECTED_ATTR (el boot con valor «boot», cada proyección del runtime con un token de instancia); la semilla solo adopta un dir SIN marca. Un dir escrito por script no es una fuente: en ejecución la dirección se afirma con prefs.setIntent('direction') o options.prefs.environment, que ganan a la semilla. Se descartaron, midiendo, la marca con valor (cierra el script y congela la dirección al navegar entre layouts) y la inferencia por valor. dispose solo retira lo que todavía es SUYO: SvelteKit crea la raíz del layout nuevo ANTES de destruir la vieja (medido), y un retiro a ciegas dejaba el <html> de la raíz nueva sin dir, lang ni data-motion/sound/haptic (medido hoy: los nueve atributos a null). Mismo principio que el unstamp de sema, que comprueba data-event-id. esbuild se declara como devDependency EXACTA 0.27.4: los bytes del boot y su hash dependen del minificador, y una subida dentro de un rango rompería la sincronía. Boot 15 198 B, sha256-hsqdGYcrRu3oEc0Q3G/A67ApQT3q9c/vT9zMDgxROg8= (el hash se mueve: firmado por el autor). VERIFICACIÓN. Constructor Opus en cuatro rondas y adversarial Opus en dos pasadas independientes, con sus reproducciones repetidas tras cada cierre: en Chromium, entrar en árabe y pasar a inglés y a español sigue al idioma, y al revés también; el dir de plantilla gana antes y después de hidratar y en una raíz recreada; una raíz recreada sobre una viva sigue al idioma con y sin boot; tras create b → destroy a, <html> conserva lo que proyectó b. Diez defectos declarados por el adversarial, cerrados (D1–D10): marca de propiedad, esquema parcial que estampaba "undefined", vigilante de dev mudo, plugin sin test, receta de CI que no cargaba en jsdom, cifras y prosa, y un vite build real que se cae cuando el boot no compila. Mutaciones en rojo, restauradas byte a byte: la proyección no marca · dispose sin guard de dueño (también repetida por el coordinador: 3 rojos) · la semilla compara valor en vez de presencia · marcar el dir del autor. Suite entera 463 ficheros / 5 437 tests, exit 0 · check 0 errores en src/ y en scripts/ (89 en web/, ledger intacto) · docs:check 0/0 · generate:boot dos veces byte-idéntico. LO QUE NO CIERRA (al ledger de cierre): (1) PREEXISTENTE — en la misma navegación entre layouts, el ActiveEidos.dispose de la raíz vieja sigue retirando data-theme/mode/density/scaling de la raíz nueva; exige cambiar el dispose de eidos, otra capa. (2) La propiedad de los atributos proyectados cuelga de la marca de dir: si la plantilla declara la dirección, dispose deja lang y data-motion puestos cuando la raíz se desmonta sin sustituta (benigno). (3) Sin boot, un script que escribe dir antes de la primera raíz es indistinguible de la plantilla y se adopta. (4) El vigilante de dev no reacciona a ficheros nuevos ni a inputs fuera del root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
### The declared limits
**The theme resolver.** 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 does not know it, so `data-theme` differs until hydration.
The line is narrower than "a custom resolver". `ActiveEidosThemeResolver` is
`(context, active) => string` and `ActiveEidos` always calls it with the live
instance (`#themeResolver(themeContext, this)`). A resolver that reads only
`context` is a pure function of plain data, so it COULD travel into the boot
the way the schema now does; one that touches `active` cannot, because at
that moment there is no instance to touch — no `ActiveEidos`, no prefs
engine, nothing but a `<script>` in `<head>`. The seam that would carry the
first kind is not built: only the schema has one today.
**Attach mode.** `attachActiveUix(app)` is NOT covered. The app owns its prefs
engine there, so the framework merges nothing for it and the boot has no way
to know which schema that engine was built with. An app on attach composes
`uixVisualPrefsDimensions()` into its own schema and owns its pre-paint stamp;
the row that closes attach properly is open.

Powered by TurnKey Linux.