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>
alpha-0.1-background
dev 3 weeks ago
parent 67edc53d10
commit 8190784003

@ -6,9 +6,10 @@ This file provides guidance to agents when working with code in this repository.
Before creating, porting, or modifying ANY UIX component, you MUST read:
1. **[`web/routes/uix/lib/COMPONENT_AUDIT_GUIDE.md`](web/routes/uix/lib/COMPONENT_AUDIT_GUIDE.md)** — pre-flight audit + reference library matrix + architectural rules + anti-patterns
2. **[`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`](web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md)** — locked demo template (6 tabs, stage, observer, snippets)
1. **[`docs/guides/component-audit.md`](docs/guides/component-audit.md)** — pre-flight audit + reference library matrix + architectural rules + anti-patterns
2. **[`docs/guides/demo-authoring.md`](docs/guides/demo-authoring.md)** — the demo template (the copies under `web/routes/uix/lib/` belong to the frozen tree; these are the canonical ones)
3. **[`src/uix/eidos/components/README.md`](src/uix/eidos/components/README.md)** — eidos contract
4. **[`docs/building-a-component.md`](docs/building-a-component.md)** — the one door: the phases and the guard for each
Skipping these produced the broken Layout Batch 1 (commit `9ec2a57a`) that was reverted + redone. Don't repeat the mistake.
@ -19,13 +20,20 @@ npm run dev # Start dev server
npm run build # Production build (static site)
npm run test # Run all tests once
npm run test:unit # Run tests in watch mode
npx vitest run src/lib/ling/test/ling.test.ts # Run single test file
npx vitest run src/uix/morfo/compile.test.ts # Run single test file
npx vitest run -t "describe name" # Run tests matching pattern
npm run check # Type check with svelte-check
npm run check:gate # ...with the policy: src/ and scripts/ owe ZERO
npm run lint # Check formatting with Prettier
npm run format # Auto-format with Prettier
npm run gate # Everything the pre-push hook runs (lint first, suite last)
```
The zones this runs over — and the frozen `web/routes/` tree — are in
[`docs/repository.md`](docs/repository.md); the gate, the format policy and the
doctrine for guards that read source text are in
[`docs/testing-and-tooling.md`](docs/testing-and-tooling.md).
## Critical Architecture
### One Alias Table
@ -43,11 +51,20 @@ Path aliases live in ONE module, [`uix.aliases.js`](uix.aliases.js), imported by
- **client**: Browser tests via Playwright for `*.svelte.{test,spec}.{js,ts}` files
- **server**: Node environment for `*.{test,spec}.{js,ts}` files (excludes svelte tests)
### Internal Library Pattern
### The Layers and the Artifacts
Each library (`ling`, `logr`, `glob`, `actx`) uses factory functions (`createLing`, `createLogr`, etc.) that return instances with internal state. The `logr` library depends on `ling` for localized messages.
`src/uix/{morfo,soma,sema,eidos}` are the layers; `src/arts/*` the runtime
artifacts (`Engine*` / `Active*`, one per concern: prefs, motion, langs, dom,
…), `src/libs/*` the zero-dependency helpers, `src/svrs/*` the
server-authoritative engines. A service is created by a composition root only
(`createActiveUix` / `attachActiveUix`, `Soma.create`, `ActiveEidos.create`).
The layer map with the hard rules is [`CLAUDE.md`](CLAUDE.md) → Architecture.
The legacy `ling` / `logr` / `glob` / `actx` libraries under `src/lib/` were
removed in the UIX refactor and must not reappear.
## Code Style
- Tabs for indentation, single quotes, no trailing commas, 100 char print width
- Spanish comments in code are acceptable
(`.prettierrc`; `npm run lint` is the first member of the gate)
- Comments in code are **English**. Some legacy Spanish comments remain — don't
add new ones (`CLAUDE.md` → Code Style)

@ -1,19 +1,32 @@
# Name
### vicen
# Synopsis
# Description
# Example
# Install:
`npm install vicen`
# Test:
`npm test`
#License:
# UIX
A Svelte 5 component system built around a **declarative contract**: a component
is declared once in `morfo` (parts, `data-*`/ARIA, keyboard, events with a
semantic family and intent), and the other layers read that declaration —
`soma` runs the behavior, `sema` projects the perceptual signal (sound, haptic,
live region), `eidos` paints against exactly those attributes.
> Morfo declares · Soma transcribes · Sema projects · Eidos paints.
This repository is **private and consumed as source**, not published to npm: an
app lives beside the framework as a workspace and imports it through the shared
alias table — [`docs/consuming.md`](docs/consuming.md).
## Start here
| If you want to… | Read |
| ------------------------------------ | ------------------------------------------------------------------- |
| Understand the corpus and find a doc | [`docs/README.md`](docs/README.md) — the map of everything |
| Run it and make a first change | [`docs/getting-started.md`](docs/getting-started.md) |
| Know what each zone of the tree is | [`docs/repository.md`](docs/repository.md) |
| Build an app on it | [`docs/consuming.md`](docs/consuming.md) · [`apps/base`](apps/base) |
| Build or finish a component | [`docs/building-a-component.md`](docs/building-a-component.md) |
| Test, validate, or reformat | [`docs/testing-and-tooling.md`](docs/testing-and-tooling.md) |
```bash
npm install
npm run dev # the demo site (web/routes — frozen, read-only)
npm run gate # everything the pre-push hook runs
```
Agent rules: [`CLAUDE.md`](CLAUDE.md) and [`AGENTS.md`](AGENTS.md).

@ -15,7 +15,9 @@ links the real document.
> **Want to run it and make a change, not just read?** →
> [`docs/getting-started.md`](./getting-started.md) — clone, run, and your first
> change, in order.
> change, in order. The repository's own zones (what `src/`, `web/routes/`,
> `apps/*` and `scripts/` are, and who may write in each) →
> [`docs/repository.md`](./repository.md).
## What this framework is (60 seconds)
@ -34,15 +36,15 @@ architecture in [`architecture/active-architecture.md`](./architecture/active-ar
The corpus is organized in layers of permanence, not by folder:
| Stratum | What it is | Where |
| ------------------------- | -------------------------------------------- | ---------------------------------------------------------------- |
| **E0 — orientation** | this file; the narrative entry; the glossary | `docs/README.md`, `architecture/overview.md`, `docs/glossary.md` |
| **E1 — architecture** | how the layers fit | `docs/architecture/` (the book chapters) + in-place stubs |
| **E2 — canon** | the fixed vocabulary & contracts | `CANON.md`, `canon/tsc.md`, `canon/recipe-contract.md` |
| **E3 — decisions / RFC** | _why_ it is built this way | `decisions.md` + the RFCs & decision logs |
| **E4 — guides** | how to do a thing | `guides/`, `theming/guide.md` |
| **E5 — module reference** | per-artifact docs | `arts/*/README`, `libs/*`, `svrs/*`, `packs/*/README` |
| **process** | ephemeral (hand-offs, snapshots, audits) | `docs/process/` — never a source of truth |
| Stratum | What it is | Where |
| ------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| **E0 — orientation** | this file; the narrative entry; the glossary; the repository's own map | `docs/README.md`, `architecture/overview.md`, `docs/glossary.md`, `docs/repository.md` |
| **E1 — architecture** | how the layers fit | `docs/architecture/` (the book chapters) + in-place stubs |
| **E2 — canon** | the fixed vocabulary & contracts | `CANON.md`, `canon/tsc.md`, `canon/recipe-contract.md` |
| **E3 — decisions / RFC** | _why_ it is built this way | `decisions.md` + the RFCs & decision logs |
| **E4 — guides** | how to do a thing | `guides/`, `theming/guide.md`, `docs/consuming.md`, `docs/testing-and-tooling.md` |
| **E5 — module reference** | per-artifact docs | `arts/*/README`, `libs/*`, `svrs/*`, `packs/*/README` |
| **process** | ephemeral (hand-offs, snapshots, audits) | `docs/process/` — never a source of truth |
Writing or editing docs? The conventions that keep this corpus drift-free —
link the canon, don't copy it; keep reference docs timeless; one source per
@ -141,7 +143,9 @@ and the server-authoritative engines in `src/svrs/`.
| Understand why a decision was made | `docs/decisions.md` → the relevant RFC / [`decisions/book-deviations.md`](./decisions/book-deviations.md) |
| Use a runtime artifact (auth, cache, http, …) | [`architecture/active-app.md`](./architecture/active-app.md) (the composition root) → [`arts/README.md`](../src/arts/README.md) (the map) → `src/arts/{name}/README.md` (per-artifact) |
| **Write or edit documentation** | [`docs/authoring.md`](./authoring.md) — the authoring rules |
| Test or validate a change | [`docs/testing-and-tooling.md`](./testing-and-tooling.md) — tests, validators, codegen, SSR |
| Test or validate a change | [`docs/testing-and-tooling.md`](./testing-and-tooling.md) — tests, validators, codegen, format policy, the gate, SSR |
| Know what a zone of the repository is for (and what is frozen) | [`docs/repository.md`](./repository.md) — the zones, the single install, the one import map |
| Write a guard (or reformat the tree without blinding one) | [`docs/testing-and-tooling.md`](./testing-and-tooling.md) § Guards that read source text |
## Authoritative sources & rules

@ -85,11 +85,17 @@ services they need — without components ever knowing `ActiveApp` directly.
layer asks for it, and fails with an explicit error if it was not declared.
5. **`prefs.direction` is the effective direction preference**; `html[dir]` is
only its DOM projection.
6. **`ActiveUix` does not auto-project preferences onto the DOM.** `arts/prefs`
projects the cross-modal attrs (`dir`, `data-motion`, `data-sound`,
`data-haptic`) via `createActivePrefsDomProjection`; `ActiveEidos` projects
the visual ones (`data-theme`, `data-mode`, `data-density`). The `frontend`
artifact was retired and must not reappear as a `locale` source.
6. **Standalone projects the cross-modal preferences by default.**
`createActiveUix` builds `createActivePrefsDomProjection` unless
`projectPrefs: false` (`types.ts` → `projectPrefs`, `@default true`), and it
owns `dir`, `lang`, `data-motion`, `data-sound` and `data-haptic`; components
stamp only ASSERTED directions, so the page must reflect the ambient
preference or nothing does. In **attach** it is the opposite: opt-in with
`projectPrefs: true`, because the host app may own `<html>`. The visual axes
(`data-theme`, `data-mode`, `data-density`, `data-scaling`) are `ActiveEidos`'
job, never this one. The `frontend` artifact was retired and must not
reappear as a `locale` source. What UIX writes into `<html dir>` carries an
ownership mark — [`canon/direction-contract.md` §6](../canon/direction-contract.md).
7. **`ActiveUix` neither creates nor knows `Soma` or `Eidos`.** `Soma.create(...)`
creates its own scope and `Soma.runtime(...)`; `ActiveEidos.create(...)`
creates the visual scope when the app needs runtime CSS.
@ -104,15 +110,13 @@ const uix = createActiveUix({ langs, prefs: { schema } });
setActiveUix(uix);
Soma.create();
const prefsProjection = createActivePrefsDomProjection({
prefs: uix.prefs,
dom: uix.dom
});
const eidos = ActiveEidos.create({ applyDom: true });
```
`prefsProjection` owns `dir`, `data-motion`, `data-sound` and `data-haptic`.
The cross-modal projection comes with the root (rule 6): `uix.prefsProjection`
owns `dir`, `lang`, `data-motion`, `data-sound` and `data-haptic`. Building
`createActivePrefsDomProjection` by hand here would be a SECOND projection over
the same attributes.
`ActiveEidos` owns `data-theme`, `data-mode` and `data-density`. Light/dark
mode is a prefs dimension like the rest (2026-09-14): the toggle writes
`uix.prefs.setIntent('mode', 'dark')` and eidos, which reads the slot,

@ -1255,7 +1255,7 @@ runtime that interprets a compiled morfo lives in soma — see
| Command | Purpose |
| ------------------------------ | ---------------------------------------------------------------------------------- |
| `npm run check` | TypeScript type-check across the repo (catches shape errors in morfos). |
| `npm run check:gate` | `check` with policy: `src/` owes zero; `web/` vs the shrinking debt ledger. |
| `npm run check:gate` | `check` with policy: `src/` and `scripts/` owe zero; `web/` vs the ledger. |
| `npm run gate` | The pre-push chain (validators + suite) — `docs/testing-and-tooling.md` §The gate. |
| `npx vitest run src/uix/morfo` | Run morfo unit tests (schema invariants). |
| `npm run smoke` | Playwright smoke over concrete `web/routes` pages (requires dev server). |

@ -393,9 +393,12 @@ pre-hydration boot and every runtime projection — carries the ownership mark
`dir` by presence; the author's `dir`, adopted, is never marked. A script that
writes over a marked `dir` is not a source either: at run time a direction is
asserted with `prefs.setIntent('direction', …)` or `options.prefs.environment`,
both of which beat the seed. The mark's value names its owner, and a
projection's `dispose` retires its attributes only while the mark still names
it — the next root is created before the old one is destroyed.
both of which beat the seed. The mark's value names its owner —
`boot` for the pre-hydration tag, `projection-<n>` for the nth runtime
projection of the process (`PREFS_DIR_BOOT_OWNER`, `arts/prefs/dom-attrs.ts`) —
and a projection's `dispose` retires its attributes only while the mark still
names it: the next root is created before the old one is destroyed, so a blind
`dispose` would strip the live root's `dir`.
That projection is the reason the common case needs no per-component
assertion at all: the page declares its direction once at the root, every

@ -505,7 +505,9 @@ intentGuidance: 'expected' | 'contextual' | 'discouraged';
- **Announce SÍ es canal desde el 2026-07-04 — y esta entrada lo había previsto.**
Su propio cierre decía: «si en el futuro `Announce` necesita ser pluggable …
entonces vale convertirlo en canal formal. Hoy no». Ese futuro llegó: el
`AnnounceChannel` es built-in, opt-in y exportado
`AnnounceChannel` es built-in y exportado; opt-in en el MOTOR desnudo, y las
raíces de composición lo cablean por defecto (ver `architecture/sema.md`
§Announce channel)
(`src/uix/sema/chans/announce.ts`), y [`sema.md` §Announce channel](../architecture/sema.md)
lo declara supersedente. No fue una deriva: fue la puerta que esta entrada
dejó abierta, cruzada sin volver a escribirlo aquí. Canales runtime reales: 4

@ -69,7 +69,7 @@ sema stamps the event, eidos paints — no layer reaches into another's job.
## 4. The verification loop
```bash
npm run check:gate # types WITH policy — src/ owes zero; web/ vs the ledger
npm run check:gate # types WITH policy — src/ and scripts/ owe zero; web/ vs the ledger
npm run check # raw svelte-check (web/ carries frozen ledger errors)
npm run test # vitest suite (two projects: browser client + node server)
npx vitest run <file> # one file
@ -105,6 +105,7 @@ npm run perm:check # re-validate morfo across state transitions
## 6. Where to go next
- The map of all docs → [`docs/README.md`](./README.md)
- What each zone of the repository is (and what is frozen) → [`docs/repository.md`](./repository.md)
- The invented vocabulary → [`docs/glossary.md`](./glossary.md)
- The semantic canon → [`docs/CANON.md`](./CANON.md)
- Writing or editing docs → [`docs/authoring.md`](./authoring.md)

@ -21,7 +21,7 @@ cierre quedó definido por los siete criterios de abajo.
`PENDIENTE` (acción concreta con dueño) · `CONFIRMADO` (medido y cierto, sin
arreglar) · `REFUTADO` (medido y falso) · `DATO` (no es defecto) · `ARREGLADO`
(con hash) · `DIFERIDO` (fuera del cierre, con razón). Hoy: ARREGLADO 16 · CONFIRMADO 14 · DATO 7 · DIFERIDO 120 · PENDIENTE 1 · REFUTADO 2.
(con hash) · `DIFERIDO` (fuera del cierre, con razón). Hoy: ARREGLADO 24 · CONFIRMADO 11 · DATO 7 · DIFERIDO 120 · PENDIENTE 1 · REFUTADO 2.
## Definición de hecho del cierre
@ -173,7 +173,7 @@ Queda una condición del autor: el push (L-123).
| L-130 | Doble foundation en prerender: index.css + uix-eidos-static inyectado por applyDom | plan F2/F3 | — | medio | CONFIRMADO | medido en apps/base (834e6c49a): CSS enlazado 482 535 B + `style#uix-eidos-static` 425 591 B + `#uix-eidos-theme` 31 780 B; los 5 153 tokens del estático están TODOS en el enlazado |
| L-131 | 14 scripts/\_\_\* siguen trackeados (destrackear = borrado del índice) | F0 | — | trivial | DIFERIDO | decisión del autor |
| L-132 | routes/web/marketing.md suelto en la raíz; scripts worker/worker:daemon apuntan a worker.js gitignorado | exploración | — | trivial | DIFERIDO | decisión del autor |
| L-133 | AGENTS.md conserva prosa caduca fuera de la sección de alias (librerías ling/logr/glob/actx que no existen) | F2 | — | pequeño | DIFERIDO | |
| L-133 | AGENTS.md conserva prosa caduca fuera de la sección de alias (librerías ling/logr/glob/actx que no existen) | F2 | — | pequeño | ARREGLADO | pasada de documentación 2026-09-17: AGENTS.md deja de mandar leer el árbol congelado y de describir librerías borradas (`ling`/`logr`/`glob`/`actx`), y su ejemplo de test apunta a un fichero que existe |
| L-134 | Rama `claude/cranky-cori-b70afe` (`6d5a8a19a`, 2026-07-04, 2 ficheros): aria-valuetext del knob formateado por Intl; HEAD ya tiene `formatValue()` (knob-provider) — puede estar superada | L-124 | — | pequeño | DIFERIDO | F5 formateó sin fusionar ramas (decisión del autor 2026-09-17); rescatar exige resolver espacios contra 468a9d113 |
| L-135 | Rama `claude/strange-thompson-58746c` (`2f43d7eb8`, 2026-08-25, 18 READMEs de theming): 13 filas de adjudicación re-derivadas; HEAD evolucionó esos ficheros después (`ef47f3447`) | L-124 | — | pequeño | DIFERIDO | ídem L-134 |
| L-136 | Worktree `.claude/worktrees/agent-aaaa749f035d12f18` con 9 ficheros SIN COMMITEAR de soma (color-field, color-picker, date-field, date-picker, date-range-picker, picker-shell ×2, time-field, time-picker) | `git -C … status` 2026-09-17 | — | medio | DIFERIDO | decisión del autor: commitear o descartar |
@ -182,8 +182,8 @@ Queda una condición del autor: el push (L-123).
| L-139 | La foundation no trae reset ni tipografía en body y las recetas asumen border-box: sin reset el Dialog mide 409 px en 375 | F3 H2/D3 | — | pequeño | DIFERIDO | la app lleva su reset (apps/base/src/lib/reset.css, doctrina vigente F18); si la fundación lo asume, se borra ese fichero y se reescribe consuming §5 |
| L-140 | `Select.Value` muestra el value crudo (`es`) hasta abrir el popup: labelRegistry vacío con los ítems en portal cerrado; select-value.svelte no pinta `children` | F3 H3 | — | pequeño | CONFIRMADO | 3 motores |
| L-141 | Los catálogos de componentes no tienen `ar` (skip link en castellano bajo lang=ar); en producción sin aviso | F3 H4 | — | medio | CONFIRMADO | |
| L-142 | `docs/theming/guide.md` (:326, :531, :621, :634) pone `themeIds: eidos.listThemes()` dentro del hook, donde no hay instancia | F3 H5 | — | trivial | CONFIRMADO | consuming §6 da la forma correcta (listEidosThemes(config)) |
| L-143 | `docs/architecture/active-uix.md` regla 6 y snippet :101-113 caducados: `projectPrefs` es `@default true` | F3 H6 | — | trivial | CONFIRMADO | |
| L-142 | `docs/theming/guide.md` (:326, :531, :621, :634) pone `themeIds: eidos.listThemes()` dentro del hook, donde no hay instancia | F3 H5 | — | trivial | ARREGLADO | pasada de documentación 2026-09-17: los cuatro sitios de la guía usan `THEME_IDS` derivado con `listEidosThemes(config)` y la guía explica que el hook no tiene instancia |
| L-143 | `docs/architecture/active-uix.md` regla 6 y snippet :101-113 caducados: `projectPrefs` es `@default true` | F3 H6 | — | trivial | ARREGLADO | pasada de documentación 2026-09-17: la regla 6 dice que standalone proyecta por defecto y attach es opt-in, y el ejemplo de arranque ya no monta una segunda proyección a mano |
| L-144 | `uix.langs` no lleva el tipo del catálogo de la app: un `t('ruta')` mal escrito compila y se prerenderiza en crudo; producción no emite KEY_NOT_FOUND (solo DEV) | F3 D1 | — | medio | CONFIRMADO | apps/base usa langs.ts(record) tipado con APP_LANGUAGES |
| L-145 | `docs:check` I8 trata todo `docs/**` como voz del framework y prohíbe `$lib` también en código de app (consuming §4 lo lleva comentado) | F3 ronda 2 | — | pequeño | DIFERIDO | |
| L-146 | El HTML prerenderizado lleva el texto en el idioma por defecto aunque el boot ponga lang=ar: el boot corrige atributos, no texto | F3 ronda 2 | — | — | DATO | inherente a prerender de una sola página |
@ -192,12 +192,17 @@ Queda una condición del autor: el push (L-123).
| L-149 | `node_modules/@vicen/base` es un junction a apps/base: un borrado recursivo que siga junctions borra la app | F3 | — | — | DATO | |
| L-150 | El build de Kit no es reproducible: el hash de su script inline de arranque cambia entre builds idénticos | F3 | — | — | DATO | no afecta al hash del boot |
| L-151 | `docs/consuming.md` incumple Prettier desde HEAD 4f3425783 (`;` del snippet de runas) | F3 H8 | — | trivial | ARREGLADO | 468a9d113 |
| L-152 | El defecto de A-106 en masa: wrappers de eidos pasan `...rest` a una parte de soma con `ref = $bindable` sin `bind:ref` ⇒ `bind:ref` del consumidor recibe null | F4 censo estático (adb680899) | — | medio | CONFIRMADO | 532 ficheros en 99 componentes (top: media-player 24, sidebar 17, color-picker 16); muestra runtime 3/3 (checkbox-group-label, field-required-indicator, proof-of-human); 27 ficheros sin resolver por el censo (chronos, date-range-picker…) |
| L-152 | El defecto de A-106 en masa: wrappers de eidos pasan `...rest` a una parte de soma con `ref = $bindable` sin `bind:ref` ⇒ `bind:ref` del consumidor recibe null | F4 censo estático (adb680899) | — | medio | CONFIRMADO | 532 ficheros en 99 componentes (top: media-player 24, sidebar 17, color-picker 16); muestra runtime 3/3. La DOCTRINA ya está escrita (`eidos/components/README.md` §Reglas duras, regla 8) y el Button reparado; los otros wrappers siguen sin reenviar |
| L-153 | `applyOverride` (resolver.ts:343-346) clona un DeltaOp LITERAL como firma cuando la familia no tiene slice haptic base (`emerge` + `channels:['haptic']` + `{ intensity: { op: 'add' } }`) ⇒ HapticChannel.handle lanza `TypeError … 'pattern'`, 0 vibraciones | F4 adversarial L-ADV-3 | — | pequeño | CONFIRMADO | bug de runtime de sema; fuera del lote |
| L-154 | «opt-in» sin calificar para announce en `docs/decisions/book-deviations.md:508` y `src/uix/sema/chans/announce.ts:13` (cierto solo para el motor desnudo) | F4 adversarial L-ADV-4 | — | trivial | CONFIRMADO | |
| L-154 | «opt-in» sin calificar para announce en `docs/decisions/book-deviations.md:508` y `src/uix/sema/chans/announce.ts:13` (cierto solo para el motor desnudo) | F4 adversarial L-ADV-4 | — | trivial | ARREGLADO | pasada de documentación 2026-09-17: `book-deviations.md`, `announce.ts` y `theming/channels.md:58` califican el opt-in (motor desnudo) frente al cableado por defecto de las raíces |
| L-155 | Announce sin pin directo en attach (default ON) ni en motor desnudo (opt-in); la viñeta de attach de sema.md nombra solo `defineEngineSemantic` y omite `defineUixServices({ events: { announce: false } })`, que también sirve | F4 adversarial L-ADV-5/6 | — | pequeño | DIFERIDO | hoy ciertas (medido); solo contracts.test.ts:670-702 pinea standalone |
| L-156 | Siete handoffs de julio en minúscula (`docs/process/continue-*.md`: agente, arts-docs-reconciliation, audit-fixes, cleanroom-fixes, proof-of-human, runed-tabbable-port, with-stumbles-s2-s6) quedaron fuera del censo del plan | F6 | — | — | DATO | no llevan cabecera CONGELADO ni están transcritos aquí: son históricos (varios se declaran cerrados en su cabecera) |
| L-157 | `docs-check` I10 cosecha los identificadores del código LÍNEA A LÍNEA (`identifiersOf`), así que el conjunto de verdad sigue a cómo prettier parte o junta las listas de import/export: 219 de 267 READMEs cambiaron su conjunto (55 crecen, 76 encogen) | F5 medición de guards (8a7a31fb9) | — | pequeño | CONFIRMADO | ceguera LATENTE, no realizada: las 979 filas de prop evaluadas dan el mismo veredicto en ambas revisiones (0 volteos). Cerrarla exige cosechar del AST, no por línea |
| L-158 | `pack-census.test.ts` detecta emisiones con `source.includes("'evento'")`: solo la comilla SIMPLE. Una emisión escrita con comilla doble o backtick produce un falso «nadie lo emite» | F5 medición de guards | — | trivial | CONFIRMADO | medido: dos emisiones de chronos estaban con comilla doble y el formateo las normalizó. Solo puede dar rojo falso, nunca tapar |
| L-159 | `recipe-css-contract.test.ts` delimita el lado derecho de un `export type` con el PRIMER `;`: un infractor repartido en ≥2 miembros de un tipo objeto queda fuera del barrido | F5 medición de guards | — | trivial | CONFIRMADO | hipotético hoy: 0 fugas medidas en el árbol (ningún literal de rol vive en el cuerpo de un `export type` fuera del span) |
| L-160 | `.prettierrc` usa `endOfLine: "auto"`: un CR huérfano en un fichero hace que Prettier lo reescriba ENTERO en CR, y entonces el fichero es UNA sola línea para cualquier guard que parta por saltos de línea | F5 (8a7a31fb9) | — | pequeño | DIFERIDO | pasó en `REVIEW-theming-2026-08-24.md` (normalizado a LF ahí mismo); fijar `endOfLine: "lf"` reescribiría el árbol entero: fila aparte |
| L-161 | El `README.md` de la raíz era una plantilla npm vacía que mandaba `npm install vicen`, con el repositorio `private: true` y sin paquete publicado | pasada de documentación 2026-09-17 | — | trivial | ARREGLADO | reescrito como puerta: la tesis en cuatro líneas, «se consume como fuente», y la tabla de a dónde ir |
| L-162 | No existía ninguna página que describiera el repositorio como objeto: sus zonas, la ley de congelación de `web/routes/`, `scripts/` como zona tipada, el workspace único | pasada de documentación 2026-09-17 | — | pequeño | ARREGLADO | NUEVA `docs/repository.md` (E0), enlazada desde el mapa, el README de la raíz, getting-started y AGENTS.md |
| L-163 | No había doctrina escrita sobre los guards que leen TEXTO fuente: que dependen del formateador y que un guard negativo puede quedarse ciego EN VERDE | pasada de documentación 2026-09-17 | — | pequeño | ARREGLADO | `testing-and-tooling.md` §Guards that read source text: los cinco síntomas medidos, cómo escribirlos y los cuatro pasos antes de un formateo masivo |
| L-164 | El inventario de codegen tenía una fila de seis y la política de formato no estaba escrita (contenido de `.prettierignore`, commit único, `git config blame.ignoreRevsFile`) | pasada de documentación 2026-09-17 | — | trivial | ARREGLADO | `testing-and-tooling.md` §Codegen (los seis artefactos con su generador y sus dos invariantes) y §Format policy |
| L-165 | Ningún guard exige que un guard de texto falle cuando su corpus queda VACÍO; la regla está escrita, pero solo `check-gate.ts` la aplica (marcador `COMPLETED`) | pasada de documentación 2026-09-17 | — | medio | DIFERIDO | la doctrina queda en `testing-and-tooling.md`; hacerla cumplir exige tocar ~30 guards, fuera del cierre |

@ -0,0 +1,79 @@
---
title: The Repository
type: reference
audience: human + agent
authority: reference — the zones of this repository, who may write in each, and the invariants that hold them apart
status: current
---
# The Repository
One tree, one install, one gate. [`docs/README.md`](./README.md) is the map of
the **docs**; this is the map of the **repository**: what each zone is, who may
write in it, and the few invariants that keep the zones from bleeding into each
other. The path from clone to first change is
[`getting-started.md`](./getting-started.md).
## The zones
| Zone | What it is | Who writes it |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- |
| `src/uix/` | The layers — `morfo` (contract), `soma` (behavior), `sema` (perceptual engine), `eidos` (CSS) — plus the `active-uix` composition root and the `blocks` tier | the framework |
| `src/arts/` · `src/libs/` · `src/svrs/` · `src/packs/` | Runtime artifacts, zero-dependency helpers, server-authoritative engines, the opt-in pack tier | the framework |
| `web/routes/` | The demo site Kit serves (`kit.files.routes`). **FROZEN** — see below | nobody |
| `apps/*` | npm workspaces that consume the framework as source; `apps/base` is the reference consumer | app authors |
| `scripts/` | The tooling: validators, generators, the pre-push hook. Type-checked and owing zero, like `src/` | whoever adds or fixes a guard |
| `docs/` | The doc corpus (strata E0–E5), plus `docs/process/` for the ephemeral record | doc authors |
| `static/` | Fonts, sounds, images served by URL; an app copies what it needs into its own `static/` | the framework |
Two zones are NOT source: `src/docs/` holds the book manuscripts the canon comes
from, and `src/lib/_demo` holds the frozen demo site's own widgets (the `$demo`
alias).
## `web/routes/` is frozen
The demo tree is being rebuilt from scratch, so it is **read-only**: read it as
a reference for composition, never edit it. Two consequences that bite:
- the browser-driven validators (`morfo:check`, `perm:check`, `smoke`,
`layer:check`) and the demo rules of `component:audit` validate AGAINST that
frozen tree, so they stay manual until an app tree replaces them;
- `web/` is in `.prettierignore`, so the format policy does not reach it.
## One install, one workspace
The root `package.json` declares `workspaces: ["apps/*"]`: one `node_modules`,
one lockfile, one copy of `svelte`, `vite` and `@sveltejs/kit` for the framework
and every app. Two copies of Svelte would be two runes runtimes, and the
framework's `.svelte.ts` modules would not share state with an app's components.
Vite derives the workspace root from that field, which is what lets an app's dev
server read files under `src/` with no `server.fs.allow`. The contract an app
follows — aliases, runes, composition root, CSS and assets, the pre-hydration
boot — is [`consuming.md`](./consuming.md).
## One import map
Every `$…` / `@/` specifier resolves through ONE module,
[`uix.aliases.js`](../uix.aliases.js), imported by `vite.config.ts`,
`svelte.config.js`, `scripts/generate-boot.ts`, `scripts/docs-check.ts` and each
app's `svelte.config.js`. Order is load-bearing — Vite and esbuild match a
string alias as a PREFIX, so a longer specifier must precede any key that is its
prefix — and `src/uix/aliases.test.ts` fails on a second copy, a broken order,
or a target that is not on disk. The table summary lives in
[`CLAUDE.md`](../CLAUDE.md) → Path Aliases.
## The framework owes zero, the demo tree owes a shrinking ledger
`npm run check:gate` fails on any type error under `src/` or `scripts/`; errors
in the frozen `web/` tree are measured against a per-file ledger that may only
shrink. That policy, the whole gate chain and the pre-push hook are in
[`testing-and-tooling.md`](./testing-and-tooling.md) § The gate — with the
format policy and the inventory of generated artifacts in the same page.
## What is open
The framework's open rows live in one place, by id:
[`docs/process/LEDGER-cierre-2026-09.md`](./process/LEDGER-cierre-2026-09.md).
It is process — a record of what happened, never a source of truth for how the
framework works — but it is the only place that knows what was deferred and why.
The `CONTINUE-*.md` hand-offs beside it are frozen and point at it.

@ -74,9 +74,29 @@ These catch classes of bug that `check` (types) and an HTTP 200 (SSR) miss.
Some surfaces are produced from a source of truth, not hand-maintained. Don't
edit the output; edit the source and regenerate.
| Command | Generates from |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `npm run generate:eidos-css` | the eidos foundation/recipe CSS, from `EidosConfig.recipes`. (`generated/base.css` is output — regenerate, don't hand-edit.) |
| Command | Output | From |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------ |
| `npm run generate:eidos-css` (+ `npm run eidos:purge`) | `src/uix/eidos/generated/base.css` (and the purged variant) | `EidosConfig.recipes` |
| `npm run generate:boot` | `src/uix/active-uix/generated/boot.js` — the framework's pre-hydration boot + its CSP hash | `src/uix/active-uix/boot/*`, compiled with esbuild |
| `npm run boot -w apps/<name>` | `apps/<name>/src/generated/boot.js` — that site's own boot, its own hash | the app's `prefs-schema.ts` (`--schema` / `--out`) |
| `npm run docs:vocabularies` | [`canon/vocabularies.md`](./canon/vocabularies.md) — the closed sets | the code consts; `docs:check` (I7) compares it byte for byte |
| `npm run generate:emoji-data` | `src/libs/emoji/data.ts` | `static/emoji/*.json` (emojibase), pruned |
| `node --import tsx/esm scripts/theming-census.ts --report` | `docs/audit/theming/` | the theming census |
Two invariants for all of them:
- **The generator owns the file.** Edit the source and regenerate; a hand edit
is lost on the next run, and `docs:check` fails outright for the generated
vocabularies.
- **They are all in `.prettierignore`.** A formatter that reformats generated
output turns every regeneration into a diff — and, for a byte-compared
artifact, into a red gate. (Measured: the repo-wide format pass reformatted
`canon/vocabularies.md` and broke `docs:check` until both were excluded.)
An app's copy of the framework's fonts and sounds
(`apps/*/static/{fonts,sounds}`) is generated the same way: `assets:sync`
refreshes it before `dev` and `build`, git ignores it, and `--check` fails when
it drifted.
The `data-*` contract is **not** a generated doc — it is the morfo itself, validated
live by `npm run morfo:check`. (A legacy `generate:contracts-docs` script and its
@ -129,7 +149,12 @@ The drift-defense scripts stopped being advisory in P0 fase B (audit
SHRINKS (the theming-census-debt discipline — a file above its entry, or an
erroring file the ledger does not name, fails). The run must end with the
`COMPLETED` marker and the parsed count must match it: a gate that
inspected nothing fails, never passes.
inspected nothing fails, never passes. The mechanism for `scripts/`: Kit's
generated tsconfig lists routes, lib and src, so `svelte.config.js` pushes
`../scripts/**/*.ts` (and `../uix.aliases.js`) into the include via
`kit.typescript.config`, and the root `tsconfig.json` excludes the probes —
its own `exclude` overrides the generated one, which is why the entry lives
there and not in the config callback.
- **`npm run gate`** — the chain the pre-push hook runs: `lint` first
(`prettier --check`; the repo-wide one-shot landed in the closure plan's F5,
and its commit is listed in `.git-blame-ignore-revs`) → `check:gate` → the
@ -144,8 +169,105 @@ The drift-defense scripts stopped being advisory in P0 fase B (audit
validators (`morfo:check`, `perm:check`, `smoke`, `layer:check`) stay
manual — they need a dev server.
## Format policy
Prettier is the only formatter, and `npm run lint` (`prettier --check .`) is the
FIRST member of the gate: the tree is either formatted or the push is rejected.
Style values (tabs, single quotes, no trailing commas, 100 columns) live in
`.prettierrc` and are summarized in [`CLAUDE.md`](../CLAUDE.md) → Code Style.
`.prettierignore` holds five kinds of exception, and nothing else:
| Excluded | Why |
| ----------------------------------------------------------------------- | --------------------------------------------------------- |
| lockfiles | the package manager owns them |
| `/static/` | assets, not source |
| the generated artifacts (§ Codegen) — including `canon/vocabularies.md` | the generator owns the bytes; see the invariants above |
| `web/` | the frozen demo tree ([`repository.md`](./repository.md)) |
| `.claude/` | tool configuration, including the author's local settings |
The historical repo-wide debt was paid in a single formatting-only commit, and
that commit is listed in `.git-blame-ignore-revs`. `git blame` ignores it only
once per clone, on request:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```
(GitHub honours the file by name; the Gitea remote this repo pushes to does
not — the mitigation is the config above, plus keeping formatting commits pure.)
**A formatting pass is a contract change for every guard that reads source
text** — the next section says why, and what to measure before committing one.
## Guards that read source text
A large family of guards here does not run the code: it READS it. A test or a
script opens `.ts` / `.svelte` / `.css` / `.md` files and asserts on the text —
`contracts.test.ts`, the censuses (`focus-census`, `source-census`,
`pack-census`, `opts-census`), `recipe-css-contract`, `value-channels`,
`docs-check`, `component-audit`, `eidos:lint`, `rtl-check`. They catch what
types cannot: a declaration nobody wired, a token spelled by hand, a doc that
copied a canonical list instead of linking it.
The price is a coupling nothing declares: **a text guard is coupled to the
formatter.** Reformat the tree and some of them change what they inspect. One
direction is loud, the other is silent:
- a **positive** guard (this must be PRESENT) that stops matching goes RED
against correct code — noisy, but you find out;
- a **negative** guard (this must be ABSENT; the offender list must stay empty)
that stops matching goes GREEN while inspecting nothing. The gate says OK and
the contract is gone. It is the same failure as a guard whose corpus is empty:
`check-gate.ts` refuses to judge without the `COMPLETED` marker for exactly
this reason.
Measured on the repo-wide pass, one of each:
| Symptom | Cause |
| ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| A capture ran past its own declaration into the next one, so a field living in the WRONG type satisfied the assert | the delimiter was `\}>;`, and a long declaration prints as `}` then `>;` on the next line |
| `/policy:.*runtime\.focus/` went red against correct code | `.` does not cross a newline, and the value wrapped |
| Disposition markers in a `## Gaps` section dropped to zero | `*diferir*` normalizes to `_diferir_`, and `_` is a word character, so `\b` stops matching |
| Six false offenders in the CSS typography rule | a per-line matcher read `calc(` as the whole value, and the `/* literal: … */` valve fell outside the match |
| A whole chronicle became ONE line for anything splitting on `\n` | `endOfLine: "auto"` amplified a single stray CR into a CR-only file |
### Writing one that does not depend on the formatter
- **Anchor on tokens, not on line shape.** Let whitespace be whitespace:
`\}\s*>;` instead of `\}>;`, `[^;{}]+` instead of `[^\n]+`.
- **Bound the scan by the construct.** A lazy `[\s\S]*?` needs a delimiter that
can only appear where the thing you are matching ends.
- **In markdown, `\b` is not a word boundary** — emphasis markers are word
characters. Use letter boundaries: `(?<!\p{L})…(?!\p{L})`.
- **Ask the compiler when you can.** A hand-written morfo-targeting selector is
an architecture violation because a rename must break at type level (CLAUDE.md
→ Eidos drift defense); a guard that could read the AST or import the module
instead of grepping is the same argument.
- **Fail on an empty corpus.** Assert the count of files/candidates you
inspected, so a guard that stops finding its subject goes red instead of
green.
- **See it fail.** Plant the violation, watch it fail, restore the file byte for
byte. A text guard whose red nobody has seen is a decoration.
### Before committing a formatting pass
1. **Neutrality of the code** — compile and minify every changed file on both
sides (esbuild for `.ts` / `.css`, the Svelte compiler for `.svelte`, client
AND server) and compare. Differences must be explained, not assumed. (On the
repo-wide pass: 1 031 files byte-identical, 9 CSS differing only in
whitespace immediately inside a parenthesis, which CSS does not tokenize.)
2. **The guards' own view** — extract the patterns of every text guard and
count their matches on both sides. A count that moves is a guard to inspect,
the negative ones first.
3. **The whole gate, plus the validators that are NOT members**
(`component:audit`, `theming:sentinel`, the browser-driven ones): a red
outside the gate is still a red.
4. **One commit, formatting only**, listed in `.git-blame-ignore-revs`.
## See also
- [`docs/repository.md`](./repository.md) — the zones these tools run over (and the frozen demo tree).
- [`docs/getting-started.md`](./getting-started.md) — the short verification loop in context.
- [`component-guide.md`](./guides/component-guide.md) — the build checklist that calls these.
- [`completion-checklist.md`](./guides/completion-checklist.md) — what `component:audit` enforces.

@ -55,9 +55,11 @@ is the key that avoids the classic confusion:
| **haptic** | haptics | sema (`chans/haptic`) |
| **announce** | — (accessible substitute, not a book channel) | sema (`chans/announce`) |
`announce` joined on 2026-07-04 as a built-in opt-in channel: it is the
accessible counterpart the book demands when a modality is unavailable
(`BK-SIGNAL-A11Y`), not a ninth perceptual dimension. `visual`, `sound` and
`announce` joined on 2026-07-04 as a built-in channel — opt-in on a bare
engine, wired by DEFAULT by the composition roots since S-19(ii) (see the
backlog entry below and [`architecture/sema.md`](../architecture/sema.md)
§Announce channel): it is the accessible counterpart the book demands when a
modality is unavailable (`BK-SIGNAL-A11Y`), not a ninth perceptual dimension. `visual`, `sound` and
`haptic` are the three that express; `announce` is the one that substitutes.
> **Don't get confused**: **motion · depth · shape · color are NOT "eidos

@ -332,6 +332,7 @@ arrives as a header.
```ts
import { renderUixBootScript } from '$active-uix/boot/render';
import { THEME_IDS } from './lib/eidos-config';
export const handle = ({ event, resolve }) =>
resolve(event, {
@ -339,7 +340,7 @@ export const handle = ({ event, resolve }) =>
html.replace('%uix.boot%', () =>
renderUixBootScript({
defaultLocale: 'es',
themeIds: eidos.listThemes()
themeIds: THEME_IDS
})
)
});
@ -348,6 +349,14 @@ export const handle = ({ event, resolve }) =>
`renderUixBootScript` returns a string and nothing else — the framework does
not own `app.html` and does not install a hook.
**`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).
**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
@ -544,7 +553,7 @@ import { UIX_BOOT_SCRIPT, UIX_BOOT_CSP_HASH } from './generated/boot.js';
renderUixBootScript({
defaultLocale: 'es',
themeIds: eidos.listThemes(),
themeIds: THEME_IDS,
artifact: { script: UIX_BOOT_SCRIPT, hash: UIX_BOOT_CSP_HASH }
});
```
@ -634,7 +643,9 @@ throws `ActiveUixInvalidBootNonceError` instead of writing it into the tag.
- `defaultLocale` — the locale you passed to `createActiveUix({ langs })`. The
preference schema is built around it, so a boot given a different one
resolves a different `lang` than the runtime will.
- `themeIds` — `eidos.listThemes()`. It feeds the "is this family already a
- `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
complete theme id?" branch; without it every family gets a `-light` /
`-dark` suffix appended and `data-theme` disagrees with the runtime.
- `pins` — the axes your ROOT `ActiveEidos` nailed, if any
@ -647,7 +658,7 @@ throws `ActiveUixInvalidBootNonceError` instead of writing it into the tag.
```ts
renderUixBootScript({
defaultLocale: 'es',
themeIds: eidos.listThemes(),
themeIds: THEME_IDS,
pins: { mode: 'dark' }
});
```

@ -99,7 +99,7 @@ NO entra en esta tabla**._
| **Persistencia** (la cookie, su caducidad) | **app-land** — por diseño (D-BLK.6) |
| **Bloqueo de scripts** hasta el sí | **app-land** — es del cargador de la app |
| **Esquina flotante** | **deferred** — otro `placement`; entra como prop cuando una demo lo pida |
| **`Button` acepta `ref` y no lo reenvía** | **canon** — `bind:ref` tipa bien y no ata nada (forma de A-94); aquí el disparador se enfoca por `id` |
| **`Button` acepta `ref` y no lo reenvía** | **RESUELTO** — el wrapper lo reenvía con `bind:ref` (A-106); el block sigue enfocando por `id` |
| **`Switch` no tiene parte de etiqueta** | **canon** — la etiqueta visible es hermana y no es diana de clic; se ata por `aria-labelledby` |
| **El `Dialog` restaura el foco a un nodo muerto** | **canon** — cuando el disparador se desmonta, el foco acaba en el `body`; el block lo devuelve él mismo |

@ -61,6 +61,15 @@ accesibles como propiedades — `<Drawer.Trigger>`, `<Drawer.Content>`, etc.
Drawer.Content = Content;
// ...
```
8. **Un bindable se REENVÍA con `bind:`, nunca por el spread del resto.** Si la
part de Soma declara `ref = $bindable(null)` —o cualquier otro bindable—, el
wrapper lo desestructura y lo ata: `ref = $bindable(null)` en sus `$props()`
y `bind:ref` en la etiqueta de Soma. El spread (`{...rest}`) pasa el VALOR,
no el enlace: el proxy de rest props de Svelte describe cada clave sin
`set`, así que el `bind:ref` del consumidor nunca recibe el elemento y el
tipo lo acepta igual. Medido en el `Button` (fila A-106) y en 532 ficheros
más de este árbol (ledger L-152): al reenviar, comprobarlo con un test
client que afirme que el nodo recibido es el que pinta el componente.
### Estructura de directorio

@ -44,6 +44,21 @@ Cambios sobre ese baseline:
</Button>
```
`ref` llega hasta el `<button>` real: el wrapper lo declara
`ref = $bindable(null)` y lo ata con `bind:ref` al de soma, así que
`<Button bind:ref={el}>` recibe el nodo pintado (también con `child` y con
`loading`). Pinchado en `button.svelte.test.ts`; la regla general para
cualquier wrapper está en
[`components/README.md`](../README.md) §Reglas duras.
```svelte
<script lang="ts">
let el = $state<HTMLElement | null>(null);
</script>
<Button bind:ref={el} onclick={() => el?.focus()}>Guardar</Button>
```
- Props visuales (eidos): `variant` (6), `size` (`xs`–`xl`, responsive),
`rounded` (`sm`–`full`, independiente de `size`), `shape` (familia de
esquina), `block`, `iconOnly`, `icon`/`endIcon`/`spinner` (snippets),

@ -10,7 +10,8 @@ import type { Channel } from './types';
* signal must reach a screen-reader user "con la prioridad adecuada, pero sin
* crear ruido continuo". Historically this lived as a runtime concern
* (`ActiveUix.announce`) or an ad-hoc app cascade example; this makes it a
* first-class, opt-in channel like sound / haptic.
* first-class channel — opt-in on a bare engine like sound / haptic, and wired
* by DEFAULT by the composition roots (`docs/architecture/sema.md` §Announce).
*
* What it does on `handle`:
* - reads the human-facing text from `signal.message` (the runtime never

Loading…
Cancel
Save

Powered by TurnKey Linux.