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/consuming.md

247 lines
11 KiB

---
title: Consuming the framework from an app
type: guide
audience: human + agent
authority: E4 guide — the contract an app in this repository follows to consume UIX as source
status: current
---
# Consuming the framework from an app
UIX is consumed **as source, from a workspace in this repository**. There is no
npm package, no `exports` map and no `bin`: an app lives next to the framework,
imports it through the same alias table every tool in the repository reads, and
compiles it with its own build. This page is that contract, section by section.
Where a subject already has a reference page, this page links it instead of
repeating it.
## 1. Where the app lives
An app is a workspace under `apps/<name>/` with its own `package.json`, sharing
the repository's single `node_modules` and lockfile. One install, one copy of
`svelte`, `vite` and `@sveltejs/kit` for the framework and every app — two
copies of Svelte would mean two runtimes of runes, and the framework's
`.svelte.ts` modules would not share state with the app's components.
Vite finds the workspace root from the root `package.json`'s `workspaces` field,
which is what lets the app's dev server read files under the repository's
`src/`. An app taken outside the workspace must add the repository root to its
own `server.fs.allow`.
feat(apps): apps/base — la primera app que consume UIX como fuente desde un workspace, y su check entra en el gate (F3 del cierre) Hasta hoy ninguna app consumía el framework fuera de su propio árbol de demos, y por eso la receta del boot pudo estar rota en producción sin que nadie lo supiera. apps/base es un esqueleto SvelteKit con adapter-static estricto y prerender, que sigue docs/consuming.md al pie de la letra: alias por resolveUixAliases, runas por config, raíz standalone (createActiveUix + setActiveUix + Soma + ActiveEidos con applyDom), boot compilado con SU esquema (es/en/ar), CSP por hash con el artefacto propio, fuentes y sonidos copiados al static/ de la app (assets:sync, con --check). La página es un AppShell con un Toggle de modo, un Select de idioma y un Dialog. Raíz: workspaces ["apps/*"] (una sola copia de svelte 5.55.0, vite 7.3.1, kit 2.55.0 y esbuild 0.27.4; lock +23 líneas); apps:check = check + build + smoke de la app, dentro del gate justo antes de la suite; .gitignore y .prettierignore para lo generado y lo copiado. El smoke sirve build/ por HTTP y pasa tres visitantes (es-ES, ar-EG, es-ES con modo oscuro y movimiento reducido), cada uno con los módulos abortados (sellos del boot) y con carga completa (0 pageerror, 0 console.error, 0 violaciones CSP, delta 0 entre boot e hidratación). Comprueba además que el tag del boot va DESPUÉS del <meta> CSP: encima, corre sin política y Kit no lo avisa. Constructor, adversarial y dos rondas de cierre (Opus). El adversarial escribió una segunda app solo con el contrato y la hizo arrancar en Chromium, Firefox y WebKit; midió la CSP en siete variantes y tres motores, y mutó la app para ver si el smoke muerde. Defectos cerrados: - D1: el smoke prometía contar traducciones ausentes y en una build de producción no hay señal (engine-langs las emite solo en DEV). Ya no lo promete; README y consuming §8 dicen dónde se ve cada cosa. - D2: el smoke nunca abría modo oscuro. Tercer visitante con el valor afirmado; un pin 'light' olvidado en la raíz ahora da exit 1. - D3: sin reset de documento el Dialog medía 409 px en un móvil de 375. La app lleva su reset de tres reglas (doctrina vigente: el reset es de la app); que la fundación lo traiga es fila del ledger. - D5: los textos del esqueleto seguían en castellano bajo lang=ar. Salen de src/lib/strings.ts por langs.ts(record), tipados. - D6: esa garantía de tipo la sostenía una unión de idiomas escrita a mano. Ahora hay UNA tupla (APP_LANGUAGES en prefs-schema.ts) que leen el esquema, los textos y el Select. Visto fallar: con 'fr' en la tupla, check exit 1 con 8 errores que nombran fr; restaurado byte a byte. - C1-C3: el contrato gana el tsconfig mínimo medido (rewriteRelativeImportExtensions, sin él 578 errores), las rutas de import de Soma/ActiveEidos/createThemeBaseEidosConfig y adapter-static + prerender. Cero cambios en src/**. Huecos del framework que la app destapa, al ledger: el Dialog modal no devuelve el foco al trigger (queda en BODY en los 3 motores: FocusScope enfoca mientras HideOthers aún tiene el inert), Select.Value muestra el value crudo hasta abrir el popup, los catálogos de componentes no tienen 'ar', la foundation se carga dos veces (482 KB enlazados + 426 KB inyectados por applyDom, 5 153 tokens duplicados), guide.md llama eidos.listThemes() dentro del hook, la regla 6 de active-uix.md está caducada, y uix.langs no lleva el tipo del catálogo de la app. Verificación: apps:check exit 0 (1391 ficheros 0 errores · build · smoke `boot 27/27 · unguarded 0 · pageerror 0 · console.error 0 · csp 0 · delta 0 · flips 0`) · boot de la app 15 365 B, byte-idéntico en dos compilaciones, su hash en el <meta> de build/index.html · docs:check 0/0 en 820 docs. Sobre el lote antes de D6 (D6 no toca src/, scripts/ ni la suite): gate entero exit 0 en 357 s, check:gate 89 dentro del ledger, suite 464/464 ficheros · 5442/5442 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
The contract is written for, and verified on, a fully prerendered static site:
`@sveltejs/adapter-static` with no `fallback`, and `export const prerender = true` in
`src/routes/+layout.ts`. The pre-hydration boot (§6) and the CSP `<meta>` are then written into
every HTML file at build time, and a page that fails to render fails the build.
## 2. Aliases
The framework imports itself through `$…` and `@/` specifiers. The table lives
in ONE module at the repository root, [`uix.aliases.js`](../uix.aliases.js), and
the app reads it exactly as the framework's own configs do:
```js
// apps/<name>/svelte.config.js
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveUixAliases } from '../../uix.aliases.js';
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
export default {
kit: {
alias: resolveUixAliases(REPO_ROOT)
}
};
```
SvelteKit writes the aliases into the app's generated tsconfig and hands them to
Vite, so the app does not repeat them in `resolve.alias`. Order is load-bearing
(a longer specifier before any key that is its prefix); never re-sort or copy the
table — `src/uix/aliases.test.ts` guards it where it is written.
`@` is the framework's `src/`, not the app's. The app keeps its own `$lib`.
feat(apps): apps/base — la primera app que consume UIX como fuente desde un workspace, y su check entra en el gate (F3 del cierre) Hasta hoy ninguna app consumía el framework fuera de su propio árbol de demos, y por eso la receta del boot pudo estar rota en producción sin que nadie lo supiera. apps/base es un esqueleto SvelteKit con adapter-static estricto y prerender, que sigue docs/consuming.md al pie de la letra: alias por resolveUixAliases, runas por config, raíz standalone (createActiveUix + setActiveUix + Soma + ActiveEidos con applyDom), boot compilado con SU esquema (es/en/ar), CSP por hash con el artefacto propio, fuentes y sonidos copiados al static/ de la app (assets:sync, con --check). La página es un AppShell con un Toggle de modo, un Select de idioma y un Dialog. Raíz: workspaces ["apps/*"] (una sola copia de svelte 5.55.0, vite 7.3.1, kit 2.55.0 y esbuild 0.27.4; lock +23 líneas); apps:check = check + build + smoke de la app, dentro del gate justo antes de la suite; .gitignore y .prettierignore para lo generado y lo copiado. El smoke sirve build/ por HTTP y pasa tres visitantes (es-ES, ar-EG, es-ES con modo oscuro y movimiento reducido), cada uno con los módulos abortados (sellos del boot) y con carga completa (0 pageerror, 0 console.error, 0 violaciones CSP, delta 0 entre boot e hidratación). Comprueba además que el tag del boot va DESPUÉS del <meta> CSP: encima, corre sin política y Kit no lo avisa. Constructor, adversarial y dos rondas de cierre (Opus). El adversarial escribió una segunda app solo con el contrato y la hizo arrancar en Chromium, Firefox y WebKit; midió la CSP en siete variantes y tres motores, y mutó la app para ver si el smoke muerde. Defectos cerrados: - D1: el smoke prometía contar traducciones ausentes y en una build de producción no hay señal (engine-langs las emite solo en DEV). Ya no lo promete; README y consuming §8 dicen dónde se ve cada cosa. - D2: el smoke nunca abría modo oscuro. Tercer visitante con el valor afirmado; un pin 'light' olvidado en la raíz ahora da exit 1. - D3: sin reset de documento el Dialog medía 409 px en un móvil de 375. La app lleva su reset de tres reglas (doctrina vigente: el reset es de la app); que la fundación lo traiga es fila del ledger. - D5: los textos del esqueleto seguían en castellano bajo lang=ar. Salen de src/lib/strings.ts por langs.ts(record), tipados. - D6: esa garantía de tipo la sostenía una unión de idiomas escrita a mano. Ahora hay UNA tupla (APP_LANGUAGES en prefs-schema.ts) que leen el esquema, los textos y el Select. Visto fallar: con 'fr' en la tupla, check exit 1 con 8 errores que nombran fr; restaurado byte a byte. - C1-C3: el contrato gana el tsconfig mínimo medido (rewriteRelativeImportExtensions, sin él 578 errores), las rutas de import de Soma/ActiveEidos/createThemeBaseEidosConfig y adapter-static + prerender. Cero cambios en src/**. Huecos del framework que la app destapa, al ledger: el Dialog modal no devuelve el foco al trigger (queda en BODY en los 3 motores: FocusScope enfoca mientras HideOthers aún tiene el inert), Select.Value muestra el value crudo hasta abrir el popup, los catálogos de componentes no tienen 'ar', la foundation se carga dos veces (482 KB enlazados + 426 KB inyectados por applyDom, 5 153 tokens duplicados), guide.md llama eidos.listThemes() dentro del hook, la regla 6 de active-uix.md está caducada, y uix.langs no lleva el tipo del catálogo de la app. Verificación: apps:check exit 0 (1391 ficheros 0 errores · build · smoke `boot 27/27 · unguarded 0 · pageerror 0 · console.error 0 · csp 0 · delta 0 · flips 0`) · boot de la app 15 365 B, byte-idéntico en dos compilaciones, su hash en el <meta> de build/index.html · docs:check 0/0 en 820 docs. Sobre el lote antes de D6 (D6 no toca src/, scripts/ ni la suite): gate entero exit 0 en 357 s, check:gate 89 dentro del ledger, suite 464/464 ficheros · 5442/5442 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
The app's `tsconfig.json` extends the one Kit generates, and carries one option the framework's
sources need:
```json
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"strict": true,
"moduleResolution": "bundler"
}
}
```
The framework imports some of its modules with their `.ts` extension (`scripts/generate-boot.ts`,
`src/libs/errs/`), and `svelte-check` follows the app's `vite.config.ts` into them. Without
`rewriteRelativeImportExtensions`, the check of `apps/base` reports 578 errors, every one
`An import path can only end with a '.ts' extension`; with it, none.
## 3. Runes
The framework is written in Svelte 5 runes mode, and the repository enforces it
by config rather than per file. The app's `svelte.config.js` carries the same
three lines, or framework components that happen to use no rune compile in
legacy mode:
```js
vitePlugin: {
dynamicCompileOptions: ({ filename }) =>
filename.includes('node_modules') ? undefined : { runes: true };
}
```
## 4. The composition root
An app boots UIX in **standalone** mode: `createActiveUix` owns the services,
`setActiveUix` publishes it to the tree, `Soma.create()` opens the headless
layer and `ActiveEidos.create({ applyDom: true })` the visual one. Both are
disposed in `onDestroy`. The boot sequence, the minimum contracts and the prefs
schema are described in [`architecture/active-uix.md`](./architecture/active-uix.md);
the per-app prefs schema is the same module the pre-hydration boot compiles
feat(apps): apps/base — la primera app que consume UIX como fuente desde un workspace, y su check entra en el gate (F3 del cierre) Hasta hoy ninguna app consumía el framework fuera de su propio árbol de demos, y por eso la receta del boot pudo estar rota en producción sin que nadie lo supiera. apps/base es un esqueleto SvelteKit con adapter-static estricto y prerender, que sigue docs/consuming.md al pie de la letra: alias por resolveUixAliases, runas por config, raíz standalone (createActiveUix + setActiveUix + Soma + ActiveEidos con applyDom), boot compilado con SU esquema (es/en/ar), CSP por hash con el artefacto propio, fuentes y sonidos copiados al static/ de la app (assets:sync, con --check). La página es un AppShell con un Toggle de modo, un Select de idioma y un Dialog. Raíz: workspaces ["apps/*"] (una sola copia de svelte 5.55.0, vite 7.3.1, kit 2.55.0 y esbuild 0.27.4; lock +23 líneas); apps:check = check + build + smoke de la app, dentro del gate justo antes de la suite; .gitignore y .prettierignore para lo generado y lo copiado. El smoke sirve build/ por HTTP y pasa tres visitantes (es-ES, ar-EG, es-ES con modo oscuro y movimiento reducido), cada uno con los módulos abortados (sellos del boot) y con carga completa (0 pageerror, 0 console.error, 0 violaciones CSP, delta 0 entre boot e hidratación). Comprueba además que el tag del boot va DESPUÉS del <meta> CSP: encima, corre sin política y Kit no lo avisa. Constructor, adversarial y dos rondas de cierre (Opus). El adversarial escribió una segunda app solo con el contrato y la hizo arrancar en Chromium, Firefox y WebKit; midió la CSP en siete variantes y tres motores, y mutó la app para ver si el smoke muerde. Defectos cerrados: - D1: el smoke prometía contar traducciones ausentes y en una build de producción no hay señal (engine-langs las emite solo en DEV). Ya no lo promete; README y consuming §8 dicen dónde se ve cada cosa. - D2: el smoke nunca abría modo oscuro. Tercer visitante con el valor afirmado; un pin 'light' olvidado en la raíz ahora da exit 1. - D3: sin reset de documento el Dialog medía 409 px en un móvil de 375. La app lleva su reset de tres reglas (doctrina vigente: el reset es de la app); que la fundación lo traiga es fila del ledger. - D5: los textos del esqueleto seguían en castellano bajo lang=ar. Salen de src/lib/strings.ts por langs.ts(record), tipados. - D6: esa garantía de tipo la sostenía una unión de idiomas escrita a mano. Ahora hay UNA tupla (APP_LANGUAGES en prefs-schema.ts) que leen el esquema, los textos y el Select. Visto fallar: con 'fr' en la tupla, check exit 1 con 8 errores que nombran fr; restaurado byte a byte. - C1-C3: el contrato gana el tsconfig mínimo medido (rewriteRelativeImportExtensions, sin él 578 errores), las rutas de import de Soma/ActiveEidos/createThemeBaseEidosConfig y adapter-static + prerender. Cero cambios en src/**. Huecos del framework que la app destapa, al ledger: el Dialog modal no devuelve el foco al trigger (queda en BODY en los 3 motores: FocusScope enfoca mientras HideOthers aún tiene el inert), Select.Value muestra el value crudo hasta abrir el popup, los catálogos de componentes no tienen 'ar', la foundation se carga dos veces (482 KB enlazados + 426 KB inyectados por applyDom, 5 153 tokens duplicados), guide.md llama eidos.listThemes() dentro del hook, la regla 6 de active-uix.md está caducada, y uix.langs no lleva el tipo del catálogo de la app. Verificación: apps:check exit 0 (1391 ficheros 0 errores · build · smoke `boot 27/27 · unguarded 0 · pageerror 0 · console.error 0 · csp 0 · delta 0 · flips 0`) · boot de la app 15 365 B, byte-idéntico en dos compilaciones, su hash en el <meta> de build/index.html · docs:check 0/0 en 820 docs. Sobre el lote antes de D6 (D6 no toca src/, scripts/ ni la suite): gate entero exit 0 en 357 s, check:gate 89 dentro del ledger, suite 464/464 ficheros · 5442/5442 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
(§6). The root layout of `apps/base`, with the app's own imports reduced to a comment:
```svelte
<!-- apps/<name>/src/routes/+layout.svelte -->
<script lang="ts">
import '$uix/eidos/index.css';
import { onDestroy } from 'svelte';
import { createActiveUix, setActiveUix } from '$active-uix';
import { Soma } from '$soma/core/soma.svelte';
import { ActiveEidos } from '$uix/eidos';
// The app's own modules: its reset (§5), `eidosConfig` and `bootPrefsSchema` (§6).
let { children } = $props();
const uix = createActiveUix({
langs: { schema: {}, defaultLocale: 'es' },
prefs: { schema: bootPrefsSchema('es') }
});
setActiveUix(uix);
Soma.create();
const eidos = ActiveEidos.create({ applyDom: true, config: eidosConfig });
onDestroy(() => {
eidos.dispose();
uix.dispose();
});
</script>
{@render children()}
```
The reset is §5; `eidosConfig` and `bootPrefsSchema` are §6. `langs.schema` is the
app's catalogue for `langs.t(path)` — the `strings` of the theming guide's snippet — and `{}`
when the app has none; the framework registers its components' own words under `components.*`
by itself. `uix.langs` is not typed with the app's catalogue, so a misspelt `t()` path
compiles, and a production build renders the raw path without a word. `apps/base` keeps its
words in records instead (`src/lib/strings.ts`, `satisfies` one string per language) and reads
them with `langs.ts(record)`, which follows the language preference the same way. The languages
are one tuple in `src/prefs-schema.ts` that the schema reads too, so a missing language, a
language added to the schema or a misspelt name fails `npm run check`.
`attachActiveUix` — an external `ActiveApp` that owns its services — exists and
is tested, but no app consumes it and the pre-hydration boot does not cover it.
Build on standalone.
## 5. CSS and static assets
- **CSS.** Import `$uix/eidos/index.css` once, in the root layout. It carries the
static foundation, which a prerendered or server-rendered page needs in its
HTML so the first paint is styled before hydration.
feat(apps): apps/base — la primera app que consume UIX como fuente desde un workspace, y su check entra en el gate (F3 del cierre) Hasta hoy ninguna app consumía el framework fuera de su propio árbol de demos, y por eso la receta del boot pudo estar rota en producción sin que nadie lo supiera. apps/base es un esqueleto SvelteKit con adapter-static estricto y prerender, que sigue docs/consuming.md al pie de la letra: alias por resolveUixAliases, runas por config, raíz standalone (createActiveUix + setActiveUix + Soma + ActiveEidos con applyDom), boot compilado con SU esquema (es/en/ar), CSP por hash con el artefacto propio, fuentes y sonidos copiados al static/ de la app (assets:sync, con --check). La página es un AppShell con un Toggle de modo, un Select de idioma y un Dialog. Raíz: workspaces ["apps/*"] (una sola copia de svelte 5.55.0, vite 7.3.1, kit 2.55.0 y esbuild 0.27.4; lock +23 líneas); apps:check = check + build + smoke de la app, dentro del gate justo antes de la suite; .gitignore y .prettierignore para lo generado y lo copiado. El smoke sirve build/ por HTTP y pasa tres visitantes (es-ES, ar-EG, es-ES con modo oscuro y movimiento reducido), cada uno con los módulos abortados (sellos del boot) y con carga completa (0 pageerror, 0 console.error, 0 violaciones CSP, delta 0 entre boot e hidratación). Comprueba además que el tag del boot va DESPUÉS del <meta> CSP: encima, corre sin política y Kit no lo avisa. Constructor, adversarial y dos rondas de cierre (Opus). El adversarial escribió una segunda app solo con el contrato y la hizo arrancar en Chromium, Firefox y WebKit; midió la CSP en siete variantes y tres motores, y mutó la app para ver si el smoke muerde. Defectos cerrados: - D1: el smoke prometía contar traducciones ausentes y en una build de producción no hay señal (engine-langs las emite solo en DEV). Ya no lo promete; README y consuming §8 dicen dónde se ve cada cosa. - D2: el smoke nunca abría modo oscuro. Tercer visitante con el valor afirmado; un pin 'light' olvidado en la raíz ahora da exit 1. - D3: sin reset de documento el Dialog medía 409 px en un móvil de 375. La app lleva su reset de tres reglas (doctrina vigente: el reset es de la app); que la fundación lo traiga es fila del ledger. - D5: los textos del esqueleto seguían en castellano bajo lang=ar. Salen de src/lib/strings.ts por langs.ts(record), tipados. - D6: esa garantía de tipo la sostenía una unión de idiomas escrita a mano. Ahora hay UNA tupla (APP_LANGUAGES en prefs-schema.ts) que leen el esquema, los textos y el Select. Visto fallar: con 'fr' en la tupla, check exit 1 con 8 errores que nombran fr; restaurado byte a byte. - C1-C3: el contrato gana el tsconfig mínimo medido (rewriteRelativeImportExtensions, sin él 578 errores), las rutas de import de Soma/ActiveEidos/createThemeBaseEidosConfig y adapter-static + prerender. Cero cambios en src/**. Huecos del framework que la app destapa, al ledger: el Dialog modal no devuelve el foco al trigger (queda en BODY en los 3 motores: FocusScope enfoca mientras HideOthers aún tiene el inert), Select.Value muestra el value crudo hasta abrir el popup, los catálogos de componentes no tienen 'ar', la foundation se carga dos veces (482 KB enlazados + 426 KB inyectados por applyDom, 5 153 tokens duplicados), guide.md llama eidos.listThemes() dentro del hook, la regla 6 de active-uix.md está caducada, y uix.langs no lleva el tipo del catálogo de la app. Verificación: apps:check exit 0 (1391 ficheros 0 errores · build · smoke `boot 27/27 · unguarded 0 · pageerror 0 · console.error 0 · csp 0 · delta 0 · flips 0`) · boot de la app 15 365 B, byte-idéntico en dos compilaciones, su hash en el <meta> de build/index.html · docs:check 0/0 en 820 docs. Sobre el lote antes de D6 (D6 no toca src/, scripts/ ni la suite): gate entero exit 0 en 357 s, check:gate 89 dentro del ledger, suite 464/464 ficheros · 5442/5442 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
- **The document reset is the app's.** The foundation ships none, and its
recipes assume three rules: `box-sizing: border-box` on every box, no browser
margin on `body`, and the theme's body style on `body`, by token
(`--style-body-font-family` and its siblings — the foundation declares them
and applies them to no element). `apps/base/src/lib/reset.css` is those
rules, imported after `index.css`. Without them, measured in Chromium,
Firefox and WebKit at 375 × 667: the document is 683 px tall under an
`AppShell` that fills `100dvh`, an open `Dialog` is 409 px wide
(`content-box` plus its padding), and `body` computes each browser's default
family, which the shell's skip link inherits. Whether the foundation should
own the reset instead is an open framework question
([F18](./process/PLAN-blocks-quality.md)); until it does, this is a step of
the contract.
- **Fonts and sounds are served by URL, not imported.** The type scale points at
`/fonts/{family}/…` and the default sound pack at `/sounds/ui/…`, both absolute
paths on the page's own origin. Their files live in the repository's
`static/fonts` and `static/sounds`; the app copies them into its own `static/`
feat(apps): apps/base — la primera app que consume UIX como fuente desde un workspace, y su check entra en el gate (F3 del cierre) Hasta hoy ninguna app consumía el framework fuera de su propio árbol de demos, y por eso la receta del boot pudo estar rota en producción sin que nadie lo supiera. apps/base es un esqueleto SvelteKit con adapter-static estricto y prerender, que sigue docs/consuming.md al pie de la letra: alias por resolveUixAliases, runas por config, raíz standalone (createActiveUix + setActiveUix + Soma + ActiveEidos con applyDom), boot compilado con SU esquema (es/en/ar), CSP por hash con el artefacto propio, fuentes y sonidos copiados al static/ de la app (assets:sync, con --check). La página es un AppShell con un Toggle de modo, un Select de idioma y un Dialog. Raíz: workspaces ["apps/*"] (una sola copia de svelte 5.55.0, vite 7.3.1, kit 2.55.0 y esbuild 0.27.4; lock +23 líneas); apps:check = check + build + smoke de la app, dentro del gate justo antes de la suite; .gitignore y .prettierignore para lo generado y lo copiado. El smoke sirve build/ por HTTP y pasa tres visitantes (es-ES, ar-EG, es-ES con modo oscuro y movimiento reducido), cada uno con los módulos abortados (sellos del boot) y con carga completa (0 pageerror, 0 console.error, 0 violaciones CSP, delta 0 entre boot e hidratación). Comprueba además que el tag del boot va DESPUÉS del <meta> CSP: encima, corre sin política y Kit no lo avisa. Constructor, adversarial y dos rondas de cierre (Opus). El adversarial escribió una segunda app solo con el contrato y la hizo arrancar en Chromium, Firefox y WebKit; midió la CSP en siete variantes y tres motores, y mutó la app para ver si el smoke muerde. Defectos cerrados: - D1: el smoke prometía contar traducciones ausentes y en una build de producción no hay señal (engine-langs las emite solo en DEV). Ya no lo promete; README y consuming §8 dicen dónde se ve cada cosa. - D2: el smoke nunca abría modo oscuro. Tercer visitante con el valor afirmado; un pin 'light' olvidado en la raíz ahora da exit 1. - D3: sin reset de documento el Dialog medía 409 px en un móvil de 375. La app lleva su reset de tres reglas (doctrina vigente: el reset es de la app); que la fundación lo traiga es fila del ledger. - D5: los textos del esqueleto seguían en castellano bajo lang=ar. Salen de src/lib/strings.ts por langs.ts(record), tipados. - D6: esa garantía de tipo la sostenía una unión de idiomas escrita a mano. Ahora hay UNA tupla (APP_LANGUAGES en prefs-schema.ts) que leen el esquema, los textos y el Select. Visto fallar: con 'fr' en la tupla, check exit 1 con 8 errores que nombran fr; restaurado byte a byte. - C1-C3: el contrato gana el tsconfig mínimo medido (rewriteRelativeImportExtensions, sin él 578 errores), las rutas de import de Soma/ActiveEidos/createThemeBaseEidosConfig y adapter-static + prerender. Cero cambios en src/**. Huecos del framework que la app destapa, al ledger: el Dialog modal no devuelve el foco al trigger (queda en BODY en los 3 motores: FocusScope enfoca mientras HideOthers aún tiene el inert), Select.Value muestra el value crudo hasta abrir el popup, los catálogos de componentes no tienen 'ar', la foundation se carga dos veces (482 KB enlazados + 426 KB inyectados por applyDom, 5 153 tokens duplicados), guide.md llama eidos.listThemes() dentro del hook, la regla 6 de active-uix.md está caducada, y uix.langs no lleva el tipo del catálogo de la app. Verificación: apps:check exit 0 (1391 ficheros 0 errores · build · smoke `boot 27/27 · unguarded 0 · pageerror 0 · console.error 0 · csp 0 · delta 0 · flips 0`) · boot de la app 15 365 B, byte-idéntico en dos compilaciones, su hash en el <meta> de build/index.html · docs:check 0/0 en 820 docs. Sobre el lote antes de D6 (D6 no toca src/, scripts/ ni la suite): gate entero exit 0 en 357 s, check:gate 89 dentro del ledger, suite 464/464 ficheros · 5442/5442 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
before `dev` and `build` (`predev` / `prebuild`), and git-ignores the copy. A
symlink is not an option on Windows without privileges, and a committed copy
duplicates binaries that drift. `apps/base/scripts/sync-assets.mjs` is the
copy; with `--check` it changes nothing and exits 1 when the copy drifted.
## 6. The pre-hydration boot
The recipe — placeholder under `%sveltekit.head%`, a `transformPageChunk` with a
function replacement, the CSP hash in `kit.csp` — is
[`theming/guide.md` § How to kill the dark-mode flash](./theming/guide.md). An
app with its own prefs schema compiles its own boot, and must: the section
[Your schema, your boot](./theming/guide.md) explains why the framework's
default artifact resolves a different direction and language than a
multi-language app's runtime. Inside this workspace every `<uix>` in that recipe
feat(apps): apps/base — la primera app que consume UIX como fuente desde un workspace, y su check entra en el gate (F3 del cierre) Hasta hoy ninguna app consumía el framework fuera de su propio árbol de demos, y por eso la receta del boot pudo estar rota en producción sin que nadie lo supiera. apps/base es un esqueleto SvelteKit con adapter-static estricto y prerender, que sigue docs/consuming.md al pie de la letra: alias por resolveUixAliases, runas por config, raíz standalone (createActiveUix + setActiveUix + Soma + ActiveEidos con applyDom), boot compilado con SU esquema (es/en/ar), CSP por hash con el artefacto propio, fuentes y sonidos copiados al static/ de la app (assets:sync, con --check). La página es un AppShell con un Toggle de modo, un Select de idioma y un Dialog. Raíz: workspaces ["apps/*"] (una sola copia de svelte 5.55.0, vite 7.3.1, kit 2.55.0 y esbuild 0.27.4; lock +23 líneas); apps:check = check + build + smoke de la app, dentro del gate justo antes de la suite; .gitignore y .prettierignore para lo generado y lo copiado. El smoke sirve build/ por HTTP y pasa tres visitantes (es-ES, ar-EG, es-ES con modo oscuro y movimiento reducido), cada uno con los módulos abortados (sellos del boot) y con carga completa (0 pageerror, 0 console.error, 0 violaciones CSP, delta 0 entre boot e hidratación). Comprueba además que el tag del boot va DESPUÉS del <meta> CSP: encima, corre sin política y Kit no lo avisa. Constructor, adversarial y dos rondas de cierre (Opus). El adversarial escribió una segunda app solo con el contrato y la hizo arrancar en Chromium, Firefox y WebKit; midió la CSP en siete variantes y tres motores, y mutó la app para ver si el smoke muerde. Defectos cerrados: - D1: el smoke prometía contar traducciones ausentes y en una build de producción no hay señal (engine-langs las emite solo en DEV). Ya no lo promete; README y consuming §8 dicen dónde se ve cada cosa. - D2: el smoke nunca abría modo oscuro. Tercer visitante con el valor afirmado; un pin 'light' olvidado en la raíz ahora da exit 1. - D3: sin reset de documento el Dialog medía 409 px en un móvil de 375. La app lleva su reset de tres reglas (doctrina vigente: el reset es de la app); que la fundación lo traiga es fila del ledger. - D5: los textos del esqueleto seguían en castellano bajo lang=ar. Salen de src/lib/strings.ts por langs.ts(record), tipados. - D6: esa garantía de tipo la sostenía una unión de idiomas escrita a mano. Ahora hay UNA tupla (APP_LANGUAGES en prefs-schema.ts) que leen el esquema, los textos y el Select. Visto fallar: con 'fr' en la tupla, check exit 1 con 8 errores que nombran fr; restaurado byte a byte. - C1-C3: el contrato gana el tsconfig mínimo medido (rewriteRelativeImportExtensions, sin él 578 errores), las rutas de import de Soma/ActiveEidos/createThemeBaseEidosConfig y adapter-static + prerender. Cero cambios en src/**. Huecos del framework que la app destapa, al ledger: el Dialog modal no devuelve el foco al trigger (queda en BODY en los 3 motores: FocusScope enfoca mientras HideOthers aún tiene el inert), Select.Value muestra el value crudo hasta abrir el popup, los catálogos de componentes no tienen 'ar', la foundation se carga dos veces (482 KB enlazados + 426 KB inyectados por applyDom, 5 153 tokens duplicados), guide.md llama eidos.listThemes() dentro del hook, la regla 6 de active-uix.md está caducada, y uix.langs no lleva el tipo del catálogo de la app. Verificación: apps:check exit 0 (1391 ficheros 0 errores · build · smoke `boot 27/27 · unguarded 0 · pageerror 0 · console.error 0 · csp 0 · delta 0 · flips 0`) · boot de la app 15 365 B, byte-idéntico en dos compilaciones, su hash en el <meta> de build/index.html · docs:check 0/0 en 820 docs. Sobre el lote antes de D6 (D6 no toca src/, scripts/ ni la suite): gate entero exit 0 en 357 s, check:gate 89 dentro del ledger, suite 464/464 ficheros · 5442/5442 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
is the path from the app to the repository root, `../..` for `apps/<name>`, and
`tsx` and `esbuild` go in the app's `devDependencies` at the root's versions.
Three things the recipe does not say, measured in `apps/base`:
- **`themeIds` has no instance to ask.** `hooks.server.ts` runs on the server,
where no `ActiveEidos` exists, so the recipe's `eidos.listThemes()` has nothing
to call. Keep the config in ONE module the root and the hook both import
(`apps/base/src/lib/eidos-config.ts`): `createThemeBaseEidosConfig()` from
`$uix/eidos/lib/themes/base` builds it, the root passes it to
`ActiveEidos.create({ applyDom: true, config })`, and the hook passes
`listEidosThemes(config)` from `$uix/eidos/lib/config` — deep imports, no
runes. The list the boot receives is then the list eidos registers, by
construction.
- **Nothing in the build checks the placeholder's position.** With `%uix.boot%`
moved above `%sveltekit.head%`, the build is green, the boot tag lands before
the CSP `<meta>` and it runs under no policy — measured with the framework's
hash instead of the app's in `kit.csp`: nine attributes stamped, zero
violations. The app's smoke checks the order in `build/index.html`. Prettier
joins the two placeholders on one line (`%sveltekit.head% %uix.boot%`); the
order is what matters.
- **The staleness guard and Kit read the current directory.** `uixBootCheck`
resolves `schema` and `out` against it, and Kit loads `svelte.config.js` from
it; `npm run <script> -w apps/<name>` sets it to the app's directory.
The direction contract the boot follows — `<html dir>` is a projection, the
template's `dir` is the only one read back — is
[`canon/direction-contract.md` §6](./canon/direction-contract.md).
## 7. What stays with the framework
Everything under `src/` is the framework's. An app that needs a component, a
token or an API the framework lacks flags the gap so it is built as a reusable
part of the framework, never reinvented inside the app
([`guides/component-guide.md`](./guides/component-guide.md) §4).
feat(apps): apps/base — la primera app que consume UIX como fuente desde un workspace, y su check entra en el gate (F3 del cierre) Hasta hoy ninguna app consumía el framework fuera de su propio árbol de demos, y por eso la receta del boot pudo estar rota en producción sin que nadie lo supiera. apps/base es un esqueleto SvelteKit con adapter-static estricto y prerender, que sigue docs/consuming.md al pie de la letra: alias por resolveUixAliases, runas por config, raíz standalone (createActiveUix + setActiveUix + Soma + ActiveEidos con applyDom), boot compilado con SU esquema (es/en/ar), CSP por hash con el artefacto propio, fuentes y sonidos copiados al static/ de la app (assets:sync, con --check). La página es un AppShell con un Toggle de modo, un Select de idioma y un Dialog. Raíz: workspaces ["apps/*"] (una sola copia de svelte 5.55.0, vite 7.3.1, kit 2.55.0 y esbuild 0.27.4; lock +23 líneas); apps:check = check + build + smoke de la app, dentro del gate justo antes de la suite; .gitignore y .prettierignore para lo generado y lo copiado. El smoke sirve build/ por HTTP y pasa tres visitantes (es-ES, ar-EG, es-ES con modo oscuro y movimiento reducido), cada uno con los módulos abortados (sellos del boot) y con carga completa (0 pageerror, 0 console.error, 0 violaciones CSP, delta 0 entre boot e hidratación). Comprueba además que el tag del boot va DESPUÉS del <meta> CSP: encima, corre sin política y Kit no lo avisa. Constructor, adversarial y dos rondas de cierre (Opus). El adversarial escribió una segunda app solo con el contrato y la hizo arrancar en Chromium, Firefox y WebKit; midió la CSP en siete variantes y tres motores, y mutó la app para ver si el smoke muerde. Defectos cerrados: - D1: el smoke prometía contar traducciones ausentes y en una build de producción no hay señal (engine-langs las emite solo en DEV). Ya no lo promete; README y consuming §8 dicen dónde se ve cada cosa. - D2: el smoke nunca abría modo oscuro. Tercer visitante con el valor afirmado; un pin 'light' olvidado en la raíz ahora da exit 1. - D3: sin reset de documento el Dialog medía 409 px en un móvil de 375. La app lleva su reset de tres reglas (doctrina vigente: el reset es de la app); que la fundación lo traiga es fila del ledger. - D5: los textos del esqueleto seguían en castellano bajo lang=ar. Salen de src/lib/strings.ts por langs.ts(record), tipados. - D6: esa garantía de tipo la sostenía una unión de idiomas escrita a mano. Ahora hay UNA tupla (APP_LANGUAGES en prefs-schema.ts) que leen el esquema, los textos y el Select. Visto fallar: con 'fr' en la tupla, check exit 1 con 8 errores que nombran fr; restaurado byte a byte. - C1-C3: el contrato gana el tsconfig mínimo medido (rewriteRelativeImportExtensions, sin él 578 errores), las rutas de import de Soma/ActiveEidos/createThemeBaseEidosConfig y adapter-static + prerender. Cero cambios en src/**. Huecos del framework que la app destapa, al ledger: el Dialog modal no devuelve el foco al trigger (queda en BODY en los 3 motores: FocusScope enfoca mientras HideOthers aún tiene el inert), Select.Value muestra el value crudo hasta abrir el popup, los catálogos de componentes no tienen 'ar', la foundation se carga dos veces (482 KB enlazados + 426 KB inyectados por applyDom, 5 153 tokens duplicados), guide.md llama eidos.listThemes() dentro del hook, la regla 6 de active-uix.md está caducada, y uix.langs no lleva el tipo del catálogo de la app. Verificación: apps:check exit 0 (1391 ficheros 0 errores · build · smoke `boot 27/27 · unguarded 0 · pageerror 0 · console.error 0 · csp 0 · delta 0 · flips 0`) · boot de la app 15 365 B, byte-idéntico en dos compilaciones, su hash en el <meta> de build/index.html · docs:check 0/0 en 820 docs. Sobre el lote antes de D6 (D6 no toca src/, scripts/ ni la suite): gate entero exit 0 en 357 s, check:gate 89 dentro del ledger, suite 464/464 ficheros · 5442/5442 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
## 8. Verification
[`apps/base`](../apps/base/README.md) is this contract followed to the letter, and
its check is part of the gate:
```bash
npm run apps:check # check + build + smoke of apps/base (member of `npm run gate`)
npm run boot -w apps/base # recompiles src/generated/boot.js; twice in a row is byte-identical
npm run assets:sync -w apps/base -- --check # the copied fonts and sounds have not drifted
```
The smoke serves `build/` from a real HTTP server and prints one line of
figures: the boot tag after the CSP `<meta>`; then, for three visitors —
`es-ES`, `ar-EG`, and `es-ES` asking for a dark scheme and reduced motion —
the nine boot attributes on `<html>` with the app's modules aborted, carrying
the values that visitor asks for (`dir="rtl" lang="ar"`;
`data-mode="dark" data-motion="reduce"`), and a full load with zero page errors,
`console.error` and CSP violations and the same nine attributes after
hydration. It does not count missing translations: langs reports them in dev
only, so a production build has nothing to show. A missing context never
reaches it: it fails the prerender. The commands and the passes are described in
[`apps/base/README.md`](../apps/base/README.md).

Powered by TurnKey Linux.