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

293 lines
16 KiB

---
title: The channels as one system — synthesis
type: notes
audience: human + agent
authority: E3 — the capstone tying the per-channel RFCs to the book's thesis
status: current
source: migrated from src/uix/eidos/CHANNELS_SYNTHESIS.md (2026-07-02, docs-book F7.3)
---
# The channels as one system — synthesis ("Diseñando lo que ocurre")
> The **capstone** of the per-channel RFCs. The thesis of the book _Diseñando
> lo que ocurre_: an interaction **is not** color, or movement, or sound — it
> is **one perceptual occurrence spread across N channels**, under the
> **two-moment** model. This ties together [`rfc-color-engine`](../rfcs/rfc-color-engine.md) ·
> [`rfc-typography`](../rfcs/rfc-typography.md) · [`rfc-depth`](../rfcs/rfc-depth.md) ·
> [`rfc-shape`](../rfcs/rfc-shape.md) ·
> [`rfc-structure`](../rfcs/rfc-structure.md). Live demo: **`/temas/orquesta`** (the mixer).
## 1. Two moments, joined by a token: sema emits, eidos reads
An occurrence flows through **two moments**, connected by the **token**:
- **Sema's moment (emission)** — sema evaluates the occurrence and **emits**
it. Sound + haptics it **executes right there** (runtime channels); for the
visual, it **stamps it as tokens** `data-event-*` (family · intent ·
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
direction · phase). Sema **knows no DOM/CSS**.
- **Eidos's moment (materialization)** — eidos **reads** those tokens
(+ `data-state`) and **materializes** them in CSS (the visual channel). It
is the **sole visual owner**.
The **token is the contract**: sema writes, eidos reads — which is why the
layers decouple (sema DOM-agnostic, eidos free of semantic logic).
Over that producer → consumer axis runs the **temporal** axis (motion F1) —
_which_ token:
| Token | Nature | Who writes it | Eidos reads it as |
| -------------- | ----------------------------------------------- | ------------- | ----------------- |
| `data-state` | persistent — what the element **is** | soma / morfo | `presets` |
| `data-event-*` | transient — what **occurs** (during the `hold`) | sema (emits) | `signatures` |
`sequence` (`pre` / `coincident` / `post`) orders the two.
## 2. Expression channels (the book) vs sema channels (runtime)
The book has **8 expression channels** (perceptual dimensions). The framework
docs(sema): la doctrina deja de mentir — deriva propia, copias podridas y dos leyes muertas Pasada de saneamiento documental tras los tres commits de sonido, con el informe de AUDIT-sema-2026-08-05 como mapa. Tres clases de defecto. 1) DERIVA QUE YO MISMO DEJE (lo mas urgente) El 05 renombre las claves silenciadoras a `commit.silent` / `emerge.silent` y el 06 las elimine del catalogo — dejando media docena de docs citando claves que ya no existen, y afirmando ademas comportamientos que ya no ocurren: - switch / toggle / toggle-group README: decian «silent-by-default (`commit.silent`)». Hoy es `commit.medium` (gain 0.1) y suenan. Corregidos con el nivel real y su razon (un tercio de una pulsacion de boton). - tooltip README + docblock del pack: decian que el tuning «resta el gain de la familia, asi que neutral emite a 0» y que «threat / fulfill siguen aflorando». Ambas cosas son falsas desde el 06: es `SILENT`, el resolver retira el canal y NADA aflora, porque no queda ganancia que subir. - toggle-group apuntaba a `LIBRO_VARIACIONES_Y_EXTENSIONES.md`, que es un stub movido; ahora apunta a book-deviations. - La nota del renombrado en D.5 decia «hoy es `commit.silent`»: una clave que vivio UN DIA. Marcada como tal, con la lista de las que siguen vivas. 2) COPIAS PODRIDAS — la ley del corpus es enlazar, no copiar, y estas dos entradas la incumplian - D.9 transcribia `SEMA_HOLDS_BY_INTENT` entera y se habia quedado atras: D.12 corrigio DOS valores el 2026-07-06 (`commit.fulfill` noticed→settled, `signal.loss` noticed→brief, ambos contra el texto del libro) y la copia siguio afirmando los viejos un mes. Sustituida por el puntero a holds.ts + el enumerado generado, con la leccion escrita en el sitio. - sema.md transcribia `SEMA_VERBS` y le faltaban DOS verbos vivos: `commit.unselect` y `handle.zoom`. Retirada; queda el puntero a verbs.ts y a vocabularies.md, que si se genera y tiene guard de frescura. 3) LEYES QUE LA PRACTICA YA HABIA DEROGADO, Y NADIE REGISTRO - El contrato `emit` publicaba `Promise<void>`; devuelve `Promise<string>` desde D.9. Corregido, y explicado que ese string es el id de la ocurrencia — el unico asidero para cerrar una senal persistente con `clear`. - channels.md fijaba «3 canales runtime» y D.8 cerraba la puerta a Announce («hoy no»). El `AnnounceChannel` existe desde el 2026-07-04: built-in, opt-in y exportado. channels.md pasa a 4 con la distincion que importa (visual/sound/haptic EXPRESAN; announce SUSTITUYE) y D.8 queda marcada PARCIALMENTE SUPERSEDED. - sema.md prescribia `{ announce: uix.announce }` — y eso NO COMPILA: las dos firmas no casan (bolsa de opciones vs posicional). Ahora ensena el adaptador de una linea que si compila, y declara que ninguna raiz cablea el canal hoy, asi que activarlo cae en el fallback que anade un SEGUNDO par de live regions. El arreglo de codigo queda sin tomar: es decision de diseno. - La cabecera de engine-sound.ts y la fila de arts/README seguian afirmando como MECANISMO que «`prefs.sound` mapea al bus ui», que la auditoria AU-4 ya habia corregido en el README del propio arte: lo garantizado por construccion es el NEGATIVO (ninguna politica de UI escribe el bus content). AMBIGUEDADES QUE NO RESUELVO PORQUE SON TUYAS, pero que dejan de estar escondidas: D.7 prohibe canonizar samples fuera de `signal` y `proof-of-human` los usa en dos eventos `commit`, aplastando la modulacion por intent que esa misma entrada existe para proteger —y el comentario del pack afirma literalmente lo contrario de lo que hace—; y D.8 declara que los packs deben respetar el `activeChannels` de la familia mientras sema.md prescribe lo contrario y el pack de dialog deja dos reglas inertes. Ambas quedan marcadas con su estado real y los dos caminos excluyentes, pendientes de tu firma. VERIFICADO: docs:check 0/616 · sonido + sema 244/244 · cero referencias vivas a las tres claves muertas (las que quedan en book-deviations son historia declarada como tal, y las de cronica no se reescriben) · prettier: los 6 docs con avisos ya estaban sucios en HEAD, no los toco. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
implements them with sema's **runtime channels**, which are **4** — and here
is the key that avoids the classic confusion:
| Sema channel (runtime) | Covers (the book's expression) | Who materializes it |
| ----------------------------------------- | --------------------------------------------- | -------------------------------------------------- |
| **visual** (sema projects `data-event-*`) | movement · presence · depth · shape · color | **eidos** — CSS over `data-event-*` + `data-state` |
| **sound** | sound | sema (`chans/sound`) |
| **haptic** | haptics | sema (`chans/haptic`) |
| **announce** | — (accessible substitute, not a book channel) | sema (`chans/announce`) |
docs(sema): la doctrina deja de mentir — deriva propia, copias podridas y dos leyes muertas Pasada de saneamiento documental tras los tres commits de sonido, con el informe de AUDIT-sema-2026-08-05 como mapa. Tres clases de defecto. 1) DERIVA QUE YO MISMO DEJE (lo mas urgente) El 05 renombre las claves silenciadoras a `commit.silent` / `emerge.silent` y el 06 las elimine del catalogo — dejando media docena de docs citando claves que ya no existen, y afirmando ademas comportamientos que ya no ocurren: - switch / toggle / toggle-group README: decian «silent-by-default (`commit.silent`)». Hoy es `commit.medium` (gain 0.1) y suenan. Corregidos con el nivel real y su razon (un tercio de una pulsacion de boton). - tooltip README + docblock del pack: decian que el tuning «resta el gain de la familia, asi que neutral emite a 0» y que «threat / fulfill siguen aflorando». Ambas cosas son falsas desde el 06: es `SILENT`, el resolver retira el canal y NADA aflora, porque no queda ganancia que subir. - toggle-group apuntaba a `LIBRO_VARIACIONES_Y_EXTENSIONES.md`, que es un stub movido; ahora apunta a book-deviations. - La nota del renombrado en D.5 decia «hoy es `commit.silent`»: una clave que vivio UN DIA. Marcada como tal, con la lista de las que siguen vivas. 2) COPIAS PODRIDAS — la ley del corpus es enlazar, no copiar, y estas dos entradas la incumplian - D.9 transcribia `SEMA_HOLDS_BY_INTENT` entera y se habia quedado atras: D.12 corrigio DOS valores el 2026-07-06 (`commit.fulfill` noticed→settled, `signal.loss` noticed→brief, ambos contra el texto del libro) y la copia siguio afirmando los viejos un mes. Sustituida por el puntero a holds.ts + el enumerado generado, con la leccion escrita en el sitio. - sema.md transcribia `SEMA_VERBS` y le faltaban DOS verbos vivos: `commit.unselect` y `handle.zoom`. Retirada; queda el puntero a verbs.ts y a vocabularies.md, que si se genera y tiene guard de frescura. 3) LEYES QUE LA PRACTICA YA HABIA DEROGADO, Y NADIE REGISTRO - El contrato `emit` publicaba `Promise<void>`; devuelve `Promise<string>` desde D.9. Corregido, y explicado que ese string es el id de la ocurrencia — el unico asidero para cerrar una senal persistente con `clear`. - channels.md fijaba «3 canales runtime» y D.8 cerraba la puerta a Announce («hoy no»). El `AnnounceChannel` existe desde el 2026-07-04: built-in, opt-in y exportado. channels.md pasa a 4 con la distincion que importa (visual/sound/haptic EXPRESAN; announce SUSTITUYE) y D.8 queda marcada PARCIALMENTE SUPERSEDED. - sema.md prescribia `{ announce: uix.announce }` — y eso NO COMPILA: las dos firmas no casan (bolsa de opciones vs posicional). Ahora ensena el adaptador de una linea que si compila, y declara que ninguna raiz cablea el canal hoy, asi que activarlo cae en el fallback que anade un SEGUNDO par de live regions. El arreglo de codigo queda sin tomar: es decision de diseno. - La cabecera de engine-sound.ts y la fila de arts/README seguian afirmando como MECANISMO que «`prefs.sound` mapea al bus ui», que la auditoria AU-4 ya habia corregido en el README del propio arte: lo garantizado por construccion es el NEGATIVO (ninguna politica de UI escribe el bus content). AMBIGUEDADES QUE NO RESUELVO PORQUE SON TUYAS, pero que dejan de estar escondidas: D.7 prohibe canonizar samples fuera de `signal` y `proof-of-human` los usa en dos eventos `commit`, aplastando la modulacion por intent que esa misma entrada existe para proteger —y el comentario del pack afirma literalmente lo contrario de lo que hace—; y D.8 declara que los packs deben respetar el `activeChannels` de la familia mientras sema.md prescribe lo contrario y el pack de dialog deja dos reglas inertes. Ambas quedan marcadas con su estado real y los dos caminos excluyentes, pendientes de tu firma. VERIFICADO: docs:check 0/616 · sonido + sema 244/244 · cero referencias vivas a las tres claves muertas (las que quedan en book-deviations son historia declarada como tal, y las de cronica no se reescriben) · prettier: los 6 docs con avisos ya estaban sucios en HEAD, no los toco. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
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
`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
docs(sema): la doctrina deja de mentir — deriva propia, copias podridas y dos leyes muertas Pasada de saneamiento documental tras los tres commits de sonido, con el informe de AUDIT-sema-2026-08-05 como mapa. Tres clases de defecto. 1) DERIVA QUE YO MISMO DEJE (lo mas urgente) El 05 renombre las claves silenciadoras a `commit.silent` / `emerge.silent` y el 06 las elimine del catalogo — dejando media docena de docs citando claves que ya no existen, y afirmando ademas comportamientos que ya no ocurren: - switch / toggle / toggle-group README: decian «silent-by-default (`commit.silent`)». Hoy es `commit.medium` (gain 0.1) y suenan. Corregidos con el nivel real y su razon (un tercio de una pulsacion de boton). - tooltip README + docblock del pack: decian que el tuning «resta el gain de la familia, asi que neutral emite a 0» y que «threat / fulfill siguen aflorando». Ambas cosas son falsas desde el 06: es `SILENT`, el resolver retira el canal y NADA aflora, porque no queda ganancia que subir. - toggle-group apuntaba a `LIBRO_VARIACIONES_Y_EXTENSIONES.md`, que es un stub movido; ahora apunta a book-deviations. - La nota del renombrado en D.5 decia «hoy es `commit.silent`»: una clave que vivio UN DIA. Marcada como tal, con la lista de las que siguen vivas. 2) COPIAS PODRIDAS — la ley del corpus es enlazar, no copiar, y estas dos entradas la incumplian - D.9 transcribia `SEMA_HOLDS_BY_INTENT` entera y se habia quedado atras: D.12 corrigio DOS valores el 2026-07-06 (`commit.fulfill` noticed→settled, `signal.loss` noticed→brief, ambos contra el texto del libro) y la copia siguio afirmando los viejos un mes. Sustituida por el puntero a holds.ts + el enumerado generado, con la leccion escrita en el sitio. - sema.md transcribia `SEMA_VERBS` y le faltaban DOS verbos vivos: `commit.unselect` y `handle.zoom`. Retirada; queda el puntero a verbs.ts y a vocabularies.md, que si se genera y tiene guard de frescura. 3) LEYES QUE LA PRACTICA YA HABIA DEROGADO, Y NADIE REGISTRO - El contrato `emit` publicaba `Promise<void>`; devuelve `Promise<string>` desde D.9. Corregido, y explicado que ese string es el id de la ocurrencia — el unico asidero para cerrar una senal persistente con `clear`. - channels.md fijaba «3 canales runtime» y D.8 cerraba la puerta a Announce («hoy no»). El `AnnounceChannel` existe desde el 2026-07-04: built-in, opt-in y exportado. channels.md pasa a 4 con la distincion que importa (visual/sound/haptic EXPRESAN; announce SUSTITUYE) y D.8 queda marcada PARCIALMENTE SUPERSEDED. - sema.md prescribia `{ announce: uix.announce }` — y eso NO COMPILA: las dos firmas no casan (bolsa de opciones vs posicional). Ahora ensena el adaptador de una linea que si compila, y declara que ninguna raiz cablea el canal hoy, asi que activarlo cae en el fallback que anade un SEGUNDO par de live regions. El arreglo de codigo queda sin tomar: es decision de diseno. - La cabecera de engine-sound.ts y la fila de arts/README seguian afirmando como MECANISMO que «`prefs.sound` mapea al bus ui», que la auditoria AU-4 ya habia corregido en el README del propio arte: lo garantizado por construccion es el NEGATIVO (ninguna politica de UI escribe el bus content). AMBIGUEDADES QUE NO RESUELVO PORQUE SON TUYAS, pero que dejan de estar escondidas: D.7 prohibe canonizar samples fuera de `signal` y `proof-of-human` los usa en dos eventos `commit`, aplastando la modulacion por intent que esa misma entrada existe para proteger —y el comentario del pack afirma literalmente lo contrario de lo que hace—; y D.8 declara que los packs deben respetar el `activeChannels` de la familia mientras sema.md prescribe lo contrario y el pack de dialog deja dos reglas inertes. Ambas quedan marcadas con su estado real y los dos caminos excluyentes, pendientes de tu firma. VERIFICADO: docs:check 0/616 · sonido + sema 244/244 · cero referencias vivas a las tres claves muertas (las que quedan en book-deviations son historia declarada como tal, y las de cronica no se reescriben) · prettier: los 6 docs con avisos ya estaban sucios en HEAD, no los toco. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
`haptic` are the three that express; `announce` is the one that substitutes.
> **Don't get confused**: **motion · depth · shape · color are NOT "eidos
> channels"** — they are sema's **`visual` channel**, which sema projects and
> **eidos materializes**. Eidos is the visual channel's _materializer_, not
> the owner of channels of its own. The eidos RFCs (`COLOR/DEPTH/SHAPE_ENGINE`)
> describe _how_ eidos materializes each visual facet — not independent
> channels.
Plus **space** ([`rfc-structure.md`](../rfcs/rfc-structure.md)) — **structural,
not expressive**: it doesn't "occur", it is the stage (state-only).
## 3. The layer chain (each with its role)
An occurrence **crosses the layers** — it is not "all sema", and eidos is not
a shim:
1. **morfo** — **declares** the event and its semantics (family · intent ·
verb). The contract / DNA; pure TypeScript, no runtime.
2. **soma** — **fires** it (`runtime.trigger`): the sequence `prewrite →
sema.emit → handler →` state attrs. It writes `data-state` (the
state-moment).
3. **sema** — **emits** it: dispatches the signal to its channels —
**executes** `sound` + `haptic`; **projects** the visual channel by
stamping `data-event-*`. It knows no DOM/CSS.
4. **eidos** — **materializes** it: reads `data-state` (`presets`) +
`data-event-*` (`signatures`) and renders them with its **token engines**
(color · motion · depth · shape · space — the RFCs). It is the **complete
visual system** and the sole owner of the visual.
> Eidos does **not "make sema visible"**: sema contributes the **what**
> (family / semantic intent), eidos contributes the **how** (the visual
> vocabulary and its materialization). Co-layers, not one subordinate.
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The split is load-bearing, and the `shift` repair of 2026-08-11 is the case
that shows it. `shift` had a sound (`slide`) and no visual signature at all —
the _shift invisible_ antipattern (book ch. 34 §14) inside the framework that
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
names it. The token was never the problem: sema was already stamping
`data-event-family='shift'`. What was missing was on eidos's side of the line,
and the fix stayed there — two entries in `BUILTIN_SIGNATURES`. Sema's only
addition was a new token, `data-event-direction` (`forward` | `backward`), the
SENSE of the crossing; eidos is what decides that a sense means the inline axis
and flips it under `:dir(rtl)`. Sema still knows nothing about left and right,
which is exactly the property that lets it stay DOM-agnostic.
What `shift` has now are two signatures keyed on that token — `shift-forward`
and `shift-backward` (they name the `shift-cross-*` keyframes): the frame
arrives displaced one `--motion-distance-xl` (30 px) along the **inline** axis
and settles in 320 ms on the `emphasized` curve. An emission that stamps no
direction matches neither and stays visually silent — deliberate, since there
is no neutral sense of travel to draw. Geometry, the `--motion-shift-sign`
pair and the selector shape: [`motion.md`](./motion.md) §8.
And the misreading that delayed the repair, because the structure invites it:
`SEMA_MAP.families.shift.activeChannels` is `['sound']`, which looks like a
family declared mute everywhere else. It is not. **"Mute by doctrine" is a
statement about a CHANNEL, never about a family** — `activeChannels` governs
only the two channels sema _executes_. `delegate` is the proof: its
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`activeChannels` is literally `[]`, and it still carries two visual signatures
(`delegate-return-fulfill`, `delegate-return-loss`). No field of `SEMA_MAP`
could say otherwise, because eidos is the sole visual owner (§1) — the visual
channel is not in the map to be switched off, which is also why motion does not
live there. Read an empty `activeChannels` as "silent", and `shift` files
itself beside `sustain`, whose continuity is genuinely carried by persistent
state and not by a firma.
(The full canonical narrative lives in `CLAUDE.md` → "Sema: open channel
registry".)
## 4. The composition — one event, N channels
The **signature** (`BUILTIN_SIGNATURES` + `BUILTIN_KEYFRAMES`) is
**cross-modal**: a single keyframe carries several modalities. A real example
(`press-squeeze`, family `contact`):
```
contact · press → scale 0.96 (motion)
+ box-shadow → flat (depth recede)
+ --shape-smoothing 2→3 (shape: the corner firms up)
+ tick (sema sound)
+ vibration (sema haptic)
```
One `engine.emit(...)` stamps `data-event-*` (the **visual** channel reacts —
eidos materializes it as color/motion/depth/shape) **and** fires sound +
haptic — from the **same** event. There are no systems coordinating by hand;
there is one occurrence expressing itself through sema's channels.
## 5. Runtime builders — the sextet (an open cage)
Every axis retunable at runtime, same pattern (seed → managed block), opt-in
over the authored scale:
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO F2 — lote mecánico (13 ítems): - DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString propio + suite de contrato (props.test.ts; soma.md §12 cerrado). - THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector (los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la deriva que el builder previene, demostrada en el propio doc). - MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9 tests (selectors.test.ts, matches() real con comillas/corchetes) · MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad · MOR-3 _resetCompileCache borrado (0 usos). - SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo · SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin. - SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de applyDominance → skip defensivo + timer tope de awaitExpression cancelado · SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11) + pin del path de VALOR. - accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) — verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33. F3 — censos con guard: - SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred (consumidores cableados: date/time-field vía soma.uix.timers; avatar/image vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige .schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo). - THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo comment-blanked) + las 15 declaraciones anotadas con su razón + canon recipe-contract §3/§4. - SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/ timeline pinneados (overrides documentados en call-site); media-player Batch-4 (35 hits, cero renderProps) = único batch restante, registrado. - THM-4 doctrinado en eidos.md §unused (comportamiento/composición = legítimo; deuda = eje visual sin consumidor; hotspots por lotes). F4-C — corpus documental (decisiones de usuario aplicadas): - DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL trackeada / des-link históricos) · docs:check I6-links WARN→ERROR. - DOC-1: tabla «Build contract» MIGRADA a component-guide con estados modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil. - DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures · gradient añadido a los DOS capstones (sextet real) · nota de paleta de demo-authoring corregida (universalPaletteDecls + decisión THM-2 = mecanismo universal como sucesor del tracker borrado). - DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado · EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado. SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11): - Reconciliación: los morfos ya no declaran close (delegated al Popover, de-dialoged 06-27); el agujero real era el cierre programático bypaseando dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS. - Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5 providers (14 sitios; select/commit → 'save' = commit.save+fulfill, cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito (ya suena commit-set/cancel por diseño S9). - Verificado en vivo (date-picker): Done → close·commit·fulfill·active · Cancel → close·emerge · cierre real. Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 · docs:check 0/0 con I6 en error · baseline propio 57. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`applyColorScheme` · `applyTypeScale` · `applyDepth` · `applyShape` ·
`applySpacing` · `applyGradients`
And the **capstone composing them**: **`applyTheme(seed)`** — a single seed
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO F2 — lote mecánico (13 ítems): - DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString propio + suite de contrato (props.test.ts; soma.md §12 cerrado). - THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector (los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la deriva que el builder previene, demostrada en el propio doc). - MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9 tests (selectors.test.ts, matches() real con comillas/corchetes) · MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad · MOR-3 _resetCompileCache borrado (0 usos). - SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo · SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin. - SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de applyDominance → skip defensivo + timer tope de awaitExpression cancelado · SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11) + pin del path de VALOR. - accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) — verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33. F3 — censos con guard: - SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred (consumidores cableados: date/time-field vía soma.uix.timers; avatar/image vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige .schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo). - THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo comment-blanked) + las 15 declaraciones anotadas con su razón + canon recipe-contract §3/§4. - SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/ timeline pinneados (overrides documentados en call-site); media-player Batch-4 (35 hits, cero renderProps) = único batch restante, registrado. - THM-4 doctrinado en eidos.md §unused (comportamiento/composición = legítimo; deuda = eje visual sin consumidor; hotspots por lotes). F4-C — corpus documental (decisiones de usuario aplicadas): - DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL trackeada / des-link históricos) · docs:check I6-links WARN→ERROR. - DOC-1: tabla «Build contract» MIGRADA a component-guide con estados modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil. - DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures · gradient añadido a los DOS capstones (sextet real) · nota de paleta de demo-authoring corregida (universalPaletteDecls + decisión THM-2 = mecanismo universal como sucesor del tracker borrado). - DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado · EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado. SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11): - Reconciliación: los morfos ya no declaran close (delegated al Popover, de-dialoged 06-27); el agujero real era el cierre programático bypaseando dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS. - Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5 providers (14 sitios; select/commit → 'save' = commit.save+fulfill, cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito (ya suena commit-set/cancel por diseño S9). - Verificado en vivo (date-picker): Done → close·commit·fulfill·active · Cancel → close·emerge · cierre real. Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 · docs:check 0/0 con I6 en error · baseline propio 57. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
(`{ color?, type?, depth?, shape?, space?, gradient? }`) composes the six
axes in **one managed write** (vs six loose `apply*`), **atomically**: the
axes you pass are applied, the ones you omit revert to the authored
foundation. `clearTheme()` reverts everything. For surgical per-axis tweaks,
the individual `apply*` remain. No reference system gathers the six
perceptual axes under a single runtime theme builder.
docs(sema,theming): el orden real, como se autora cada canal, y el hueco del theming Tres cosas que el cambio del sonido dejo sin plasmar. 1. EL ORDEN CANONICO MENTIA. La numeracion 1·2·3·4·5a·5b esta citada como canonica en sema.md, resolver.ts y engine.ts, y despues del reorden era falsa para `sound`: un pack ya no autora en 5a, lo hace en 1.5. Corregido en los tres sitios a la vez, con el porque — todo lo que AUTORA un sonido corre ahi conservando su precedencia relativa; solo cambio su posicion respecto al intent. El resto de la regla (channels/haptic/hold) mantiene el «gana el ultimo» porque no son ejes evaluativos. 2. COMO SE AUTORA CADA CANAL (CANON §7). El reparto por dueño estaba escrito; lo que un componente ESCRIBE, no. Tabla nueva: el canal visual no se escribe (se declara el evento y eidos reacciona), `sound` es UN NOMBRE, `haptic` es un `kind`. La regla es la misma en las tres filas y ese es el punto: un componente dice QUE ocurre, nunca cuan fuerte, cuan brillante ni cuanto dura. El sonido era la excepcion hasta ayer. 3. EL ANALISIS DEL THEMING que el autor pidio (theming/channels.md §5b). Lo que el cambio NO toco: nada de eidos. El canal visual se proyecta estampando data-event-*, y esa ruta quedo intacta — ni una receta, ni un token, ni un selector. Eidos no sabe que existe el sonido y no le hizo falta. Lo que cambio fue DONDE se autora (un catalogo en vez de 71 ficheros) y CUANDO se aplica (antes del intent), ambos dentro de sema. Lo que si cambio, y es lo util: `applyTheme(seed)` retunea SEIS ejes visuales (color·type·depth·shape·space·gradient) y el sonido tiene CERO. Antes esa asimetria se justificaba sola — con 214 reglas autorando 33 firmas a mano no habia objeto que retunear. Hoy hay exactamente uno: 16 nombres en un const. Un eje `sound` en ThemeSeed seria la misma forma que los otros seis. Y las tres puertas de personalizacion, medidas, ninguna llega al catalogo: `overrides.runtime` recorre rutas de SEMA_MAP y el catalogo no esta en el mapa (ademas se traga las erratas, S-09); `overrides.cascade` es por selector, no un tema; `masterGain` se descarta en SILENCIO (S-05/S-32). Un producto puede silenciar y puede pisar una ocurrencia, pero NO puede re-voceear el sistema. Queda escrito en vez de ser folclore. ⚠️ chronos: `src/uix/sema/components/chronos.ts` entro en la migracion de `769e426c5` pese a ser de escritura excluida. Era forzoso — con el tipo estrechado, dejarlo sin migrar rompe la compilacion del arbol entero — y el cambio es mecanico (soundTuning(...) -> nombre), sin decision de diseño. Su SPEC.md NO se ha tocado y sigue citando `soundTuning('commit.soft')`. VERIFICADO: sema+morfo+sound 437/437 · check 75 · docs:check 0/618. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 5b. How each channel is AUTHORED — and the asymmetry that is left
The sextet above retunes the **visual** channel at runtime. Sound and haptic
have no equivalent, and after the 2026-08-06 rework that gap is worth naming
precisely, because the reason it existed is gone.
| Channel | Where a component AUTHORS it | Where a THEME retunes it |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| visual — color · motion · depth · shape · space | eidos recipes, reacting in CSS to `data-event-*` | `applyTheme(seed)` + the six `apply*` (runtime, atomic) |
| **sound** | **names one entry of `SOUNDS`** (`src/uix/sema/sound-names.ts`) — a word, never a parameter | **nothing** — see below |
| **haptic** | a categorical `kind` (`tick` / `tap` / `pulse` / …) | **nothing** |
docs(sema,theming): el orden real, como se autora cada canal, y el hueco del theming Tres cosas que el cambio del sonido dejo sin plasmar. 1. EL ORDEN CANONICO MENTIA. La numeracion 1·2·3·4·5a·5b esta citada como canonica en sema.md, resolver.ts y engine.ts, y despues del reorden era falsa para `sound`: un pack ya no autora en 5a, lo hace en 1.5. Corregido en los tres sitios a la vez, con el porque — todo lo que AUTORA un sonido corre ahi conservando su precedencia relativa; solo cambio su posicion respecto al intent. El resto de la regla (channels/haptic/hold) mantiene el «gana el ultimo» porque no son ejes evaluativos. 2. COMO SE AUTORA CADA CANAL (CANON §7). El reparto por dueño estaba escrito; lo que un componente ESCRIBE, no. Tabla nueva: el canal visual no se escribe (se declara el evento y eidos reacciona), `sound` es UN NOMBRE, `haptic` es un `kind`. La regla es la misma en las tres filas y ese es el punto: un componente dice QUE ocurre, nunca cuan fuerte, cuan brillante ni cuanto dura. El sonido era la excepcion hasta ayer. 3. EL ANALISIS DEL THEMING que el autor pidio (theming/channels.md §5b). Lo que el cambio NO toco: nada de eidos. El canal visual se proyecta estampando data-event-*, y esa ruta quedo intacta — ni una receta, ni un token, ni un selector. Eidos no sabe que existe el sonido y no le hizo falta. Lo que cambio fue DONDE se autora (un catalogo en vez de 71 ficheros) y CUANDO se aplica (antes del intent), ambos dentro de sema. Lo que si cambio, y es lo util: `applyTheme(seed)` retunea SEIS ejes visuales (color·type·depth·shape·space·gradient) y el sonido tiene CERO. Antes esa asimetria se justificaba sola — con 214 reglas autorando 33 firmas a mano no habia objeto que retunear. Hoy hay exactamente uno: 16 nombres en un const. Un eje `sound` en ThemeSeed seria la misma forma que los otros seis. Y las tres puertas de personalizacion, medidas, ninguna llega al catalogo: `overrides.runtime` recorre rutas de SEMA_MAP y el catalogo no esta en el mapa (ademas se traga las erratas, S-09); `overrides.cascade` es por selector, no un tema; `masterGain` se descarta en SILENCIO (S-05/S-32). Un producto puede silenciar y puede pisar una ocurrencia, pero NO puede re-voceear el sistema. Queda escrito en vez de ser folclore. ⚠️ chronos: `src/uix/sema/components/chronos.ts` entro en la migracion de `769e426c5` pese a ser de escritura excluida. Era forzoso — con el tipo estrechado, dejarlo sin migrar rompe la compilacion del arbol entero — y el cambio es mecanico (soundTuning(...) -> nombre), sin decision de diseño. Su SPEC.md NO se ha tocado y sigue citando `soundTuning('commit.soft')`. VERIFICADO: sema+morfo+sound 437/437 · check 75 · docs:check 0/618. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**What the sound rework did NOT change.** Nothing in eidos. The visual channel
is projected by stamping `data-event-*`, and that path was untouched: no
recipe, no token, no selector moved. Eidos does not know sound exists and did
not need to. The rework changed WHERE the sound is authored (a catalogue
instead of 71 pack files) and WHEN it is applied (before the intent instead of
after it) — both entirely inside sema.
**What it did change, and it is the useful part.** Until then, "theming the
sound" was not a coherent idea: 214 cascade rules authored 33 hand-written
signatures across 71 files, and there was no object to retune. Today there
is exactly one — sixteen names in one const — so a `sound` axis in `ThemeSeed`
would be the same shape as the other six, and a brand could ship its own
feat(sema,sound): un pack se entrega como quieras — y un `data:` no se pide, se descodifica La entrega deja de ser parte del contrato. Un pack de ficheros puede llegar como quince URLs o como UN JSON de base64 (`packFromBase64`): mismo tipo, mismo intercambio en caliente, misma reserva sintética por entrada, y un nombre que no existe sigue sin compilar. Medido sobre `static/sounds/ui-inline.json`: una petición en vez de quince, y 69.082 B con brotli frente a 79.332 B de los ficheros sueltos — el +33 % del base64 lo deshace la compresión, y el JSON gana además porque nadie comprime `audio/mpeg` y todos comprimen `application/json`. ## `step` no se oía Era a la vez el MÁS CORTO (18 ms) y el MÁS FLOJO (gain 0.025), y esas dos cosas se multiplican: el oído integra energía durante ~100-200 ms, así que un tono corto necesita MÁS amplitud, no menos. Medido, rendía a −29,8 dBFS con la novena parte de la energía de `touch`. Ahora 0.06 a los mismos 18 ms. Lo que no había era el guard: la distinguibilidad es una RELACIÓN, y dos sonidos pueden ser perfectamente distintos entre sí y ser los dos inaudibles. `sound-names.test.ts` gana un suelo absoluto sobre `gain² × duración`. ## El precalentamiento no precalentaba - `preload` pedía el contexto con `getOrCreateContext()`, que hace `await resume()` — y antes del primer gesto esa promesa se queda PENDIENTE en Chrome, no se rechaza. No descargaba nada hasta que el usuario ya había pulsado, que es justo la latencia que el preload existe para quitar. `decodeAudioData` funciona en un contexto suspendido. - Aplicar un pack en caliente no calentaba la caché: cada nombre pagaba su viaje la primera vez que sonaba. Ahora `rebuildMap()` llama a `warmSamples()`. - Con `preferences.sound === 'off'` no se descarga NADA — un pack que no se puede oír son bytes gastados en silencio. Pero la petición se RECUERDA en vez de tirarse: el nivel se lee en el dispatch, así que la primera reproducción audible vacía la cola y encender el sonido no devuelve una caché fría. ## Un `data:` se descodifica en el sitio `connect-src 'self'` BLOQUEA `fetch('data:…')` — la directiva casa por ESQUEMA y `'self'` no cubre `data:` — mientras deja pasar una ruta del mismo origen. Un pack inline enrutado por `fetch` caía a síntesis EN SILENCIO justo en el entorno que llevó a alguien a hacerlo inline. Y es ~6x más lento. `bytesFor()` lo resuelve con `atob`. ## De la revisión adversarial - Las claves del JSON se validan: una que no sea `SoundName` LANZA, igual que una ruta mala en `assertMapPaths` y por la misma razón — tragarse la errata da el síntoma «parte de mi pack no se aplicó» sin nada a lo que apuntar. - `isSoundName` preguntaba `name in SEMA_MAP.sounds`, y `in` recorre el prototipo: `isSoundName('toString')` devolvía `true`. Inofensivo mientras todos los llamantes tecleaban el nombre; nada inofensivo al validar red. - `warmSamples` sólo tenía una de las dos ramas que sí tiene el constructor, así que una app con canal de sonido propio se calentaba al arrancar y nunca más; y esa rama leía la preferencia UNA vez, en construcción. Las dos se caían por lo mismo: ahora comparten `warmUrls()` en vez de duplicarse. - El demo del catálogo cancela su fetch: elegir `inline` y saltar a `synth` antes de que llegara reinstalaba el pack sobre un `clearMap()` ya hecho. - `fallbackFor(name)` borra los dos casts que copiaba todo el que autorase un pack de ficheros. ## Documentación `architecture/sema.md` gana la sección de las dos formas de entrega y el párrafo de `preloadSamples` reescrito — describía un precalentamiento que ya no es el que ocurre. El README del arte conoce la ruta `data:`. La página de packs gana su sección equivalente y deja de señalar unos `.wav` sueltos como «el ejemplo». Y un fantasma: `soft` no existe — es un nombre del catálogo viejo que quedaba como ejemplo en ocho sitios, incluido el docstring de `applySounds`, cuyo ejemplo copiado literalmente LANZABA. `static/sounds/ui-inline.json` se genera desde `packs/ui-mp3.ts`; el comando está documentado ahí y reproduce el fichero byte a byte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`tick` the way it ships its own colour ramp.
docs(sema,theming): el orden real, como se autora cada canal, y el hueco del theming Tres cosas que el cambio del sonido dejo sin plasmar. 1. EL ORDEN CANONICO MENTIA. La numeracion 1·2·3·4·5a·5b esta citada como canonica en sema.md, resolver.ts y engine.ts, y despues del reorden era falsa para `sound`: un pack ya no autora en 5a, lo hace en 1.5. Corregido en los tres sitios a la vez, con el porque — todo lo que AUTORA un sonido corre ahi conservando su precedencia relativa; solo cambio su posicion respecto al intent. El resto de la regla (channels/haptic/hold) mantiene el «gana el ultimo» porque no son ejes evaluativos. 2. COMO SE AUTORA CADA CANAL (CANON §7). El reparto por dueño estaba escrito; lo que un componente ESCRIBE, no. Tabla nueva: el canal visual no se escribe (se declara el evento y eidos reacciona), `sound` es UN NOMBRE, `haptic` es un `kind`. La regla es la misma en las tres filas y ese es el punto: un componente dice QUE ocurre, nunca cuan fuerte, cuan brillante ni cuanto dura. El sonido era la excepcion hasta ayer. 3. EL ANALISIS DEL THEMING que el autor pidio (theming/channels.md §5b). Lo que el cambio NO toco: nada de eidos. El canal visual se proyecta estampando data-event-*, y esa ruta quedo intacta — ni una receta, ni un token, ni un selector. Eidos no sabe que existe el sonido y no le hizo falta. Lo que cambio fue DONDE se autora (un catalogo en vez de 71 ficheros) y CUANDO se aplica (antes del intent), ambos dentro de sema. Lo que si cambio, y es lo util: `applyTheme(seed)` retunea SEIS ejes visuales (color·type·depth·shape·space·gradient) y el sonido tiene CERO. Antes esa asimetria se justificaba sola — con 214 reglas autorando 33 firmas a mano no habia objeto que retunear. Hoy hay exactamente uno: 16 nombres en un const. Un eje `sound` en ThemeSeed seria la misma forma que los otros seis. Y las tres puertas de personalizacion, medidas, ninguna llega al catalogo: `overrides.runtime` recorre rutas de SEMA_MAP y el catalogo no esta en el mapa (ademas se traga las erratas, S-09); `overrides.cascade` es por selector, no un tema; `masterGain` se descarta en SILENCIO (S-05/S-32). Un producto puede silenciar y puede pisar una ocurrencia, pero NO puede re-voceear el sistema. Queda escrito en vez de ser folclore. ⚠️ chronos: `src/uix/sema/components/chronos.ts` entro en la migracion de `769e426c5` pese a ser de escritura excluida. Era forzoso — con el tipo estrechado, dejarlo sin migrar rompe la compilacion del arbol entero — y el cambio es mecanico (soundTuning(...) -> nombre), sin decision de diseño. Su SPEC.md NO se ha tocado y sigue citando `soundTuning('commit.soft')`. VERIFICADO: sema+morfo+sound 437/437 · check 75 · docs:check 0/618. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(sema): el mapa perceptual se TEMATIZA, y con la misma forma que los ejes visuales El autor: «que yo no pueda definir a nivel de tema la personalizacion es un fallo». Lo era, y medirlo destapo algo peor que el hueco que se veia. NO ERA UN CAMPO QUE FALTABA EN EL SEED: eran dos formas distintas de personalizar. Eidos retunea en vivo, atomico y revertible (applyX/clearX); sema leia `overrides.runtime` UNA VEZ en el constructor y nunca mas, sin forma de revertir. Un `applyTheme` unico construido encima habria sido peor que dos puertas honestas — una llamada donde el color cambia ahora y el sonido no cambia nunca, porque ya arranco. Por eso la puerta unica va la ultima y aqui va el sustrato. 1. EL CATALOGO ENTRA EN EL MAPA (`families · intents · sounds`). Es dato, y el mapa es la estructura que un tema direcciona. Mientras vivio fuera como const de modulo —error mio de ayer— el VOCABULARIO era el unico eje perceptual que un producto no podia re-vocear a ninguna hora. El resolver pasa a resolver el nombre contra el mapa que recibe, no contra un const, que es lo que hace que un tema surta efecto. 2. `applyMap(seed)` · `applySounds({ soft: { gain } })` · `clearMap()` — en vivo y revertibles, reconstruyendo desde el mapa autorado, asi que aplicar dos veces es aplicar una. Misma forma que los seis applyX de eidos. El engine ya pasaba `this.map` por emision, asi que el retuneo surte efecto en la siguiente ocurrencia sin re-arranque. 3. UNA RUTA MALA LANZA (S-09). Antes CREABA la rama: `families.commmit.…` dejaba el valor real intacto y hacia crecer un fantasma al lado, en silencio; y descender a traves de una hoja primitiva la sustituia por un objeto. En una API de theming eso es intolerable — el sintoma es «el sonido no cambio» y no hay nada a lo que apuntar. Se valida tambien en construccion, no solo en el retuneo. LO QUE SIGUE CERRADO A PROPOSITO: el vocabulario. `SoundName` es `keyof SOUND_CATALOGUE`, asi que un tema cambia a que suena un nombre y no puede inventar uno que ningun componente pueda referenciar. La voz se abre; las palabras no. ⚠️ REGRESION PROPIA, cazada y corregida antes de commitear: tipar el mapa `as const satisfies SemaMap` daba las claves literales que `SoundName` necesita, pero estrechaba TODO el mapa y rompia a los consumidores que lo recorren en generico (el estudio de sema: +5 errores en check). El catalogo pasa a ser su propio const y `SEMA_MAP` conserva su anotacion ancha. ⚠️ Y un hallazgo del harness: los tres primeros tests median 0.85x de lo esperado. No era el codigo — era BK-FREQ-MEMORY atenuando la tercera emision del mismo evento. La memoria de frecuencia funciona. VERIFICADO: `theming.test.ts` 7/7 (re-vocea un NOMBRE en vivo y revierte, re-vocea una FAMILIA, idempotente, no muta el mapa canonico, rechaza las dos rutas malas de S-09) · sema+morfo+sound 444/444 · check 75 = baseline · docs:check 0/618. QUEDA, y es decision de producto no defecto: la puerta unica `uix.applyTheme(seed)` en la raiz de composicion. Mas dos menores: `masterGain` sigue descartandose en silencio (S-05/S-32) y la tabla haptica `kind → pattern` sigue en el canal en vez de en el mapa, asi que es el unico eje perceptual que un tema no alcanza. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**The gap, and its closing (2026-08-06).** The table above used to have a
second failure hiding behind the first: not only was there no `sound` axis in
the seed, the two halves had different SHAPES. Eidos retunes live, atomically,
revertibly; sema read `overrides.runtime` ONCE in the engine constructor and
never again. A single `applyTheme` built over that would have been worse than
two honest doors — one call where the colour changes now and the sound changes
never, because it had already booted.
So the substrate was made symmetric first:
- **The catalogue moved INTO `SEMA_MAP`** (`families · intents · sounds`). It
is data, and the map is the structure a theme addresses. While it sat outside
as a module const, the VOCABULARY was the one perceptual axis a product could
not re-voice at any time.
- **`applyMap(seed)` / `applySounds(seed)` / `clearMap()`** — live and
revertible, rebuilt from the authored map so applying twice is applying once.
Same shape as every `applyX` / `clearX` on the visual side.
- **A bad path now THROWS** (`SemaConfigError`). It used to create the branch:
`families.commmit.…` left the real value untouched and grew a phantom beside
it, in silence (audit S-09). A theming API that swallows a typo is worse than
no API — the symptom is «the sound did not change», with nothing to point at.
What a theme can reach today, measured in `theming.test.ts`:
| Target | Path |
| --------------------------- | ------------------------------------------------------------- |
| a family's base | `families.commit.base.sound.gain` |
| an intent's delta | `intents.fulfill.deltas.sound.gain` |
feat(sema,sound): un pack se entrega como quieras — y un `data:` no se pide, se descodifica La entrega deja de ser parte del contrato. Un pack de ficheros puede llegar como quince URLs o como UN JSON de base64 (`packFromBase64`): mismo tipo, mismo intercambio en caliente, misma reserva sintética por entrada, y un nombre que no existe sigue sin compilar. Medido sobre `static/sounds/ui-inline.json`: una petición en vez de quince, y 69.082 B con brotli frente a 79.332 B de los ficheros sueltos — el +33 % del base64 lo deshace la compresión, y el JSON gana además porque nadie comprime `audio/mpeg` y todos comprimen `application/json`. ## `step` no se oía Era a la vez el MÁS CORTO (18 ms) y el MÁS FLOJO (gain 0.025), y esas dos cosas se multiplican: el oído integra energía durante ~100-200 ms, así que un tono corto necesita MÁS amplitud, no menos. Medido, rendía a −29,8 dBFS con la novena parte de la energía de `touch`. Ahora 0.06 a los mismos 18 ms. Lo que no había era el guard: la distinguibilidad es una RELACIÓN, y dos sonidos pueden ser perfectamente distintos entre sí y ser los dos inaudibles. `sound-names.test.ts` gana un suelo absoluto sobre `gain² × duración`. ## El precalentamiento no precalentaba - `preload` pedía el contexto con `getOrCreateContext()`, que hace `await resume()` — y antes del primer gesto esa promesa se queda PENDIENTE en Chrome, no se rechaza. No descargaba nada hasta que el usuario ya había pulsado, que es justo la latencia que el preload existe para quitar. `decodeAudioData` funciona en un contexto suspendido. - Aplicar un pack en caliente no calentaba la caché: cada nombre pagaba su viaje la primera vez que sonaba. Ahora `rebuildMap()` llama a `warmSamples()`. - Con `preferences.sound === 'off'` no se descarga NADA — un pack que no se puede oír son bytes gastados en silencio. Pero la petición se RECUERDA en vez de tirarse: el nivel se lee en el dispatch, así que la primera reproducción audible vacía la cola y encender el sonido no devuelve una caché fría. ## Un `data:` se descodifica en el sitio `connect-src 'self'` BLOQUEA `fetch('data:…')` — la directiva casa por ESQUEMA y `'self'` no cubre `data:` — mientras deja pasar una ruta del mismo origen. Un pack inline enrutado por `fetch` caía a síntesis EN SILENCIO justo en el entorno que llevó a alguien a hacerlo inline. Y es ~6x más lento. `bytesFor()` lo resuelve con `atob`. ## De la revisión adversarial - Las claves del JSON se validan: una que no sea `SoundName` LANZA, igual que una ruta mala en `assertMapPaths` y por la misma razón — tragarse la errata da el síntoma «parte de mi pack no se aplicó» sin nada a lo que apuntar. - `isSoundName` preguntaba `name in SEMA_MAP.sounds`, y `in` recorre el prototipo: `isSoundName('toString')` devolvía `true`. Inofensivo mientras todos los llamantes tecleaban el nombre; nada inofensivo al validar red. - `warmSamples` sólo tenía una de las dos ramas que sí tiene el constructor, así que una app con canal de sonido propio se calentaba al arrancar y nunca más; y esa rama leía la preferencia UNA vez, en construcción. Las dos se caían por lo mismo: ahora comparten `warmUrls()` en vez de duplicarse. - El demo del catálogo cancela su fetch: elegir `inline` y saltar a `synth` antes de que llegara reinstalaba el pack sobre un `clearMap()` ya hecho. - `fallbackFor(name)` borra los dos casts que copiaba todo el que autorase un pack de ficheros. ## Documentación `architecture/sema.md` gana la sección de las dos formas de entrega y el párrafo de `preloadSamples` reescrito — describía un precalentamiento que ya no es el que ocurre. El README del arte conoce la ruta `data:`. La página de packs gana su sección equivalente y deja de señalar unos `.wav` sueltos como «el ejemplo». Y un fantasma: `soft` no existe — es un nombre del catálogo viejo que quedaba como ejemplo en ocho sitios, incluido el docstring de `applySounds`, cuyo ejemplo copiado literalmente LANZABA. `static/sounds/ui-inline.json` se genera desde `packs/ui-mp3.ts`; el comando está documentado ahí y reproduce el fichero byte a byte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **what a NAME sounds like** | `sounds.tick.gain`, or `applySounds({ tick: { gain: 0.1 } })` |
feat(sema): el mapa perceptual se TEMATIZA, y con la misma forma que los ejes visuales El autor: «que yo no pueda definir a nivel de tema la personalizacion es un fallo». Lo era, y medirlo destapo algo peor que el hueco que se veia. NO ERA UN CAMPO QUE FALTABA EN EL SEED: eran dos formas distintas de personalizar. Eidos retunea en vivo, atomico y revertible (applyX/clearX); sema leia `overrides.runtime` UNA VEZ en el constructor y nunca mas, sin forma de revertir. Un `applyTheme` unico construido encima habria sido peor que dos puertas honestas — una llamada donde el color cambia ahora y el sonido no cambia nunca, porque ya arranco. Por eso la puerta unica va la ultima y aqui va el sustrato. 1. EL CATALOGO ENTRA EN EL MAPA (`families · intents · sounds`). Es dato, y el mapa es la estructura que un tema direcciona. Mientras vivio fuera como const de modulo —error mio de ayer— el VOCABULARIO era el unico eje perceptual que un producto no podia re-vocear a ninguna hora. El resolver pasa a resolver el nombre contra el mapa que recibe, no contra un const, que es lo que hace que un tema surta efecto. 2. `applyMap(seed)` · `applySounds({ soft: { gain } })` · `clearMap()` — en vivo y revertibles, reconstruyendo desde el mapa autorado, asi que aplicar dos veces es aplicar una. Misma forma que los seis applyX de eidos. El engine ya pasaba `this.map` por emision, asi que el retuneo surte efecto en la siguiente ocurrencia sin re-arranque. 3. UNA RUTA MALA LANZA (S-09). Antes CREABA la rama: `families.commmit.…` dejaba el valor real intacto y hacia crecer un fantasma al lado, en silencio; y descender a traves de una hoja primitiva la sustituia por un objeto. En una API de theming eso es intolerable — el sintoma es «el sonido no cambio» y no hay nada a lo que apuntar. Se valida tambien en construccion, no solo en el retuneo. LO QUE SIGUE CERRADO A PROPOSITO: el vocabulario. `SoundName` es `keyof SOUND_CATALOGUE`, asi que un tema cambia a que suena un nombre y no puede inventar uno que ningun componente pueda referenciar. La voz se abre; las palabras no. ⚠️ REGRESION PROPIA, cazada y corregida antes de commitear: tipar el mapa `as const satisfies SemaMap` daba las claves literales que `SoundName` necesita, pero estrechaba TODO el mapa y rompia a los consumidores que lo recorren en generico (el estudio de sema: +5 errores en check). El catalogo pasa a ser su propio const y `SEMA_MAP` conserva su anotacion ancha. ⚠️ Y un hallazgo del harness: los tres primeros tests median 0.85x de lo esperado. No era el codigo — era BK-FREQ-MEMORY atenuando la tercera emision del mismo evento. La memoria de frecuencia funciona. VERIFICADO: `theming.test.ts` 7/7 (re-vocea un NOMBRE en vivo y revierte, re-vocea una FAMILIA, idempotente, no muta el mapa canonico, rechaza las dos rutas malas de S-09) · sema+morfo+sound 444/444 · check 75 = baseline · docs:check 0/618. QUEDA, y es decision de producto no defecto: la puerta unica `uix.applyTheme(seed)` en la raiz de composicion. Mas dos menores: `masterGain` sigue descartandose en silencio (S-05/S-32) y la tabla haptica `kind → pattern` sigue en el canal en vez de en el mapa, asi que es el unico eje perceptual que un tema no alcanza. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**What stays closed on purpose**: the vocabulary. `SoundName` is
`keyof SOUND_CATALOGUE`, so a theme changes what a name sounds like and cannot
invent one no component could reference. The voice is open; the words are not.
feat(eidos,sema): un tema es UNA cosa — siete ejes, y ninguno fuera del alcance Cierra el eje: el haptico entra al mapa, `masterGain` deja de descartarse, y `applyTheme` gana el septimo eje. ⚠️ CORRIGE MI PROPIA PROPUESTA. Dije que la puerta unica iba en la raiz de composicion. Al medirlo, la dependencia corre AL REVES: `ActiveEidos` tiene `#uix` y lo conduce (registra presets con `uix.motion`), mientras `ActiveUix` no tiene eidos. Un tema compuesto en la raiz habria invertido la direccion. Va donde ya vive el metodo que se llama a si mismo «the capstone composing them», y que hasta hoy componia seis de ocho. eidos.applyTheme({ color: '#3b5bdb', sound: { soft: { gain: 0.08 } } }); eidos.clearTheme(); // revierte los siete Un eje que pasas se aplica, uno que omites revierte — la misma lectura atomica que ya tenian los seis visuales, ahora sobre siete. Sin `uix` (render headless) el eje perceptual se salta y los visuales siguen: un tema que se aplica a medias es mejor que uno que revienta. EL HAPTICO, AL MAPA. Su tabla `kind → pattern` vivia como const dentro del canal, asi que era el unico eje perceptual que un tema no alcanzaba — exactamente la asimetria que el trabajo del sonido existia para quitar, dejada en pie un canal mas alla. Ahora es `SEMA_MAP.haptics`, con perfiles derivados (`floorMs` + `scale`/`gapMs`) o constantes (`pattern`): los tres kinds evaluativos son ritmos constantes porque lo que hace reconocible a un `error` es su cadencia, no su longitud. El canal lo lee por GETTER, no por copia, asi que un `applyMap()` en caliente le llega sin re-registrarlo. `masterGain` SE APLICA. Pasarlo con motor compartido se tiraba en silencio (S-05/S-32) pese a ser una perilla viva del motor y la forma natural de poner el volumen desde la raiz. Ahora llega a `master.setGain`, y de las opciones que de verdad no pueden aplicarse a un motor ajeno se AVISA en vez de desaparecer. Y `DeltaValue` pasa a exportarse: es la forma de toda semilla de override, sin ella un consumidor no puede tipar el tema que esta pasando. Su ausencia era un error VIVO en el estudio de sema, que lo importaba y no podia — por eso check BAJA a 73 desde el baseline de 75. VERIFICADO: theming 8/8 (incluye re-vocear el haptico y que la tabla autorada quede intacta) · sema+eidos+morfo 802/802 · check 73 (baseline 75, −2 por el export) · docs:check 0/618 · suite completa 7 fallos, los mismos ajenos y preexistentes de contracts.test. ⚠️ Nada de esto se ha OIDO ni TOCADO en un dispositivo real: todo esta medido con el resolver y las suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**One door, and it is `applyTheme`.** The seed gained a seventh axis:
```ts
feat(sema,sound): un pack se entrega como quieras — y un `data:` no se pide, se descodifica La entrega deja de ser parte del contrato. Un pack de ficheros puede llegar como quince URLs o como UN JSON de base64 (`packFromBase64`): mismo tipo, mismo intercambio en caliente, misma reserva sintética por entrada, y un nombre que no existe sigue sin compilar. Medido sobre `static/sounds/ui-inline.json`: una petición en vez de quince, y 69.082 B con brotli frente a 79.332 B de los ficheros sueltos — el +33 % del base64 lo deshace la compresión, y el JSON gana además porque nadie comprime `audio/mpeg` y todos comprimen `application/json`. ## `step` no se oía Era a la vez el MÁS CORTO (18 ms) y el MÁS FLOJO (gain 0.025), y esas dos cosas se multiplican: el oído integra energía durante ~100-200 ms, así que un tono corto necesita MÁS amplitud, no menos. Medido, rendía a −29,8 dBFS con la novena parte de la energía de `touch`. Ahora 0.06 a los mismos 18 ms. Lo que no había era el guard: la distinguibilidad es una RELACIÓN, y dos sonidos pueden ser perfectamente distintos entre sí y ser los dos inaudibles. `sound-names.test.ts` gana un suelo absoluto sobre `gain² × duración`. ## El precalentamiento no precalentaba - `preload` pedía el contexto con `getOrCreateContext()`, que hace `await resume()` — y antes del primer gesto esa promesa se queda PENDIENTE en Chrome, no se rechaza. No descargaba nada hasta que el usuario ya había pulsado, que es justo la latencia que el preload existe para quitar. `decodeAudioData` funciona en un contexto suspendido. - Aplicar un pack en caliente no calentaba la caché: cada nombre pagaba su viaje la primera vez que sonaba. Ahora `rebuildMap()` llama a `warmSamples()`. - Con `preferences.sound === 'off'` no se descarga NADA — un pack que no se puede oír son bytes gastados en silencio. Pero la petición se RECUERDA en vez de tirarse: el nivel se lee en el dispatch, así que la primera reproducción audible vacía la cola y encender el sonido no devuelve una caché fría. ## Un `data:` se descodifica en el sitio `connect-src 'self'` BLOQUEA `fetch('data:…')` — la directiva casa por ESQUEMA y `'self'` no cubre `data:` — mientras deja pasar una ruta del mismo origen. Un pack inline enrutado por `fetch` caía a síntesis EN SILENCIO justo en el entorno que llevó a alguien a hacerlo inline. Y es ~6x más lento. `bytesFor()` lo resuelve con `atob`. ## De la revisión adversarial - Las claves del JSON se validan: una que no sea `SoundName` LANZA, igual que una ruta mala en `assertMapPaths` y por la misma razón — tragarse la errata da el síntoma «parte de mi pack no se aplicó» sin nada a lo que apuntar. - `isSoundName` preguntaba `name in SEMA_MAP.sounds`, y `in` recorre el prototipo: `isSoundName('toString')` devolvía `true`. Inofensivo mientras todos los llamantes tecleaban el nombre; nada inofensivo al validar red. - `warmSamples` sólo tenía una de las dos ramas que sí tiene el constructor, así que una app con canal de sonido propio se calentaba al arrancar y nunca más; y esa rama leía la preferencia UNA vez, en construcción. Las dos se caían por lo mismo: ahora comparten `warmUrls()` en vez de duplicarse. - El demo del catálogo cancela su fetch: elegir `inline` y saltar a `synth` antes de que llegara reinstalaba el pack sobre un `clearMap()` ya hecho. - `fallbackFor(name)` borra los dos casts que copiaba todo el que autorase un pack de ficheros. ## Documentación `architecture/sema.md` gana la sección de las dos formas de entrega y el párrafo de `preloadSamples` reescrito — describía un precalentamiento que ya no es el que ocurre. El README del arte conoce la ruta `data:`. La página de packs gana su sección equivalente y deja de señalar unos `.wav` sueltos como «el ejemplo». Y un fantasma: `soft` no existe — es un nombre del catálogo viejo que quedaba como ejemplo en ocho sitios, incluido el docstring de `applySounds`, cuyo ejemplo copiado literalmente LANZABA. `static/sounds/ui-inline.json` se genera desde `packs/ui-mp3.ts`; el comando está documentado ahí y reproduce el fichero byte a byte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
eidos.applyTheme({ color: '#3b5bdb', sound: { tick: { gain: 0.1 } } });
feat(eidos,sema): un tema es UNA cosa — siete ejes, y ninguno fuera del alcance Cierra el eje: el haptico entra al mapa, `masterGain` deja de descartarse, y `applyTheme` gana el septimo eje. ⚠️ CORRIGE MI PROPIA PROPUESTA. Dije que la puerta unica iba en la raiz de composicion. Al medirlo, la dependencia corre AL REVES: `ActiveEidos` tiene `#uix` y lo conduce (registra presets con `uix.motion`), mientras `ActiveUix` no tiene eidos. Un tema compuesto en la raiz habria invertido la direccion. Va donde ya vive el metodo que se llama a si mismo «the capstone composing them», y que hasta hoy componia seis de ocho. eidos.applyTheme({ color: '#3b5bdb', sound: { soft: { gain: 0.08 } } }); eidos.clearTheme(); // revierte los siete Un eje que pasas se aplica, uno que omites revierte — la misma lectura atomica que ya tenian los seis visuales, ahora sobre siete. Sin `uix` (render headless) el eje perceptual se salta y los visuales siguen: un tema que se aplica a medias es mejor que uno que revienta. EL HAPTICO, AL MAPA. Su tabla `kind → pattern` vivia como const dentro del canal, asi que era el unico eje perceptual que un tema no alcanzaba — exactamente la asimetria que el trabajo del sonido existia para quitar, dejada en pie un canal mas alla. Ahora es `SEMA_MAP.haptics`, con perfiles derivados (`floorMs` + `scale`/`gapMs`) o constantes (`pattern`): los tres kinds evaluativos son ritmos constantes porque lo que hace reconocible a un `error` es su cadencia, no su longitud. El canal lo lee por GETTER, no por copia, asi que un `applyMap()` en caliente le llega sin re-registrarlo. `masterGain` SE APLICA. Pasarlo con motor compartido se tiraba en silencio (S-05/S-32) pese a ser una perilla viva del motor y la forma natural de poner el volumen desde la raiz. Ahora llega a `master.setGain`, y de las opciones que de verdad no pueden aplicarse a un motor ajeno se AVISA en vez de desaparecer. Y `DeltaValue` pasa a exportarse: es la forma de toda semilla de override, sin ella un consumidor no puede tipar el tema que esta pasando. Su ausencia era un error VIVO en el estudio de sema, que lo importaba y no podia — por eso check BAJA a 73 desde el baseline de 75. VERIFICADO: theming 8/8 (incluye re-vocear el haptico y que la tabla autorada quede intacta) · sema+eidos+morfo 802/802 · check 73 (baseline 75, −2 por el export) · docs:check 0/618 · suite completa 7 fallos, los mismos ajenos y preexistentes de contracts.test. ⚠️ Nada de esto se ha OIDO ni TOCADO en un dispositivo real: todo esta medido con el resolver y las suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
eidos.clearTheme(); // reverts all seven, visual and perceptual
```
It lives on `ActiveEidos` and not on the composition root, which corrects the
first reading of this section: **the dependency runs eidos → uix**, not the
other way. `ActiveEidos` already holds `uix` and drives it (it registers motion
presets through `uix.motion`); `ActiveUix` does not hold eidos, so a composed
theme could not live there without inverting the direction. An axis you pass is
applied, an axis you omit reverts — the same atomic reading the six visual axes
already had, now over seven. Without a `uix` (a headless render) the sound axis
is skipped and the visual ones still apply: a theme that half-applies beats one
that throws.
**And the vocabulary is complete**: the haptic `kind → pattern` table moved into
the map too (`haptics.success.pattern`), so no perceptual axis is left that a
theme cannot reach. Leaving haptic behind would have swapped one asymmetry for
another.
`masterGain` is applied as well: passing it to a shared engine used to be
dropped in silence (audit S-05 / S-32); it now reaches `engine.master.setGain`,
and the options that genuinely cannot apply to a shared engine are warned about
instead of vanishing.
docs(sema,theming): el orden real, como se autora cada canal, y el hueco del theming Tres cosas que el cambio del sonido dejo sin plasmar. 1. EL ORDEN CANONICO MENTIA. La numeracion 1·2·3·4·5a·5b esta citada como canonica en sema.md, resolver.ts y engine.ts, y despues del reorden era falsa para `sound`: un pack ya no autora en 5a, lo hace en 1.5. Corregido en los tres sitios a la vez, con el porque — todo lo que AUTORA un sonido corre ahi conservando su precedencia relativa; solo cambio su posicion respecto al intent. El resto de la regla (channels/haptic/hold) mantiene el «gana el ultimo» porque no son ejes evaluativos. 2. COMO SE AUTORA CADA CANAL (CANON §7). El reparto por dueño estaba escrito; lo que un componente ESCRIBE, no. Tabla nueva: el canal visual no se escribe (se declara el evento y eidos reacciona), `sound` es UN NOMBRE, `haptic` es un `kind`. La regla es la misma en las tres filas y ese es el punto: un componente dice QUE ocurre, nunca cuan fuerte, cuan brillante ni cuanto dura. El sonido era la excepcion hasta ayer. 3. EL ANALISIS DEL THEMING que el autor pidio (theming/channels.md §5b). Lo que el cambio NO toco: nada de eidos. El canal visual se proyecta estampando data-event-*, y esa ruta quedo intacta — ni una receta, ni un token, ni un selector. Eidos no sabe que existe el sonido y no le hizo falta. Lo que cambio fue DONDE se autora (un catalogo en vez de 71 ficheros) y CUANDO se aplica (antes del intent), ambos dentro de sema. Lo que si cambio, y es lo util: `applyTheme(seed)` retunea SEIS ejes visuales (color·type·depth·shape·space·gradient) y el sonido tiene CERO. Antes esa asimetria se justificaba sola — con 214 reglas autorando 33 firmas a mano no habia objeto que retunear. Hoy hay exactamente uno: 16 nombres en un const. Un eje `sound` en ThemeSeed seria la misma forma que los otros seis. Y las tres puertas de personalizacion, medidas, ninguna llega al catalogo: `overrides.runtime` recorre rutas de SEMA_MAP y el catalogo no esta en el mapa (ademas se traga las erratas, S-09); `overrides.cascade` es por selector, no un tema; `masterGain` se descarta en SILENCIO (S-05/S-32). Un producto puede silenciar y puede pisar una ocurrencia, pero NO puede re-voceear el sistema. Queda escrito en vez de ser folclore. ⚠️ chronos: `src/uix/sema/components/chronos.ts` entro en la migracion de `769e426c5` pese a ser de escritura excluida. Era forzoso — con el tipo estrechado, dejarlo sin migrar rompe la compilacion del arbol entero — y el cambio es mecanico (soundTuning(...) -> nombre), sin decision de diseño. Su SPEC.md NO se ha tocado y sigue citando `soundTuning('commit.soft')`. VERIFICADO: sema+morfo+sound 437/437 · check 75 · docs:check 0/618. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 6. Position vs the references
No reference system gathers the channels under **one semantic model**.
Material has shape morph + motion (ad-hoc, without integrated
sound/haptics); Apple, continuity + materials (platform-bound); everyone,
color. **UIX gathers them** — the visual channel (sema → eidos) + sound +
haptic (sema) — over the two-moment model, fired from one event, with
runtime builders and an open registry. That is the framework, not the sum of
its parts.
## 7. Demo
`/temas/orquesta` — the mixer: press a control and mute each facet to hear
its part. The **visual channel's** 4 facets materialize with eidos tokens
(toggleable so each can be muted one by one); **sound + haptic** are fired by
the real sema engine (`EngineSemantic.emit`, filtered channels). One event,
sema's channels.
docs: la doctrina alcanza a los cambios de la jornada — cinco backlogs y una llave de mas Auditoria de deriva doc<->codigo sobre los 33 commits del 2026-08-13/14: cinco sitios seguian describiendo lo que el codigo dejo de hacer ayer. Se corrigen COMO BITACORA, al final de cada fichero (el cuerpo es el acta de lo que se firmo, no el estado del codigo; reescribirlo falsearia la firma). El formato es el que ya existia en `arts/adom` y `soma/textarea`: `## Backlog`. - `decisions/book-deviations.md` — dos entradas. D.7 nombraba `IntentExpectedFamily`, tipo que M6 (13246a2c2) borro, y describia una derivacion de DOS cubos que S-33 (1a174d5a6) hizo positiva y TRIPLE. Y el caveat SEM-4 de tree-view/tree-grid nombraba `targetOverride` en presente: b8aa333fd lo borro, hoy se ancla por identidad (`partInstance`). - `theming/channels.md` — decia «built-in opt-in channel» de announce. S-19(ii) (23b20fff5) lo encendio por defecto en las DOS raices; ese commit actualizo `sema.md` y se dejo este atras. - `architecture/morfo.md` — el §semaSelector no nombraba los dos matchers que M5 (b53c93e42) anadio: `state` tipado contra `DataPairOf<M>` y el escape `undeclaredState`, que LANZA si el attr resulta estar declarado — es lo que mantiene obligatorio el tipado. - READMEs de virtual-list / virtual-grid — su columna informal «Emitted? NO» precede al campo del contrato. c73cae022 hizo que el morfo diga QUIEN dispara: `emission: 'host'`. La conducta no cambia; cambia su estatus. Ademas, `architecture/sema.md`: `{{ announce: false }}` -> `{ announce: false }`, llave doblada que introduje ayer en 23b20fff5. Verificado: docs-check 0/625 · prettier sin regresion (los 6 ficheros que avisan ya avisaban en HEAD, medido con stash). Los ficheros de blocks (`blocks.md`, `AUDIT-blocks-ledger.md`, `CONTINUE-blocks.md`, `PLAN-blocks.md`) son de la otra sesion y quedan fuera. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Backlog / Evolution decisions
### 2026-08-13 — `announce` is ON by default in both composition roots
Supersedes the wording in §2 above («`announce` joined on 2026-07-04 as a
built-in **opt-in** channel»). Opt-in described the engine option; it is no
longer what an app gets.
S-19(ii) (`23b20fff5`) wires the channel in `createActiveUix` AND
`attachActiveUix`, default ON. Announce is SUBSTITUTION, not ornament, so the
a11y announcer ships ambient — the field norm (Angular CDK `LiveAnnouncer`,
React Aria). The root is the only place that holds both ends (the shared sink
and the engine), and it registers the channel post-construction with a
late-bound closure, because `uix.announce` does not exist yet at
engine-construction time; any wiring an app wrote itself fell back to the
channel's self-owned regions — a SECOND live `role=status` / `role=alert`
pair, the thing this chapter forbids.
Opt out with `events: { announce: false }`; an explicitly passed `announce`
wins — the root never overwrites an existing channel. Detail:
[`architecture/sema.md`](../architecture/sema.md).

Powered by TurnKey Linux.