docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
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).
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 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** .
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **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
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
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`) |
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
| **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
`haptic` are the three that express; `announce` is the one that substitutes.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **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).
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 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
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
`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.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
(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`
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
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(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
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** |
**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
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 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.