feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
# CONTINUE — el hilo del audio: REDISEÑO en curso (`$sound` = orquestador)
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
fix(slider,media-player): el anillo del commit no enmarca contenedores, y saltar al final no es completar
Cuatro defectos que solo aparecieron cuando el usuario MIRO la pagina.
1) EL BORDE DE LOS SLIDERS ("se viene arrastrando desde el principio").
No era del arquetipo ni del slider: es la firma GLOBAL de la familia
commit. BUILTIN_SIGNATURES.commit monta commit-settle, cuyo 30% pinta
box-shadow 0 0 0 3px. El commit-set del Slider apunta al PROVIDER, asi que
cada paso de flecha y cada release dibujaban un aro alrededor de la caja
entera (medido en vivo: 544x40). Un aro lee como feedback en un boton y
como borde parasito en un rail ancho y transparente. Exencion acotada en
slider.css — el patron que gradient-builder.css ya practicaba. Alcanza a
todo slider embebido (waveform, media-player, color-picker, time-picker)
porque todos llevan [data-slider]. `animation` es la palanca, no
`box-shadow`: las declaraciones de keyframe ganan a las normales, asi que
solo reiniciar el NOMBRE de la animacion detiene la pintura.
2) EL MISMO ANILLO EN EL PLAYER: commit-complete / commit-fail apuntan al
provider -> borde alrededor de los ~700px del reproductor. Misma exencion
en media-player.css. Los botones conservan su aro pequeno (20x36), que es
donde el feedback pertenece.
3) SALTAR AL FINAL ANUNCIABA UNA COMPLECION FALSA. Los botones de salto no
tienen evento propio: seek() delega a proposito (D-AP2.13, "el release del
scrubber ya ES el commit-set del Slider") — cierto para el scrubber y
VACIO para los botones, que no tienen Slider detras. Al saltar mas alla del
final el elemento dispara `ended` -> commit-complete en el provider ->
sonido (es audible por D-AP2.7) + anillo, mientras el boton de retroceso
quedaba mudo. Saltar al final no es completar: es abandonar la obra en el
final. seek()/scrubTo() marcan el salto que aterriza en el final y `ended`
no anuncia; un `play` posterior limpia la marca, asi que el final natural
se sigue anunciando. Los dos botones quedan simetricos y mudos, que es la
doctrina del transporte.
4) MEDIASESSION SIN OVERLAY DEL SO: el modo audio YA funcionaba (metadata
completa, medida en vivo); el roto era VIDEO, el modo por defecto de la
demo. <AudioPlayer> deriva metadata de title/artist/artwork; el
<MediaPlayer> compositivo no puede leer su hijo Title sin volverse
data-driven, asi que la declara el consumidor — y la demo no lo hacia.
El arreglo es de la demo; el art se comporta como D-SR.6/D-AP2.5 lo firmo.
Guard nuevo (no anunciar compleccion al saltar al final; si al final
natural) VISTO EN ROJO antes de darlo por bueno.
Nota de doctrina, tras leer el corpus a peticion del usuario: la exencion
acotada en la receta ES la puerta, no un olor. motion.md §4 fija que la
firma es generica por decision estructural ("un commit asienta igual en
todo el sistema"), eidos es el UNICO dueno del canal visual, los packs de
sema solo transportan sound/haptic, y el VisualChannel no consulta
activeChannels — no hay palanca declarativa para callar solo lo visual.
Queda anotado, menor: commit-settle escribe 3px a pelo donde sus hermanos
announce-pulse-* usan var(--focus-ring-width).
Verificado: check 75 = baseline, 0 propios; 11/11 media-player;
motion.test.ts verde (la firma global intacta); sonda en Chrome vivo —
slider real -> animation none, elemento no-slider con el mismo sello ->
sigue commit-settle, aro de foco intacto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **Kickoff para la sesión siguiente**: _"Lee `docs/process/CONTINUE-sound-engine.md`
> y sigue por donde toque."_
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
> **Fecha**: 2026-07-31 (tercera sesión) · Rama `alpha-0.1-sec-dom`.
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
## Lo que pasó el 2026-07-31 — giro de raíz
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
Se presentó el gate del reproductor (D-AP.1…13) y el usuario lo **rechazó de
raíz**: el sistema de sonido es un frankenstein — nació de los diseños de otros
módulos (sema, media-player) y se parchea en función de lo que había, en vez de
partir de su función: **orquestador de TODOS los aspectos del sonido** , como
`motion` / `timers` / `scene` . Los consumidores se adaptan al servicio, no al
revés. El rediseño se hizo clean-room y su gate quedó **FIRMADO** el mismo día
(aprobación del plan + D-SR.4/D-SR.7 explícitas).
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Estado
fix(slider,media-player): el anillo del commit no enmarca contenedores, y saltar al final no es completar
Cuatro defectos que solo aparecieron cuando el usuario MIRO la pagina.
1) EL BORDE DE LOS SLIDERS ("se viene arrastrando desde el principio").
No era del arquetipo ni del slider: es la firma GLOBAL de la familia
commit. BUILTIN_SIGNATURES.commit monta commit-settle, cuyo 30% pinta
box-shadow 0 0 0 3px. El commit-set del Slider apunta al PROVIDER, asi que
cada paso de flecha y cada release dibujaban un aro alrededor de la caja
entera (medido en vivo: 544x40). Un aro lee como feedback en un boton y
como borde parasito en un rail ancho y transparente. Exencion acotada en
slider.css — el patron que gradient-builder.css ya practicaba. Alcanza a
todo slider embebido (waveform, media-player, color-picker, time-picker)
porque todos llevan [data-slider]. `animation` es la palanca, no
`box-shadow`: las declaraciones de keyframe ganan a las normales, asi que
solo reiniciar el NOMBRE de la animacion detiene la pintura.
2) EL MISMO ANILLO EN EL PLAYER: commit-complete / commit-fail apuntan al
provider -> borde alrededor de los ~700px del reproductor. Misma exencion
en media-player.css. Los botones conservan su aro pequeno (20x36), que es
donde el feedback pertenece.
3) SALTAR AL FINAL ANUNCIABA UNA COMPLECION FALSA. Los botones de salto no
tienen evento propio: seek() delega a proposito (D-AP2.13, "el release del
scrubber ya ES el commit-set del Slider") — cierto para el scrubber y
VACIO para los botones, que no tienen Slider detras. Al saltar mas alla del
final el elemento dispara `ended` -> commit-complete en el provider ->
sonido (es audible por D-AP2.7) + anillo, mientras el boton de retroceso
quedaba mudo. Saltar al final no es completar: es abandonar la obra en el
final. seek()/scrubTo() marcan el salto que aterriza en el final y `ended`
no anuncia; un `play` posterior limpia la marca, asi que el final natural
se sigue anunciando. Los dos botones quedan simetricos y mudos, que es la
doctrina del transporte.
4) MEDIASESSION SIN OVERLAY DEL SO: el modo audio YA funcionaba (metadata
completa, medida en vivo); el roto era VIDEO, el modo por defecto de la
demo. <AudioPlayer> deriva metadata de title/artist/artwork; el
<MediaPlayer> compositivo no puede leer su hijo Title sin volverse
data-driven, asi que la declara el consumidor — y la demo no lo hacia.
El arreglo es de la demo; el art se comporta como D-SR.6/D-AP2.5 lo firmo.
Guard nuevo (no anunciar compleccion al saltar al final; si al final
natural) VISTO EN ROJO antes de darlo por bueno.
Nota de doctrina, tras leer el corpus a peticion del usuario: la exencion
acotada en la receta ES la puerta, no un olor. motion.md §4 fija que la
firma es generica por decision estructural ("un commit asienta igual en
todo el sistema"), eidos es el UNICO dueno del canal visual, los packs de
sema solo transportan sound/haptic, y el VisualChannel no consulta
activeChannels — no hay palanca declarativa para callar solo lo visual.
Queda anotado, menor: commit-settle escribe 3px a pelo donde sus hermanos
announce-pulse-* usan var(--focus-ring-width).
Verificado: check 75 = baseline, 0 propios; 11/11 media-player;
motion.test.ts verde (la firma global intacta); sonda en Chrome vivo —
slider real -> animation none, elemento no-slider con el mismo sello ->
sigue commit-settle, aro de foco intacto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Iniciativa | Estado
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| [`PLAN-sound-engine.md` ](./PLAN-sound-engine.md ) | ✅ CERRADA (histórica). Su §16.1 («la voz es del art») queda **REVOCADA** por el rediseño
| [`PLAN-sound-redesign.md` ](./PLAN-sound-redesign.md ) | ✅ **COMPLETA (las dos entregas, 2026-07-31) — sin commitear.** E1: F0– F2 + escucha A/B del usuario («suena igual»). E2: F3 (media citizenship: `sound.media(el)` provee el transporte con la forma del puerto `MediaProvider` + foco + `duckUiWhileContent` + visibilidad content-aware + `attach()` opt-in) · F4 (MediaSession de la fuente activa) · F5 (factoría con `EngineSoundPolicyOptions` ; `contracts.ts` y estudio sin tocar, declarado). **AUDITADA (§9 del plan): AU-1…AU-9 resueltas** — duck ahora PERSISTENTE (estado derivado, holder = ducker más reciente; test nacido en rojo) · guards post-dispose · re-registro idempotente por elemento · drift guard soma↔art · bundle medido (16,2 KB min / 5,6 KB gz) · 3 guards vistos fallar por mutación. **28 suites / 305 tests verdes · check 73 = baseline, 0 propios · docs:check 0 · prettier limpio en lo propio.** Pendientes que exigen una página consumidora (llegan con el re-plan del player): medición Chrome del escenario mixto · prueba manual de teclas de medios
| [`PLAN-audio-player.md` ](./PLAN-audio-player.md ) | 🗄️ ARCHIVO (hechos + análisis que el v2 referencia)
| [`PLAN-audio-player-v2.md` ](./PLAN-audio-player-v2.md ) | ✅ **GATE FIRMADO 2026-07-31** («ok, continua»; H-9 = CABLEAR · D-AP2.7 = solo duck global). **F1 ✅ · F2 ✅ COMPLETAS (2026-07-31; morfo 8/8 · player 18/18 · check 74=baseline 0 propios)** . Detalle F2 en la fila del plan; el commit de rate cabalga `ratechange` (patrón G-5 de nacimiento). ⚠️ `translations:check` BLOQUEADO POR AJENO (`langs/components/palabras.ts` con `url:` rompe el stripper del script; palabras = excluido, no se toca) — correr cuando se arregle. **F3 ✅ COMPLETA (2026-07-31; player 10/10 · morfo 8/8 · check 74=baseline viva, 0 propios)** — detalle en la fila F3 del plan: transporte por el servicio como default (pin con motor REAL) · `metadata` público (opts+types+wrapper) · `nativeMediaProvider` retirado (media-provider.ts = contrato puro; drift-guard borrado) · G-5 aplicado · `formatDuration` · 6 sub-providers+wrappers+exports · README (tablas al día). **F4 ✅ (docblock del pack = doctrina D-AP2.7: el silencio lo posee el servicio vía `duckUiWhileContent`, receta escrita; la resta-de-gain v1 queda diseñada SIN cablear a propósito; stamp en navegador → F7). F5 EN CURSO — buffer-swap ✅ COMPLETO (29/29 · check 74=baseline 0 propios · eidos-lint limpio)** : el `TimeSlider` soma pasa `secondaryValue={bufferedTime}` y monta `Slider.SecondaryRange` ; el div `.mp-buffered` + `--media-buffered` + `--_mp-buffered` + `--_mp-rail` RETIRADOS (css/provider/test/morfo-docblocks/READMEs al día); el look se conserva (42% → `--slider-secondary-bg` en scope). **F5 ✅ COMPLETA** (detalle en la fila F5 del plan: 6 wrappers + AudioLayout + `<AudioPlayer>` con metadata derivada + 4 variantes + tokens; 37/37 contratos · lint 0 inválidos · check 78 = baseline+4 ajenos NUEVOS de `arts/connection` , 0 propios). **D-AP2.7 v2 COMMITEADA (`85b269f74`) + fix composición demos (`63f20f345`: el pack NO estaba en la lista del layout — packs sema son OPT-IN por app) + AMPLIADA a sliders por escucha real (`5d2816a0c`: pick/drag → `channels:['haptic']` por descendientes — estructurales sin intent, tirar su canal no borra carga evaluativa): el transporte del player es SILENCIOSO por defecto** — pack D.5 (resta de gain; `contact.silent` nuevo en sounds.ts) sobre commits de botones + descendientes (contact de Buttons, commit-set de Sliders); `commit-fail` /`commit-complete` audibles; `handle-drag` conserva su resolver por mecanismo (declarado); voz reactivable en capa 5b. **F7-construible ✅ COMMITEADO en `c4d64ba98`** (live badge localizado vía soma · demo con `<AudioPlayer>` + selector de variante + snippet en paridad · README eidos §modo-audio · `component:audit --only media-player` PASS · check 74 = baseline, 0 propios). `smoke` +`perm:check` corridos (2026-07-31): **todas las rutas del hilo PASS** (301/305; los 4 rojos son ajenos — ethereal·heroscrolling·dome-gallery·card, hipótesis: los M vivos de tipografía de otra sesión — y combobox/stepper en perm); la demo del player sin `data-perm-step` aún (opt-in para el dossier). **Waveform: gate D-WF.1…8 FIRMADO («ok») · F1– F5 ✅ HECHAS — F4+F5 el 2026-08-01, SIN commitear** (F1– F3 en `0ab6cc822` ; F4 eidos completo: wrapper+Wave+recipe+tokens `waveform:{height-xs…xl, bar-width, color, color-played, playhead-width}` — `playhead-width` añadido sobre la lista firmada, declarado — + guards visual-attrs/lint-allowlist; F5: demo `/uix/components/waveform` con picos REALES de `/sounds/*.wav` vía `decode` +`peaks`, READMEs soma+eidos, `component:audit --only waveform` PASS, `docs:check` 0; **fix F3 declarado** : `dir` no se reenviaba al Slider embebido — completado y verificado en vivo. Detalle en las filas F4/F5 del plan). **QUEDA F6 mirada** (ojos del usuario; la sonda en el 5173 vivo verificó estructura/cascada/interacción, pero el panel oculto no composita → sin captura). Los packs sema de waveform NO existen (delegated ✓ nada que registrar). **QUEDAN — y todos exigen ojos o la máquina del usuario** : F6 verificación VISUAL de las 4 variant
refactor(sema): los 3 resolvers de gesto se retiran — esta vez desde el eje, y el registro se hace verdad
El rediseno del 2026-08-06 (gestos por REPETICION, sound:'step' por emision)
los dejo sin llamador, y book-deviations D.7 escribio en pasado un borrado que
nunca ocurrio. Hoy ocurre, firmado y desde una sesion del eje que empezo por
su handoff — la condicion que la retirada revertida de 5571f3bf7 se salto.
Fuera: los 3 resolvers + DragSoundParams + el SoundContour de sema (el
homonimo del art queda solo, 5 valores) + clamp/lerp huerfanos + 5 re-exports
+ los 2 tests que solo se probaban a si mismos. El registro (SEMA_SOUND_VOICE,
sample, resolveSoundDefinition) intacto.
Adjudicados los 8 sitios que los describian como vigentes: los 5 de la ficha
D10, el sexto del re-analisis (sema.md §per-emit payload, reescrito a la
verdad — el payload es un NOMBRE mas la curva haptica) y DOS que la ficha no
listaba, hallados al leer el eje (la fila de estado de CONTINUE-sound-engine y
D-AP2.7 del gate v2), ambos con nota fechada. La tabla de corte del plan
cerrado queda anotada y D.7 gana la nota de que su pasado se hizo verdad hoy.
AUDIT-sema-2026-08-05 no se toca: es cronica por cabecera propia.
sema+sound 333/333 · check 69 (base 70 menos el probe scratch de adom borrado
en fase 0) · docs:check 0/623 · prettier limpio en lo propio con eol auto
(motion/types y splitter-provider ya fallaban en HEAD; no se reformatean).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> ⚠️ **2026-08-12** — donde la fila del plan v2 dice «`handle-drag` conserva su
> resolver por mecanismo (declarado)», quedó rancio el 2026-08-06: el rediseño
> del sonido pasó los gestos a REPETICIÓN (`sound: 'step'` por emisión) y los 3
> resolvers quedaron sin llamador; hoy están RETIRADOS
> ([`AUDIT-docs-code-ledger.md` §D10](./AUDIT-docs-code-ledger.md)). El
> silencio por canal de los sliders del player (D-AP2.7 v2) no cambia.
docs(process): handoff del RTL — el Slider esta roto y es la prioridad de manana
Nace CONTINUE-player-rtl.md con lo que la MIRADA del usuario abrio el
2026-08-01 y quedo sin cerrar, mas el enlace cruzado desde el handoff del
hilo de audio.
Lo que lleva:
1. PRIORIDAD — el Slider esta roto en RTL, confirmado por el usuario con el
componente SUELTO. No es del media-player: es el canonico del que cuelgan
waveform, media-player, color-picker y los dos time-pickers. Se deja
descartado por lectura que getValueFromPointer y rangeStyle estan bien en
aislamiento, y se deja una HIPOTESIS comprobable en dos minutos: el Slider
estampa `dir` como atributo HTML, lo que activa `direction: rtl` en CSS, y
slider.css mezcla propiedades logicas con los estilos fisicos en linea que
calcula el provider — doble volteo. El arreglo sera elegir UN dueno de la
inversion.
2. La disposicion de los botones del player, que el usuario insiste en que es
un problema APARTE. Las sondas de la sesion decian que mirroriza, pero esas
sondas fallaron varias veces ese dia (dos midieron video creyendo audio):
quedan marcadas como NO fiables.
3. Iconos de seek: decision tomada (voltearlos, con su razon), sin aplicar.
4. La medicion del escenario mixto quedo a medias y se explica por que: se
instrumento DESPUES del arranque, asi que el "0 contextos" no prueba lo que
parecia. Lo que si queda establecido es que la media no abre ningun
AudioContext.
5. Captions y el cableado del waveform, con su diagnostico completo.
Y las lecciones de metodo del dia, que son el motivo de que este documento
exista: medir el contrato NO es verificar la experiencia (se afirmo tres veces
lo contrario y las tres las caza el usuario); el panel embebido no sirve para
nada visual (innerWidth 0 — llego a inventar tres "overflow" inexistentes);
comprobar que el control se pulso de verdad antes de creerse la medida; la
distancia RGB es ciega al croma; y la FORMA manda sobre el color.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **2026-08-01 — hay un handoff hermano.** Lo que la mirada del usuario abrió ese
> día y quedó SIN cerrar (el **Slider roto en RTL**, la disposición de los botones
> del player, los iconos de seek, la medición del earcon mal hecha, `Captions` y
> el cableado del waveform) vive en
> [`CONTINUE-player-rtl.md`](./CONTINUE-player-rtl.md). **Empezar por ahí.**
fix(slider,media-player): el anillo del commit no enmarca contenedores, y saltar al final no es completar
Cuatro defectos que solo aparecieron cuando el usuario MIRO la pagina.
1) EL BORDE DE LOS SLIDERS ("se viene arrastrando desde el principio").
No era del arquetipo ni del slider: es la firma GLOBAL de la familia
commit. BUILTIN_SIGNATURES.commit monta commit-settle, cuyo 30% pinta
box-shadow 0 0 0 3px. El commit-set del Slider apunta al PROVIDER, asi que
cada paso de flecha y cada release dibujaban un aro alrededor de la caja
entera (medido en vivo: 544x40). Un aro lee como feedback en un boton y
como borde parasito en un rail ancho y transparente. Exencion acotada en
slider.css — el patron que gradient-builder.css ya practicaba. Alcanza a
todo slider embebido (waveform, media-player, color-picker, time-picker)
porque todos llevan [data-slider]. `animation` es la palanca, no
`box-shadow`: las declaraciones de keyframe ganan a las normales, asi que
solo reiniciar el NOMBRE de la animacion detiene la pintura.
2) EL MISMO ANILLO EN EL PLAYER: commit-complete / commit-fail apuntan al
provider -> borde alrededor de los ~700px del reproductor. Misma exencion
en media-player.css. Los botones conservan su aro pequeno (20x36), que es
donde el feedback pertenece.
3) SALTAR AL FINAL ANUNCIABA UNA COMPLECION FALSA. Los botones de salto no
tienen evento propio: seek() delega a proposito (D-AP2.13, "el release del
scrubber ya ES el commit-set del Slider") — cierto para el scrubber y
VACIO para los botones, que no tienen Slider detras. Al saltar mas alla del
final el elemento dispara `ended` -> commit-complete en el provider ->
sonido (es audible por D-AP2.7) + anillo, mientras el boton de retroceso
quedaba mudo. Saltar al final no es completar: es abandonar la obra en el
final. seek()/scrubTo() marcan el salto que aterriza en el final y `ended`
no anuncia; un `play` posterior limpia la marca, asi que el final natural
se sigue anunciando. Los dos botones quedan simetricos y mudos, que es la
doctrina del transporte.
4) MEDIASESSION SIN OVERLAY DEL SO: el modo audio YA funcionaba (metadata
completa, medida en vivo); el roto era VIDEO, el modo por defecto de la
demo. <AudioPlayer> deriva metadata de title/artist/artwork; el
<MediaPlayer> compositivo no puede leer su hijo Title sin volverse
data-driven, asi que la declara el consumidor — y la demo no lo hacia.
El arreglo es de la demo; el art se comporta como D-SR.6/D-AP2.5 lo firmo.
Guard nuevo (no anunciar compleccion al saltar al final; si al final
natural) VISTO EN ROJO antes de darlo por bueno.
Nota de doctrina, tras leer el corpus a peticion del usuario: la exencion
acotada en la receta ES la puerta, no un olor. motion.md §4 fija que la
firma es generica por decision estructural ("un commit asienta igual en
todo el sistema"), eidos es el UNICO dueno del canal visual, los packs de
sema solo transportan sound/haptic, y el VisualChannel no consulta
activeChannels — no hay palanca declarativa para callar solo lo visual.
Queda anotado, menor: commit-settle escribe 3px a pelo donde sus hermanos
announce-pulse-* usan var(--focus-ring-width).
Verificado: check 75 = baseline, 0 propios; 11/11 media-player;
motion.test.ts verde (la firma global intacta); sonda en Chrome vivo —
slider real -> animation none, elemento no-slider con el mismo sello ->
sigue commit-settle, aro de foco intacto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Defectos que la MIRADA del usuario encontró (2026-08-01) — arreglados
La verificación visual F6 destapó cuatro cosas que ninguna sonda había visto. Se
arreglaron el mismo día; todas verificadas, **el guard de cada una nacido en rojo** .
1. **El anillo alrededor de los sliders** («se viene arrastrando desde el
principio»). NO era del arquetipo ni del slider: es la **firma global de la
familia `commit` ** — `BUILTIN_SIGNATURES.commit` (`lib/motion/presets/css.ts`)
monta `commit-settle` , cuyo 30% pinta `box-shadow: 0 0 0 3px` . El `commit-set`
del Slider apunta al **provider** , así que el anillo enmarcaba la caja entera
(medido: 544× 40). Un aro lee como feedback en un botón y como borde parásito en
un raíl ancho. **Arreglo** : exención acotada en `slider.css` , el patrón que
`gradient-builder.css` ya practicaba. Alcanza a todo slider embebido
(waveform, media-player, color-picker, time-picker) porque todos llevan
`[data-slider]` .
2. **El mismo anillo en el media-player** : `commit-complete` / `commit-fail`
apuntan al provider → borde alrededor de los ~700px del reproductor. Misma
exención en `media-player.css` . Los botones conservan su aro pequeño (20× 36),
que es donde el feedback pertenece.
3. **El botón de saltar al final anunciaba una compleción FALSA.** Los botones de
salto no tienen evento propio (D-AP2.13 delega en el `commit-set` del
scrubber — cierto para el scrubber, **vacío para los botones** , que no tienen
Slider detrás). Al saltar más allá del final el elemento dispara `ended` →
`commit-complete` en el provider → sonido (es audible por D-AP2.7) + anillo.
El de retroceso quedaba mudo: asimetría audible. **Doctrina** : saltar al final
NO es completar, es abandonar la obra en el final. `seek()` /`scrubTo()` marcan
el salto que aterriza en el final y `ended` no anuncia; un `play` posterior
limpia la marca, así que el final natural sí se sigue anunciando.
4. **MediaSession sin overlay del SO** : el modo **audio ya funcionaba**
(metadata completa, medida en vivo); el roto era **vídeo** , que es el modo por
defecto de la demo. `<AudioPlayer>` deriva su metadata de title/artist/artwork;
el `<MediaPlayer>` compositivo no puede leer su hijo `Title` sin volverse
data-driven, así que la declara el consumidor — y la demo no lo hacía.
**Lección de método**: se propuso al usuario cambiar el keyframe `commit-settle`
(doctrina) SIN haber leído el corpus, y el usuario lo paró: _«es una temeridad
plantear esto sin haber hecho la lectura de todo el sistema»_. La lectura después
lo desmintió — `motion.md` §4 fija que la firma es genérica por decisión
estructural («un `commit` asienta igual en todo el sistema»), eidos es el ÚNICO
dueño del canal visual, los packs de sema solo transportan `sound` /`haptic`, y el
`VisualChannel` no consulta `activeChannels` : **la regla acotada en la receta ES
la puerta**, no un olor. Lo único que queda en pie de aquel informe es una
observación menor: `commit-settle` escribe `3px` a pelo donde sus hermanos
`announce-pulse-*` usan `var(--focus-ring-width)` .
Precedente relacionado que YA estaba decidido y no se había leído: `VirtualList`
/ `commit-set-resize` lleva ** `channels: []` ** (`design-chat-block.md`) — el
override de morfo que salta proyección, dispatch y hold, para eventos que son
contabilidad estructural y no gesto del usuario. Está cableado en
`engine.ts` (no está latente); lo latente es el silenciado PARCIAL (callar lo
visual conservando el sonido), que hoy no tiene palanca declarativa.
feat(media-player): captions propias + el eje de dirección, canonizado
Dos hilos que comparten ficheros, así que van juntos.
## Captions — el player pinta las cues
La parte estaba declarada en el morfo desde el principio y nunca implementada.
El render nativo no servía: con la pista en mode='showing' el navegador dibuja
su caja de cues en bottom:0 del <video> — la misma franja de 49px que la barra
de controles — y contra el z-index:4 de la barra al 82% de opacidad llegaba el
18% del texto. ::cue no puede moverlo (la spec no admite propiedades de
posición ahí).
Ahora la pista se mantiene en 'hidden': la spec deja la pista activa —cues
parseadas, activeCues mantenida, cuechange disparándose— y el navegador no
pinta nada, así que el texto se monta en la parte Captions, por encima de la
barra. Es el movimiento de Plyr, y por el mismo motivo.
Tres trampas de la especificación, encaradas por escrito:
- Pasar una pista a 'disabled' desactiva sus cues SIN disparar cuechange. Nadie
avisa de que hay que borrar, así que se limpia a mano — si no, el último
subtítulo se queda congelado sobre el vídeo.
- Los eventos de cue no corren mientras está puesto el show-poster flag, así
que el primer pintado se siembra leyendo activeCues.
- cues/activeCues son null en 'disabled'.
De paso cierra un defecto latente: captionsOn era un espejo de una sola
dirección que nunca releía track.mode, así que si el SO activaba subtítulos
salían cues mientras el botón decía que estaban apagados. Ahora se DERIVA de
los modos reales, y una pista que el navegador encienda se adopta (degradada a
'hidden', para que no pinte encima de la nuestra).
Decisiones declaradas: el marcado inline se conserva (se monta el
DocumentFragment de getCueAsHTML como nodos, nunca por innerHTML — el sumidero
de XSS que abre Plyr al serializarlo de vuelta a cadena); la metadata de
posición (line/position/align/vertical/snapToLines/regiones) se DESCARTA. Esto
no es una implementación de WebVTT y el README lo dice.
## Dirección — el player estaba partido por la mitad
Con dir="rtl" por prop y sin dir ambiental, el cromo se disponía en LTR y sus
dos Sliders en RTL: el wrapper pasaba la prop cruda y sólo la reenviaba a los
hijos, sin resolver la cadena ni estampar. En la demo estaba enmascarado porque
el stage estampa dir además de pasar la prop.
Canonizado con el patrón de CONTINUE-direction.md §9.16: activeDir en el
wrapper, resolvedDir en el provider, dir crudo + data-dir resuelto en los
props. El censo del §9.15 no lo tenía — era un cuarto caso que su búsqueda no
cubre (acepta dir y no lo escribe en ningún sitio).
Y los glifos direccionales no se volteaban: en RTL el transporte se leía
▶▶ ▶ ◀◀, con las dos flechas exteriores apuntando HACIA DENTRO — el síntoma que
carousel.css ya documentaba. Se voltean con scale:-1 1 (no rotate:180deg: sólo
coinciden en glifos verticalmente simétricos, y rotar el PiP le llevaría el
recuadro a la esquina de arriba), anclado a data-dir, el atributo propio del
componente. Alcance elegido: seek, play, volumen y PiP; captions y fullscreen
NO. El teclado pasa a resolver por getDirectionalKeys, así que las flechas
concuerdan con el glifo y con el Slider embebido.
## Verificación
check 0 errores propios · vitest del player 14/14, con los tres guards nuevos
vistos en ROJO por mutación · eidos-lint invalid 0 (morfo-backed 25 → 29) ·
morfo:check sin fallos del player · component:audit PASS · smoke PASS ·
docs:check 0 · prettier limpio en lo propio. A ojo en Chrome real: claro y
oscuro, LTR y RTL, y las dos regresiones (el espejo del SO y el subtítulo
congelado).
Nota: el README de soma se lleva una línea ajena — la fila `dir` de la tabla de
props, del barrido de otra sesión, que describe justamente esta cadena.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
~~**Abierto, sin tocar**: los subtítulos nativos se solapan con la barra de
controles.~~ **CERRADO 2026-08-04.** El diagnóstico era correcto y el arreglo
fue el que predecía: la pista se mantiene en `mode='hidden'` —activa, con
`activeCues` y `cuechange` , pero sin pintado del navegador— y el texto se monta
en la parte `Captions` , que llevaba declarada desde el principio y nunca se
había implementado. Verificado a ojo en Chrome, claro y oscuro: el texto sale
**encima** de la barra con 8px de aire. El espejo de una sola dirección de
`captionsOn` cerró en el mismo arreglo. Detalle en el README de eidos del
media-player, §Captions.
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
## Qué es el rediseño (resumen; la verdad está en el plan)
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
`$sound` gobierna 7 dominios: sustrato (contexto único — se conserva, verificado)
· **mezcla** (master → buses `ui` /`content`; `prefs.sound` ↔ `ui` SOLO) ·
**voces** (calibración = dato del consumidor; sema registra la suya; default =
valores actuales, cero cambio audible) · **ciudadanía de media** (el servicio
PROVEE el transporte con la forma del puerto `MediaProvider` ) · política/foco
(`mixed`/`exclusive`/`duck`; `duckUiWhileContent` sustituye el parche D-AP.7) ·
percepción (`decode`; analyser/picos v2) · MediaSession (del servicio).
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## MediaSession — VERIFICADA POR EL USUARIO (2026-08-01) ✅
El pendiente que arrastraba tres sesiones («prueba manual de teclas de medios /
pantalla de bloqueo — cuando exista una página que consuma `sound.media()` »)
queda **CERRADO** . El usuario abrió el media hub de Chrome con la demo de audio
sonando y confirmó: **carátula + título + artista + transporte** , origen
`localhost:5173` .
Notas de la verificación, para no repetir el camino:
- **Las teclas de VOLUMEN no sirven** para esto: muestran el OSD de volumen de
Windows y nada más. En Windows 11 Microsoft retiró el panel de medios que
antes se pegaba a ese OSD. La superficie fiable es el **botón de nota musical
de la barra de Chrome** (solo existe mientras suenan medios), o las teclas de
Play/Pause / pista.
- **La sospecha de la carátula era infundada**: se temió que `artwork` sin
`sizes` /`type` y sin CORS (`media.w3.org` no manda
`Access-Control-Allow-Origin` ) hiciera que el `MediaImageManager` la
descartara. **Carga perfectamente.** No se tocó nada; queda registrado para
que nadie «arregle» un no-problema.
- **Anterior/siguiente pista salen apagados y es CORRECTO**: `setActive()`
registra `play` , `pause` y `seekto` — un reproductor de una sola fuente no
tiene lista, y declarar esos handlers mentiría sobre su capacidad. Los ±10s
del hub los sintetiza Chrome desde `seekto` .
- **Lección de método**: se afirmó «el audio ya funcionaba» apoyándose en una
medición JS (`navigator.mediaSession.metadata` poblada) como si zanjara lo que
el usuario ve. No es lo mismo: el contrato JS puede cumplirse y el overlay del
SO no aparecer (media muteada, modo equivocado, tecla equivocada). Una
medición del contrato NO es una verificación de la experiencia.
feat(media-player): captions propias + el eje de dirección, canonizado
Dos hilos que comparten ficheros, así que van juntos.
## Captions — el player pinta las cues
La parte estaba declarada en el morfo desde el principio y nunca implementada.
El render nativo no servía: con la pista en mode='showing' el navegador dibuja
su caja de cues en bottom:0 del <video> — la misma franja de 49px que la barra
de controles — y contra el z-index:4 de la barra al 82% de opacidad llegaba el
18% del texto. ::cue no puede moverlo (la spec no admite propiedades de
posición ahí).
Ahora la pista se mantiene en 'hidden': la spec deja la pista activa —cues
parseadas, activeCues mantenida, cuechange disparándose— y el navegador no
pinta nada, así que el texto se monta en la parte Captions, por encima de la
barra. Es el movimiento de Plyr, y por el mismo motivo.
Tres trampas de la especificación, encaradas por escrito:
- Pasar una pista a 'disabled' desactiva sus cues SIN disparar cuechange. Nadie
avisa de que hay que borrar, así que se limpia a mano — si no, el último
subtítulo se queda congelado sobre el vídeo.
- Los eventos de cue no corren mientras está puesto el show-poster flag, así
que el primer pintado se siembra leyendo activeCues.
- cues/activeCues son null en 'disabled'.
De paso cierra un defecto latente: captionsOn era un espejo de una sola
dirección que nunca releía track.mode, así que si el SO activaba subtítulos
salían cues mientras el botón decía que estaban apagados. Ahora se DERIVA de
los modos reales, y una pista que el navegador encienda se adopta (degradada a
'hidden', para que no pinte encima de la nuestra).
Decisiones declaradas: el marcado inline se conserva (se monta el
DocumentFragment de getCueAsHTML como nodos, nunca por innerHTML — el sumidero
de XSS que abre Plyr al serializarlo de vuelta a cadena); la metadata de
posición (line/position/align/vertical/snapToLines/regiones) se DESCARTA. Esto
no es una implementación de WebVTT y el README lo dice.
## Dirección — el player estaba partido por la mitad
Con dir="rtl" por prop y sin dir ambiental, el cromo se disponía en LTR y sus
dos Sliders en RTL: el wrapper pasaba la prop cruda y sólo la reenviaba a los
hijos, sin resolver la cadena ni estampar. En la demo estaba enmascarado porque
el stage estampa dir además de pasar la prop.
Canonizado con el patrón de CONTINUE-direction.md §9.16: activeDir en el
wrapper, resolvedDir en el provider, dir crudo + data-dir resuelto en los
props. El censo del §9.15 no lo tenía — era un cuarto caso que su búsqueda no
cubre (acepta dir y no lo escribe en ningún sitio).
Y los glifos direccionales no se volteaban: en RTL el transporte se leía
▶▶ ▶ ◀◀, con las dos flechas exteriores apuntando HACIA DENTRO — el síntoma que
carousel.css ya documentaba. Se voltean con scale:-1 1 (no rotate:180deg: sólo
coinciden en glifos verticalmente simétricos, y rotar el PiP le llevaría el
recuadro a la esquina de arriba), anclado a data-dir, el atributo propio del
componente. Alcance elegido: seek, play, volumen y PiP; captions y fullscreen
NO. El teclado pasa a resolver por getDirectionalKeys, así que las flechas
concuerdan con el glifo y con el Slider embebido.
## Verificación
check 0 errores propios · vitest del player 14/14, con los tres guards nuevos
vistos en ROJO por mutación · eidos-lint invalid 0 (morfo-backed 25 → 29) ·
morfo:check sin fallos del player · component:audit PASS · smoke PASS ·
docs:check 0 · prettier limpio en lo propio. A ojo en Chrome real: claro y
oscuro, LTR y RTL, y las dos regresiones (el espejo del SO y el subtítulo
congelado).
Nota: el README de soma se lleva una línea ajena — la fila `dir` de la tabla de
props, del barrido de otra sesión, que describe justamente esta cadena.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Escenario mixto — MEDIDO Y CERRADO (2026-08-04) ✅
El pendiente que arrastraba desde E2 («medición Chrome del escenario mixto —
earcon + media, 1 contexto») queda cerrado. Detalle completo en
[`CONTINUE-player-rtl.md` ](./CONTINUE-player-rtl.md ) §4; lo esencial:
- **Con media real sonando (sin mutear, vol 1) y 26 earcons encima: UN solo
`AudioContext` .** `createMediaElementSource` = 0 → la media nunca entra en el
grafo. 0 contextos cerrados, cero churn.
- **La receta que los handoffs prescribían no hacía falta.** No hay que
instrumentar antes del boot ni navegar client-side: eso resuelve el parche del
CONSTRUCTOR, que sólo sabe decir «¿se creó uno nuevo?». Se parchea el
**PROTOTIPO** (`createGain` / `createOscillator` / `resume` ) y se recoge el
`this` en un `Set` : enumera INSTANCIAS, así que un contexto preexistente
aparece igual. **Es el patrón a usar la próxima vez.**
- Fechado: el contexto nace en **t ≈ 4,1s tras la carga, sin gesto de usuario** ,
ya `running` . Y **no lo crea el player** — en `/uix/components/button` , sin
ningún elemento media, nace igual (t ≈ 4,08s). Es del arranque de la app.
⚠️ ~~**Hallazgo nuevo, de la app de demos y NO tocado**~~ **CABLEADO el
2026-08-06 (`d161cedcc`) y MEDIDO el 2026-08-12**: `+layout@.svelte` pasa ya un
`SemaPreferences` VIVO (`$state`) a `events.preferences` , y el toggle escribe
`semaPreferences.sound = 'full' | 'off'` además del intent de prefs. A/B en
navegador (parche de PROTOTIPO contando instancias, el patrón de abajo): click
con Sound ON → ** +2 osciladores** (el par del earcon); mismo click con Sound
OFF → ** +0**. Y desde `fa79cc542` (S-27/S-41) el `off` tampoco abre contexto ni
registra listeners en `prepare` . El texto original se conserva como registro de
la medición del 2026-08-04: el interruptor **Sound**
feat(media-player): captions propias + el eje de dirección, canonizado
Dos hilos que comparten ficheros, así que van juntos.
## Captions — el player pinta las cues
La parte estaba declarada en el morfo desde el principio y nunca implementada.
El render nativo no servía: con la pista en mode='showing' el navegador dibuja
su caja de cues en bottom:0 del <video> — la misma franja de 49px que la barra
de controles — y contra el z-index:4 de la barra al 82% de opacidad llegaba el
18% del texto. ::cue no puede moverlo (la spec no admite propiedades de
posición ahí).
Ahora la pista se mantiene en 'hidden': la spec deja la pista activa —cues
parseadas, activeCues mantenida, cuechange disparándose— y el navegador no
pinta nada, así que el texto se monta en la parte Captions, por encima de la
barra. Es el movimiento de Plyr, y por el mismo motivo.
Tres trampas de la especificación, encaradas por escrito:
- Pasar una pista a 'disabled' desactiva sus cues SIN disparar cuechange. Nadie
avisa de que hay que borrar, así que se limpia a mano — si no, el último
subtítulo se queda congelado sobre el vídeo.
- Los eventos de cue no corren mientras está puesto el show-poster flag, así
que el primer pintado se siembra leyendo activeCues.
- cues/activeCues son null en 'disabled'.
De paso cierra un defecto latente: captionsOn era un espejo de una sola
dirección que nunca releía track.mode, así que si el SO activaba subtítulos
salían cues mientras el botón decía que estaban apagados. Ahora se DERIVA de
los modos reales, y una pista que el navegador encienda se adopta (degradada a
'hidden', para que no pinte encima de la nuestra).
Decisiones declaradas: el marcado inline se conserva (se monta el
DocumentFragment de getCueAsHTML como nodos, nunca por innerHTML — el sumidero
de XSS que abre Plyr al serializarlo de vuelta a cadena); la metadata de
posición (line/position/align/vertical/snapToLines/regiones) se DESCARTA. Esto
no es una implementación de WebVTT y el README lo dice.
## Dirección — el player estaba partido por la mitad
Con dir="rtl" por prop y sin dir ambiental, el cromo se disponía en LTR y sus
dos Sliders en RTL: el wrapper pasaba la prop cruda y sólo la reenviaba a los
hijos, sin resolver la cadena ni estampar. En la demo estaba enmascarado porque
el stage estampa dir además de pasar la prop.
Canonizado con el patrón de CONTINUE-direction.md §9.16: activeDir en el
wrapper, resolvedDir en el provider, dir crudo + data-dir resuelto en los
props. El censo del §9.15 no lo tenía — era un cuarto caso que su búsqueda no
cubre (acepta dir y no lo escribe en ningún sitio).
Y los glifos direccionales no se volteaban: en RTL el transporte se leía
▶▶ ▶ ◀◀, con las dos flechas exteriores apuntando HACIA DENTRO — el síntoma que
carousel.css ya documentaba. Se voltean con scale:-1 1 (no rotate:180deg: sólo
coinciden en glifos verticalmente simétricos, y rotar el PiP le llevaría el
recuadro a la esquina de arriba), anclado a data-dir, el atributo propio del
componente. Alcance elegido: seek, play, volumen y PiP; captions y fullscreen
NO. El teclado pasa a resolver por getDirectionalKeys, así que las flechas
concuerdan con el glifo y con el Slider embebido.
## Verificación
check 0 errores propios · vitest del player 14/14, con los tres guards nuevos
vistos en ROJO por mutación · eidos-lint invalid 0 (morfo-backed 25 → 29) ·
morfo:check sin fallos del player · component:audit PASS · smoke PASS ·
docs:check 0 · prettier limpio en lo propio. A ojo en Chrome real: claro y
oscuro, LTR y RTL, y las dos regresiones (el espejo del SO y el subtítulo
congelado).
Nota: el README de soma se lleva una línea ajena — la fila `dir` de la tabla de
props, del barrido de otra sesión, que describe justamente esta cadena.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
del topbar **no silencia los earcons** . Con `data-sound="reduce"` la envolvente
del earcon es idéntica a con Sound ON (`[0, 0.25, 0.25, 0.0001]`, mismos nodos).
El toggle escribe el intent y estampa el atributo, pero
`web/routes/uix/+layout@.svelte` no pasa `preferences` al canal de sonido de
sema, que se queda en nivel `'full'` . El framework sí lo soporta
(`sema/engine.ts` reenvía `soundOptions.preferences ?? opts.preferences` ).
Medido sobre la envolvente del propio earcon; una ganancia de bus aguas abajo
NO se midió, así que el veredicto de «se oye o no» es del oído.
docs(sound-engine): el stamp de sema y la matriz del modo audio, medidos
Cierra los dos pendientes del hilo que no exigían los ojos del usuario, y
sanea el documento, que afirmaba cosas ya cumplidas.
STAMP DE SEMA — cerrado por las dos mitades.
Dónde aterriza: con un MutationObserver sobre el subárbol del player, un clic
en play estampa contact-activate y commit-toggle-play (commit · neutral) sobre
el PROPIO play-button, no sobre el provider, y los limpia al cerrarse el hold.
Que la regla del pack casa: A/B del pico de la envolvente dentro del mismo
componente — transporte (play, mute) 0.0001, o sea silenciado; commit-complete
0.05, audible. Los osciladores sí se construyen, la ganancia nunca sube: es la
resta de gain de D-AP2.7 funcionando. Queda anotada la trampa en la que caí,
comparar ese pico con el 0.25 de un Button de otra página y creer ver una
contradicción: el A/B tiene que ser dentro del mismo componente.
F6, MATRIZ DEL MODO AUDIO — las 16 combinaciones (4 variantes × claro/oscuro ×
LTR/RTL) recorridas buscando partes colapsadas, botones estrujados y desbordes.
Un solo defecto: en `bar` el time-slider colapsa a ancho 0 y el mute a 20px, en
las CUATRO combinaciones, así que no es del eje ni del tema. Visto: el scrubber
queda reducido a un punto ámbar entre 0:00 y 6:12 — una barra de app sin barra
de progreso — mientras el volumen conserva su ancho completo. `card` y `row`
limpias; lo que oculta `inline` es lo que su receta declara.
HIGIENE — el documento decía «sin commitear» del rediseño y del waveform, y
pedía un re-plan del player que ya está hecho. Verificado contra git: rediseño
2f872ef86, waveform 0ab6cc822 + 905012507, rutas limpias. La sección «Por dónde
sigue» se reescribe con lo que queda de verdad: dos miradas del usuario y dos
decisiones (qué cede en `bar`, y el rojo de audio-player en lint.test.ts).
docs:check 0 sobre 564 documentos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Stamp de sema en navegador — CERRADO por las dos mitades (2026-08-04) ✅
**Mitad 1 — dónde aterriza.** Medido con un `MutationObserver` sobre el subárbol
del player, filtrando `data-event*` . Un clic en play produce, **sobre el propio
`[data-media-player-play-button]` ** y no sobre el provider:
| evento | familia · intent | fase |
| -------------------- | -------------------- | -------- |
| `contact-activate` | `contact` | `active` |
| `commit-toggle-play` | `commit` · `neutral` | `active` |
…y a continuación los atributos se limpian, que es la ventana del hold
cerrándose. El stamp aterriza en el **botón** , como el morfo promete.
**Mitad 2 — la regla del pack CASA.** A/B dentro del propio player, midiendo el
pico de la envolvente programada (parche sobre `AudioParam.prototype` ):
| disparo | pico | veredicto |
| -------------------------------- | ---------- | ---------- |
| play-button (transporte) | **0.0001** | silenciado |
| mute-button (transporte) | **0.0001** | silenciado |
| `commit-complete` (fin de pista) | **0.05** | audible |
Los osciladores **sí se construyen** (8 parámetros programados) pero la ganancia
nunca sube: es la resta de gain de D-AP2.7 / pack D.5, funcionando. Un `<Button>`
corriente fuera del player mide **0.25** — 2500× el transporte.
⚠️ **Trampa en la que caí** : comparé ese 0.25 de un Button de otra página con el
transporte y creí ver una contradicción con D-AP2.7. No la había — son
poblaciones distintas. **El A/B tiene que ser dentro del mismo componente.**
## F6 — matriz visual del modo audio: MEDIDA, 1 defecto (2026-08-04)
Las **16 combinaciones** (4 variantes × claro/oscuro × LTR/RTL) recorridas
programáticamente, buscando partes colapsadas, botones estrujados y desbordes:
- `card` y `row` : limpias en las 4. Sólo se oculta `live-indicator` , que es lo
correcto (la receta lo esconde si no hay directo).
- `inline` : oculta artwork/title/artist/time/rate/mute/volume — **por diseño** ,
es lo que la receta declara para la nota de voz.
- **`bar`: colapsa, pero SÓLO por debajo de ~800px.** Barrido de anchos del
contenedor, con la variante verificada en la misma medida:
| ancho | scrubber | mute |
| -----: | -------: | -----: |
| 720px | **0** | **20** |
| 900px | 147 | 36 |
| 1100px | 347 | 36 |
| 1800px | 1047 | 36 |
De 900px en adelante está **sano** . Los 720px son el ancho al que **la demo**
encierra al player, no un ancho propio de una barra de app persistente (la
referencia declarada es Spotify, que ocupa la ventana entera). **Visto a
720px**: el scrubber queda reducido a un punto ámbar entre `0:00` y `6:12`
mientras el volumen conserva sus 64px. Ningún desborde en ninguna combinación.
### El diagnóstico correcto — corrección del usuario (2026-08-04)
La pregunta que yo había planteado, ** «¿qué cede en `bar` ?», es la pregunta
equivocada a nivel de framework**. El usuario lo zanjó: _«¿no es un diseño por
composición? debe de ser por composición y eso lo elegirá el diseñador»_. Y
encaja con la doctrina que el propio `audio-player.css` ya declara — un variant
sólo decide **qué se muestra y cómo fluye** , nunca comportamiento — y con la
salida que ya existe: componer `<MediaPlayer media='audio'>` a mano mantiene el
flujo sin opinión, porque todo el CSS de variantes cuelga de `[data-variant]` ,
que **sólo estampa `<AudioPlayer>`** . Quien quiera otra mezcla a 720px, compone.
Lo que NO se disuelve en composición: que un preset **colapse en silencio** . El
diseñador no recibe ninguna señal de que está por debajo del ancho viable — el
control simplemente desaparece.
### ARREGLADO — el usuario fijó el criterio (2026-08-04)
Aclaración del usuario: no era `bar` lo que estaba mal, era **el track dentro de
`bar` **, «reducido a un punto». Y con ella, las dos decisiones de diseño, suyas:
> _«no puede ser que el slider del tracking sea más pequeño que la del
> volumen»_ · _«lo que sí quitaría del bar es lo que aparece ahora con
> Vicen…, sólo dejaría el título»_
**La causa era de una línea.** `media-player.css` le da a todo scrubber un suelo
(`min-width: var(--space-8)`) y la regla de `bar` lo **anulaba explícitamente**
con `min-inline-size: 0` y mayor especificidad — por eso flex podía encogerlo
hasta la nada y dejar sólo el pulgar.
Aplicado:
- El ancho del volumen sale a token (`--_mp-volume-width`) y el scrubber de
`bar` **se apoya en él** como suelo, así que los dos no pueden divergir cuando
alguien retoque uno.
- El **artista se oculta** en `bar` (`card` y `row` lo conservan: tienen sitio).
Medido tras el arreglo, con la variante verificada en la misma pasada:
| ancho | scrubber | volumen | artista | mute |
| -----: | -------: | ------: | ------: | ---: |
| 560px | **64** | 64 | oculto | 20 |
| 720px | **64** | 64 | oculto | 25 |
| 900px | 229 | 64 | oculto | 36 |
| 1200px | 529 | 64 | oculto | 36 |
Sin desborde en ninguno. **Visto** : los dos sliders se leen ya del mismo tamaño
y el título gana el espacio del artista.
⚠️ **Queda un estrujado que NO toqué** , porque arreglarlo sí arbitra: el
`mute-button` baja a **20– 25px** por debajo de 800px (su tamaño es 36). Ponerle
`flex: none` empujaría el problema a otro control, y eso es decisión de diseño.
Lo que sigue sin resolverse en composición y queda vivo:
- ~~documentar en el README el ancho mínimo de `bar` (~900px), que es un valor de
diseño de la variante, como ya lo son los tamaños de carátula.~~ **HECHO
2026-08-12** (README de eidos, sección de variantes, con la disposición del
orden de descarte firmada).
docs(sound-engine): el stamp de sema y la matriz del modo audio, medidos
Cierra los dos pendientes del hilo que no exigían los ojos del usuario, y
sanea el documento, que afirmaba cosas ya cumplidas.
STAMP DE SEMA — cerrado por las dos mitades.
Dónde aterriza: con un MutationObserver sobre el subárbol del player, un clic
en play estampa contact-activate y commit-toggle-play (commit · neutral) sobre
el PROPIO play-button, no sobre el provider, y los limpia al cerrarse el hold.
Que la regla del pack casa: A/B del pico de la envolvente dentro del mismo
componente — transporte (play, mute) 0.0001, o sea silenciado; commit-complete
0.05, audible. Los osciladores sí se construyen, la ganancia nunca sube: es la
resta de gain de D-AP2.7 funcionando. Queda anotada la trampa en la que caí,
comparar ese pico con el 0.25 de un Button de otra página y creer ver una
contradicción: el A/B tiene que ser dentro del mismo componente.
F6, MATRIZ DEL MODO AUDIO — las 16 combinaciones (4 variantes × claro/oscuro ×
LTR/RTL) recorridas buscando partes colapsadas, botones estrujados y desbordes.
Un solo defecto: en `bar` el time-slider colapsa a ancho 0 y el mute a 20px, en
las CUATRO combinaciones, así que no es del eje ni del tema. Visto: el scrubber
queda reducido a un punto ámbar entre 0:00 y 6:12 — una barra de app sin barra
de progreso — mientras el volumen conserva su ancho completo. `card` y `row`
limpias; lo que oculta `inline` es lo que su receta declara.
HIGIENE — el documento decía «sin commitear» del rediseño y del waveform, y
pedía un re-plan del player que ya está hecho. Verificado contra git: rediseño
2f872ef86, waveform 0ab6cc822 + 905012507, rutas limpias. La sección «Por dónde
sigue» se reescribe con lo que queda de verdad: dos miradas del usuario y dos
decisiones (qué cede en `bar`, y el rojo de audio-player en lint.test.ts).
docs:check 0 sobre 564 documentos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
## Pendiente que solo puede hacer el usuario
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- ~~Escucha A/B de F2~~ ✅ **hecha: «suena igual» (2026-07-31)** .
- Prueba manual de teclas de medios / pantalla de bloqueo — **cuando exista una
página que consuma `sound.media()` ** (el re-plan del player la traerá).
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
docs(sound-engine): el stamp de sema y la matriz del modo audio, medidos
Cierra los dos pendientes del hilo que no exigían los ojos del usuario, y
sanea el documento, que afirmaba cosas ya cumplidas.
STAMP DE SEMA — cerrado por las dos mitades.
Dónde aterriza: con un MutationObserver sobre el subárbol del player, un clic
en play estampa contact-activate y commit-toggle-play (commit · neutral) sobre
el PROPIO play-button, no sobre el provider, y los limpia al cerrarse el hold.
Que la regla del pack casa: A/B del pico de la envolvente dentro del mismo
componente — transporte (play, mute) 0.0001, o sea silenciado; commit-complete
0.05, audible. Los osciladores sí se construyen, la ganancia nunca sube: es la
resta de gain de D-AP2.7 funcionando. Queda anotada la trampa en la que caí,
comparar ese pico con el 0.25 de un Button de otra página y creer ver una
contradicción: el A/B tiene que ser dentro del mismo componente.
F6, MATRIZ DEL MODO AUDIO — las 16 combinaciones (4 variantes × claro/oscuro ×
LTR/RTL) recorridas buscando partes colapsadas, botones estrujados y desbordes.
Un solo defecto: en `bar` el time-slider colapsa a ancho 0 y el mute a 20px, en
las CUATRO combinaciones, así que no es del eje ni del tema. Visto: el scrubber
queda reducido a un punto ámbar entre 0:00 y 6:12 — una barra de app sin barra
de progreso — mientras el volumen conserva su ancho completo. `card` y `row`
limpias; lo que oculta `inline` es lo que su receta declara.
HIGIENE — el documento decía «sin commitear» del rediseño y del waveform, y
pedía un re-plan del player que ya está hecho. Verificado contra git: rediseño
2f872ef86, waveform 0ab6cc822 + 905012507, rutas limpias. La sección «Por dónde
sigue» se reescribe con lo que queda de verdad: dos miradas del usuario y dos
decisiones (qué cede en `bar`, y el rojo de audio-player en lint.test.ts).
docs:check 0 sobre 564 documentos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Por dónde sigue (reescrito 2026-08-04 — lo anterior estaba cumplido)
⚠️ **Higiene del documento** : la tabla de Estado de arriba dice «sin commitear»
del rediseño y de las fases F4/F5 del waveform. **Es rancio y está verificado** :
el rediseño es `2f872ef86` , el waveform `0ab6cc822` + `905012507` , y esas rutas
están limpias en el árbol. El «re-plan del reproductor sobre el servicio» que
esta sección pedía **también está hecho** (F3: el player consume
`uix.sound.media(el, { metadata })` ), igual que sus dos pendientes de cierre —
escenario mixto y teclas de medios.
De este hilo quedan **cuatro cosas, y ninguna es código a ciegas** :
1. ~~**La mirada del waveform (su F6)**~~ ✅ **APROBADA por el usuario
(2026-08-04): «waveform … las veo bien».** Cierra el pendiente que arrastraba
desde el 2026-08-01.
2. ~~**La mirada sobre el resto del modo audio**~~ ✅ **LAS CUATRO APROBADAS.**
`card` el 2026-08-04 («la variante card la veo bien») y ** `row` e `inline` el
2026-08-05** («ya te las di por buenas row e inline»). El eje visual del modo
audio queda CERRADO — no volver a listarlas como pendientes.
3. ~~**`bar` por debajo de su ancho**~~ ✅ **ARREGLADO** (ver §F6): el scrubber
ya nunca se dibuja más estrecho que el volumen y el artista sale de `bar` ,
las dos por decisión del usuario. **Queda vivo** el `mute-button` estrujado a
20– 25px bajo 800px — no lo toqué porque arreglarlo empuja el problema a otro
control, y eso es diseño.
**Disposición FIRMADA (2026-08-12)** : el ancho mínimo (~900px) queda
documentado en el README de eidos como valor de diseño de la variante; el
arreglo real firmado es ORDEN DE DESCARTE — bajo el umbral cae el slider de
volumen y el mute recupera sus 36px — con container query como mecanismo
honesto: el primer caso concreto que justifica reabrir D-AP2.9.
4. ~~**El rojo de `audio-player` en `lint.test.ts`**~~ **RESUELTO — la decisión
ya estaba tomada en el código** (verificado 2026-08-12, 18/18 verde): existe
la categoría `COMPETING_ROOTS` (`lint.test.ts:70`) con la razón escrita,
separada a propósito de `KNOWN_MISSING_MORFO` , que «prometería una
construcción que no debe ocurrir jamás».
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Sesión 2026-08-04 (tarde) — cuatro entregas, todas verificadas en navegador
Commits `ee51172d8` · `5cbb03904` · `dbd239bcc` (+ el waveform, `127c4c95b` ).
El detalle está en los mensajes y en el README de eidos del componente; aquí
sólo lo que cambia el mapa.
1. **El waveform ya se VE en la demo.** No se veía por dos causas encadenadas:
el scrubber compone el Waveform de _soma_ , así que su envoltorio de eidos
nunca montaba y **su receta no se importaba** (onda sin estilar, 255px de
alto); y la muestra local era `whoosh.wav` , de **0,18 s** . El player carga
ahora `waveform.css` igual que ya cargaba `slider.css` , y la demo usa
`/music/t2.wav` (2:44). ⚠️ **Lección reutilizable** : componer un primitivo
desde soma deja su receta fuera — la carga el componente que lo compone.
2. **La velocidad ya se puede BAJAR** (`RateFloat`). El `RateButton` sólo subía
y recortaba en `2× ` . Ahora `AudioLayout` compone un `DropdownMenu` +
`RadioGroup` ; el disparador sigue siendo el `RateButton` real, con su ciclado
anulado vía `preventDefault()` en la costura. ⚠️ Dos defectos del panel
portado, los mismos que el float de volumen: el suelo de `12rem` del menú, y
**la etiqueta redimensionaba el botón y desplazaba la botonera** (ancho fijo
`4.6em` , medido 64·74·88·121px).
3. **API de eventos para la app** — `onPlay` · `onPause` · `onEnded` ·
`onError` . Antes no había NINGUNA: la única vía era enchufar un
`MediaProvider` entero. Dos divergencias deliberadas respecto a la capa
perceptual, las dos en el test: el `pause` del final natural no se reporta, y
`onEnded` SÍ dispara para un final por salto (que `commit-complete` exime).
4. **Transporte de lista** — `RestartButton` + `SkipButton` (morfo **25** partes,
las dos opt-in, sin eventos sema nuevos por decisión del usuario). El player
**no tiene cola a propósito** : `onPrevious` /`onNext` piden y la app cambia el
`src` . Una dirección sin manejador se pinta deshabilitada, que es justo el
aspecto de los extremos de una cola.
5. **Selector de pista de subtítulos** (`CaptionFloat`, `21d817c1f` ).
`toggleCaptions()` sólo alcanza la PRIMERA pista — a propósito, es un toggle,
y las demás quedan en `disabled` porque ** `hidden` obliga al navegador a
descargar el VTT**. Con dos idiomas no había forma de llegar al segundo.
Añadidos a soma: `selectCaptionTrack(track|null)` y los espejos reactivos
`captionTrackList` / `activeCaptionTrack` (el `TextTrackList` vivo NO
notifica, así que un chrome que LISTA pistas no veía las que llegan tarde).
⚠️ Cambiar de idioma **no** dispara `commit-toggle-captions` : sólo lo hace una
transición real de encendido/apagado.
6. **La calidad, especificada sin implementar** (`e0206f6b2`).
`docs/decisions/design-media-quality.md` . Grep del puerto: **cero** campos de
calidad, y no es descuido — los niveles los aporta un motor HLS/DASH que este
framework no vendoriza. Por eso `settings-button` **sigue sin implementar** :
la velocidad tiene lista propia, la pista también, y el tercer eje no existe.
Un engranaje con un solo cajón sería el `CaptionFloat` disfrazado.
fix(media-player): `c` ya no descarta el idioma elegido, y el menu deja de robarle teclas al player
Cierra los cuatro que la revision adversarial dejo vivos y que dependian del
provider. La otra sesion commiteo su refactor (5983b00d5), asi que el fichero
quedo libre.
## `c` restauraba la primera pista, no la tuya
Elegias «Espanol» en el CaptionFloat, pulsabas `c` dos veces y volvias a ingles:
`toggleCaptions` forzaba el indice 0 — el toggle deshaciendo una decision de la
que no sabia nada. Ahora recuerda la ultima elegida y la RESTAURA al encender;
cae en la primera solo si nadie ha elegido, que es el caso de una sola pista y el
comportamiento de siempre.
`preferredCaptionTrack` no es `$state`: nada deriva de el, solo sobrevive entre
dos turnos. Y solo recuerda una eleccion REAL — `null` es «apagado», no «olvida
mi idioma».
## El menu le robaba teclas al player
Portalizar arreglo el panel abierto, pero el DISPARADOR sigue dentro del player:
ArrowDown sobre el boton de subtitulos bajaba el volumen en la misma pulsacion.
La guarda de exencion ya predicaba la regla correcta en su propio comentario
—«the exemption is by ownership, not by tag»— y un menu tambien posee sus teclas,
asi que se ensancha a `[data-slider],[role='menu'],[aria-haspopup='menu']`.
Medido por las dos caras: con el foco en el disparador, cuatro teclas y ni el
volumen ni el tiempo se mueven; con el foco en el player, ArrowDown sigue bajando
el volumen a 0.95 — no se ensancho de mas.
## Lo que NO se puede arreglar desde ahi, y queda declarado
El disparador del CaptionFloat se sigue anunciando como alternancia
(`aria-pressed`, `aria-keyshortcuts='c'`, «Activar subtitulos») siendo un
disparador de menu. Se intento anularlo en la costura y **se ignora en silencio**:
soma compone `mergeProps(restProps, state.props)`, asi que los atributos que
POSEE el morfo ganan a lo que le pases — que es el sentido del contrato. Medido.
El override muerto se retiro (aparentaba funcionar), la clave de texto que habia
anadido para el se retiro con el, y la decision queda escrita en el handoff como
lo que es: una DECISION DE MORFO, no un arreglo de implementacion.
Test nacido en rojo. 43/43 · morfo:check media-player PASS · eidos-lint invalid 0
· docs:check 0 · check 0 errores propios (77).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Cola de la revisión adversarial — CERRADA (2026-08-05)
Una revisión adversarial (5 lentes × 3 escépticos) devolvió 15 supervivientes de 19. Los míos están arreglados en `aaced3ebe` (portal + `dir` + paridad de la demo
- tabla de eventos) y los cuatro que dependían del provider, en cuanto la otra
sesión commiteó (`5983b00d5`), quedaron así:
1. ✅ ** `toggleCaptions` ya no descarta la pista elegida.** Recuerda la última
elegida (`preferredCaptionTrack`) y la RESTAURA al volver a encender; cae en
la primera sólo si nadie ha elegido, que es el caso de una sola pista y el
comportamiento de siempre. Medido: elegir Español, `c` , `c` → sigue Español.
2. ✅ **Fuga de teclas desde el disparador.** La guarda de propiedad se ensanchó
a `[data-slider],[role='menu'],[aria-haspopup='menu']` , que es justo lo que su
propio comentario ya predicaba («the exemption is by ownership, not by tag»).
Medido: con el foco en el disparador, cuatro teclas y nada se mueve; con el
foco en el player, `ArrowDown` sigue bajando el volumen — no se ensanchó de
más.
3. ⚠️ ** `aria-pressed` + `aria-keyshortcuts='c'` sobre el disparador del
`CaptionFloat` : NO arreglado, y no se puede desde ahí.** Se intentó anularlos
en la costura y **se ignoran en silencio** : soma compone
`mergeProps(restProps, state.props)` , así que los atributos que posee el morfo
ganan a lo que le pases — que es el sentido del contrato. El override muerto
se retiró para que nadie crea que funciona.
**Queda como DECISIÓN DE MORFO** (necesita tu criterio): el `caption-button`
declara `aria-pressed` incondicionalmente. Dentro del float se comporta como
disparador de menú (`aria-haspopup` + `aria-expanded` ) y anunciarse además
como alternancia pulsada es contradictorio. Opciones: (a) dejarlo —una parte
compuesta de dos formas no puede tener dos ARIA—, (b) hacer `aria-pressed`
condicional en el morfo, (c) declarar una parte aparte para el disparador de
lista. **No lo decido yo.**
**FIRMADA Y EJECUTADA (2026-08-12): la (b).** `aria-pressed` es condicional
en el morfo (`prop-falsy menuTrigger` — el patrón `fieldLabelled` ,
`morfo.md` Step 4); el provider publica la fuente por parte y el
`CaptionFloat` pasa `menuTrigger` . Test visto fallar: compuesto como
disparador de lista el attr queda genuinamente AUSENTE; solo, sigue
anunciando su estado. `aria-keyshortcuts='c'` se queda — la tecla alterna
los subtítulos desde cualquier foco, así que el anuncio es verdad.
fix(media-player): `c` ya no descarta el idioma elegido, y el menu deja de robarle teclas al player
Cierra los cuatro que la revision adversarial dejo vivos y que dependian del
provider. La otra sesion commiteo su refactor (5983b00d5), asi que el fichero
quedo libre.
## `c` restauraba la primera pista, no la tuya
Elegias «Espanol» en el CaptionFloat, pulsabas `c` dos veces y volvias a ingles:
`toggleCaptions` forzaba el indice 0 — el toggle deshaciendo una decision de la
que no sabia nada. Ahora recuerda la ultima elegida y la RESTAURA al encender;
cae en la primera solo si nadie ha elegido, que es el caso de una sola pista y el
comportamiento de siempre.
`preferredCaptionTrack` no es `$state`: nada deriva de el, solo sobrevive entre
dos turnos. Y solo recuerda una eleccion REAL — `null` es «apagado», no «olvida
mi idioma».
## El menu le robaba teclas al player
Portalizar arreglo el panel abierto, pero el DISPARADOR sigue dentro del player:
ArrowDown sobre el boton de subtitulos bajaba el volumen en la misma pulsacion.
La guarda de exencion ya predicaba la regla correcta en su propio comentario
—«the exemption is by ownership, not by tag»— y un menu tambien posee sus teclas,
asi que se ensancha a `[data-slider],[role='menu'],[aria-haspopup='menu']`.
Medido por las dos caras: con el foco en el disparador, cuatro teclas y ni el
volumen ni el tiempo se mueven; con el foco en el player, ArrowDown sigue bajando
el volumen a 0.95 — no se ensancho de mas.
## Lo que NO se puede arreglar desde ahi, y queda declarado
El disparador del CaptionFloat se sigue anunciando como alternancia
(`aria-pressed`, `aria-keyshortcuts='c'`, «Activar subtitulos») siendo un
disparador de menu. Se intento anularlo en la costura y **se ignora en silencio**:
soma compone `mergeProps(restProps, state.props)`, asi que los atributos que
POSEE el morfo ganan a lo que le pases — que es el sentido del contrato. Medido.
El override muerto se retiro (aparentaba funcionar), la clave de texto que habia
anadido para el se retiro con el, y la decision queda escrita en el handoff como
lo que es: una DECISION DE MORFO, no un arreglo de implementacion.
Test nacido en rojo. 43/43 · morfo:check media-player PASS · eidos-lint invalid 0
· docs:check 0 · check 0 errores propios (77).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
4. ✅ ** `aria-keyshortcuts='c'` ** deja de ser un problema de comportamiento en
cuanto el punto 1 está arreglado: la tecla ya no descarta nada. Queda sólo
como parte del punto 3 (qué se ANUNCIA sobre ese botón).
## Lo que NO he podido verificar (pendiente de tus ojos)
⚠️ **El teclado real sobre los tres floats.** El panel de navegador embebido dejó
de componer frames a media tarde (`document.visibilityState === 'hidden'`), y con
él **no llegan las teclas** : instrumenté un listener y una pulsación real de
Escape registró **cero** eventos. Llegué a creer que había un defecto —«Escape no
cierra el float dentro del player»— y **era falso** : lo roto era mi vía de
entrada, no el componente. Misma familia que las transiciones CSS congeladas.
Verificado igualmente con eventos despachados: un `keydown` de Enter/Espacio abre
el menú, y el `click` que el navegador sintetiza detrás **no** toca los
subtítulos (el `preventDefault()` de la costura aguanta). Falta comprobar con
teclado de verdad: abrir con Enter desde cerrado, moverse con flechas, y cerrar
con Escape.
⚠️ **Otra sesión commitea en esta misma rama** (hilo de dirección): verificar
`HEAD` antes de dar nada por perdido, y `git reset -q` + `add` sólo lo propio.
docs(sound-engine): el stamp de sema y la matriz del modo audio, medidos
Cierra los dos pendientes del hilo que no exigían los ojos del usuario, y
sanea el documento, que afirmaba cosas ya cumplidas.
STAMP DE SEMA — cerrado por las dos mitades.
Dónde aterriza: con un MutationObserver sobre el subárbol del player, un clic
en play estampa contact-activate y commit-toggle-play (commit · neutral) sobre
el PROPIO play-button, no sobre el provider, y los limpia al cerrarse el hold.
Que la regla del pack casa: A/B del pico de la envolvente dentro del mismo
componente — transporte (play, mute) 0.0001, o sea silenciado; commit-complete
0.05, audible. Los osciladores sí se construyen, la ganancia nunca sube: es la
resta de gain de D-AP2.7 funcionando. Queda anotada la trampa en la que caí,
comparar ese pico con el 0.25 de un Button de otra página y creer ver una
contradicción: el A/B tiene que ser dentro del mismo componente.
F6, MATRIZ DEL MODO AUDIO — las 16 combinaciones (4 variantes × claro/oscuro ×
LTR/RTL) recorridas buscando partes colapsadas, botones estrujados y desbordes.
Un solo defecto: en `bar` el time-slider colapsa a ancho 0 y el mute a 20px, en
las CUATRO combinaciones, así que no es del eje ni del tema. Visto: el scrubber
queda reducido a un punto ámbar entre 0:00 y 6:12 — una barra de app sin barra
de progreso — mientras el volumen conserva su ancho completo. `card` y `row`
limpias; lo que oculta `inline` es lo que su receta declara.
HIGIENE — el documento decía «sin commitear» del rediseño y del waveform, y
pedía un re-plan del player que ya está hecho. Verificado contra git: rediseño
2f872ef86, waveform 0ab6cc822 + 905012507, rutas limpias. La sección «Por dónde
sigue» se reescribe con lo que queda de verdad: dos miradas del usuario y dos
decisiones (qué cede en `bar`, y el rojo de audio-player en lint.test.ts).
docs:check 0 sobre 564 documentos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
El siguiente ítem de CÓDIGO del componente no es de este hilo sino del handoff
hermano: ** `settings-button` **, lo único que separa al morfo de 25/25 — y su
alcance ENCOGIÓ: la velocidad ya tiene su lista, así que al menú de ajustes sólo
le quedan calidad y pista.
docs(sound-engine): el stamp de sema y la matriz del modo audio, medidos
Cierra los dos pendientes del hilo que no exigían los ojos del usuario, y
sanea el documento, que afirmaba cosas ya cumplidas.
STAMP DE SEMA — cerrado por las dos mitades.
Dónde aterriza: con un MutationObserver sobre el subárbol del player, un clic
en play estampa contact-activate y commit-toggle-play (commit · neutral) sobre
el PROPIO play-button, no sobre el provider, y los limpia al cerrarse el hold.
Que la regla del pack casa: A/B del pico de la envolvente dentro del mismo
componente — transporte (play, mute) 0.0001, o sea silenciado; commit-complete
0.05, audible. Los osciladores sí se construyen, la ganancia nunca sube: es la
resta de gain de D-AP2.7 funcionando. Queda anotada la trampa en la que caí,
comparar ese pico con el 0.25 de un Button de otra página y creer ver una
contradicción: el A/B tiene que ser dentro del mismo componente.
F6, MATRIZ DEL MODO AUDIO — las 16 combinaciones (4 variantes × claro/oscuro ×
LTR/RTL) recorridas buscando partes colapsadas, botones estrujados y desbordes.
Un solo defecto: en `bar` el time-slider colapsa a ancho 0 y el mute a 20px, en
las CUATRO combinaciones, así que no es del eje ni del tema. Visto: el scrubber
queda reducido a un punto ámbar entre 0:00 y 6:12 — una barra de app sin barra
de progreso — mientras el volumen conserva su ancho completo. `card` y `row`
limpias; lo que oculta `inline` es lo que su receta declara.
HIGIENE — el documento decía «sin commitear» del rediseño y del waveform, y
pedía un re-plan del player que ya está hecho. Verificado contra git: rediseño
2f872ef86, waveform 0ab6cc822 + 905012507, rutas limpias. La sección «Por dónde
sigue» se reescribe con lo que queda de verdad: dos miradas del usuario y dos
decisiones (qué cede en `bar`, y el rojo de audio-player en lint.test.ts).
docs:check 0 sobre 564 documentos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Ver [`CONTINUE-player-rtl.md` ](./CONTINUE-player-rtl.md ) §0.
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Trampas de este repo (verificadas a base de perder tiempo)
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- **NO arrancar un dev server si el del usuario está vivo** (`vite dev --force`
reescribe `deps` → dos runtimes → `lifecycle_outside_component` ). Acoplarse al
puerto 5173. Recuperación: parar node → borrar `node_modules/.vite/deps` →
`npm run dev` → Ctrl+Shift+R.
- **El panel del navegador no pinta** (viewport 0× 0); `javascript_tool` sí — con
fix(slider,media-player): el anillo del commit no enmarca contenedores, y saltar al final no es completar
Cuatro defectos que solo aparecieron cuando el usuario MIRO la pagina.
1) EL BORDE DE LOS SLIDERS ("se viene arrastrando desde el principio").
No era del arquetipo ni del slider: es la firma GLOBAL de la familia
commit. BUILTIN_SIGNATURES.commit monta commit-settle, cuyo 30% pinta
box-shadow 0 0 0 3px. El commit-set del Slider apunta al PROVIDER, asi que
cada paso de flecha y cada release dibujaban un aro alrededor de la caja
entera (medido en vivo: 544x40). Un aro lee como feedback en un boton y
como borde parasito en un rail ancho y transparente. Exencion acotada en
slider.css — el patron que gradient-builder.css ya practicaba. Alcanza a
todo slider embebido (waveform, media-player, color-picker, time-picker)
porque todos llevan [data-slider]. `animation` es la palanca, no
`box-shadow`: las declaraciones de keyframe ganan a las normales, asi que
solo reiniciar el NOMBRE de la animacion detiene la pintura.
2) EL MISMO ANILLO EN EL PLAYER: commit-complete / commit-fail apuntan al
provider -> borde alrededor de los ~700px del reproductor. Misma exencion
en media-player.css. Los botones conservan su aro pequeno (20x36), que es
donde el feedback pertenece.
3) SALTAR AL FINAL ANUNCIABA UNA COMPLECION FALSA. Los botones de salto no
tienen evento propio: seek() delega a proposito (D-AP2.13, "el release del
scrubber ya ES el commit-set del Slider") — cierto para el scrubber y
VACIO para los botones, que no tienen Slider detras. Al saltar mas alla del
final el elemento dispara `ended` -> commit-complete en el provider ->
sonido (es audible por D-AP2.7) + anillo, mientras el boton de retroceso
quedaba mudo. Saltar al final no es completar: es abandonar la obra en el
final. seek()/scrubTo() marcan el salto que aterriza en el final y `ended`
no anuncia; un `play` posterior limpia la marca, asi que el final natural
se sigue anunciando. Los dos botones quedan simetricos y mudos, que es la
doctrina del transporte.
4) MEDIASESSION SIN OVERLAY DEL SO: el modo audio YA funcionaba (metadata
completa, medida en vivo); el roto era VIDEO, el modo por defecto de la
demo. <AudioPlayer> deriva metadata de title/artist/artwork; el
<MediaPlayer> compositivo no puede leer su hijo Title sin volverse
data-driven, asi que la declara el consumidor — y la demo no lo hacia.
El arreglo es de la demo; el art se comporta como D-SR.6/D-AP2.5 lo firmo.
Guard nuevo (no anunciar compleccion al saltar al final; si al final
natural) VISTO EN ROJO antes de darlo por bueno.
Nota de doctrina, tras leer el corpus a peticion del usuario: la exencion
acotada en la receta ES la puerta, no un olor. motion.md §4 fija que la
firma es generica por decision estructural ("un commit asienta igual en
todo el sistema"), eidos es el UNICO dueno del canal visual, los packs de
sema solo transportan sound/haptic, y el VisualChannel no consulta
activeChannels — no hay palanca declarativa para callar solo lo visual.
Queda anotado, menor: commit-settle escribe 3px a pelo donde sus hermanos
announce-pulse-* usan var(--focus-ring-width).
Verificado: check 75 = baseline, 0 propios; 11/11 media-player;
motion.test.ts verde (la firma global intacta); sonda en Chrome vivo —
slider real -> animation none, elemento no-slider con el mismo sello ->
sigue commit-settle, aro de foco intacto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
eso se midieron los contextos. Matiz 2026-08-01: con el panel oculto las
CSSTransitions quedan `running` CONGELADAS → `getComputedStyle` devuelve el
valor intermedio del from, no el target de la cascada. Para verificar estilo,
leer las custom props (`getPropertyValue`), no los valores usados.
- **MUTEAR antes de reproducir, y PARAR al terminar.** Medir el escenario mixto
y los earcons exige reproducir de verdad, pero el Chrome que suena es el DEL
USUARIO: dejar una pista de 6 minutos sonando a volumen 1 mientras se hacen
otras cosas es una molestia real, y pasó (2026-08-04). `el.muted = true` basta
para casi todo lo que se mide —contextos, stamps, envolventes, geometría—
porque nada de eso depende de que se oiga; el volumen sólo hace falta para la
escucha, que es del usuario. Y al acabar cada medición, `el.pause()` .
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- **Un guard que no has visto fallar no vale** — quitar el cableado y ver el rojo.
- **Una medición no es un diagnóstico** — README → CANON §8 → book-deviations →
código, antes de acusar a una pieza.
- **Filtrar `npm run check` con cuidado**: rutas con `\\` doble; un patrón con `/`
no casa y da falsos «0 propios».
- **El árbol está compartido con otras sesiones** — `git reset -q` + add solo lo
propio, en un bloque atómico. `contracts.test.ts` viene modificado de otra
sesión: **no tocarlo en E1** (su fila `sound` no pinea métodos y sigue verdadera).
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Ajenos y preexistentes detectados (NO tocados)
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- `contracts.test.ts` 3 rojos ajenos: `aura` , `menubar` , `radio-group` .
fix(slider,media-player): el anillo del commit no enmarca contenedores, y saltar al final no es completar
Cuatro defectos que solo aparecieron cuando el usuario MIRO la pagina.
1) EL BORDE DE LOS SLIDERS ("se viene arrastrando desde el principio").
No era del arquetipo ni del slider: es la firma GLOBAL de la familia
commit. BUILTIN_SIGNATURES.commit monta commit-settle, cuyo 30% pinta
box-shadow 0 0 0 3px. El commit-set del Slider apunta al PROVIDER, asi que
cada paso de flecha y cada release dibujaban un aro alrededor de la caja
entera (medido en vivo: 544x40). Un aro lee como feedback en un boton y
como borde parasito en un rail ancho y transparente. Exencion acotada en
slider.css — el patron que gradient-builder.css ya practicaba. Alcanza a
todo slider embebido (waveform, media-player, color-picker, time-picker)
porque todos llevan [data-slider]. `animation` es la palanca, no
`box-shadow`: las declaraciones de keyframe ganan a las normales, asi que
solo reiniciar el NOMBRE de la animacion detiene la pintura.
2) EL MISMO ANILLO EN EL PLAYER: commit-complete / commit-fail apuntan al
provider -> borde alrededor de los ~700px del reproductor. Misma exencion
en media-player.css. Los botones conservan su aro pequeno (20x36), que es
donde el feedback pertenece.
3) SALTAR AL FINAL ANUNCIABA UNA COMPLECION FALSA. Los botones de salto no
tienen evento propio: seek() delega a proposito (D-AP2.13, "el release del
scrubber ya ES el commit-set del Slider") — cierto para el scrubber y
VACIO para los botones, que no tienen Slider detras. Al saltar mas alla del
final el elemento dispara `ended` -> commit-complete en el provider ->
sonido (es audible por D-AP2.7) + anillo, mientras el boton de retroceso
quedaba mudo. Saltar al final no es completar: es abandonar la obra en el
final. seek()/scrubTo() marcan el salto que aterriza en el final y `ended`
no anuncia; un `play` posterior limpia la marca, asi que el final natural
se sigue anunciando. Los dos botones quedan simetricos y mudos, que es la
doctrina del transporte.
4) MEDIASESSION SIN OVERLAY DEL SO: el modo audio YA funcionaba (metadata
completa, medida en vivo); el roto era VIDEO, el modo por defecto de la
demo. <AudioPlayer> deriva metadata de title/artist/artwork; el
<MediaPlayer> compositivo no puede leer su hijo Title sin volverse
data-driven, asi que la declara el consumidor — y la demo no lo hacia.
El arreglo es de la demo; el art se comporta como D-SR.6/D-AP2.5 lo firmo.
Guard nuevo (no anunciar compleccion al saltar al final; si al final
natural) VISTO EN ROJO antes de darlo por bueno.
Nota de doctrina, tras leer el corpus a peticion del usuario: la exencion
acotada en la receta ES la puerta, no un olor. motion.md §4 fija que la
firma es generica por decision estructural ("un commit asienta igual en
todo el sistema"), eidos es el UNICO dueno del canal visual, los packs de
sema solo transportan sound/haptic, y el VisualChannel no consulta
activeChannels — no hay palanca declarativa para callar solo lo visual.
Queda anotado, menor: commit-settle escribe 3px a pelo donde sus hermanos
announce-pulse-* usan var(--focus-ring-width).
Verificado: check 75 = baseline, 0 propios; 11/11 media-player;
motion.test.ts verde (la firma global intacta); sonda en Chrome vivo —
slider real -> animation none, elemento no-slider con el mismo sello ->
sigue commit-settle, aro de foco intacto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `docs:check` 1 error ajeno: `eidos/components/callout/README.md:23` («8 roles», son 9) — **ya no aparece el 2026-08-01 (docs:check 0)** ; alguien lo arregló.
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- `temas/sema/_lib/audition.ts` : 2 errores de tipos preexistentes.
fix(slider,media-player): el anillo del commit no enmarca contenedores, y saltar al final no es completar
Cuatro defectos que solo aparecieron cuando el usuario MIRO la pagina.
1) EL BORDE DE LOS SLIDERS ("se viene arrastrando desde el principio").
No era del arquetipo ni del slider: es la firma GLOBAL de la familia
commit. BUILTIN_SIGNATURES.commit monta commit-settle, cuyo 30% pinta
box-shadow 0 0 0 3px. El commit-set del Slider apunta al PROVIDER, asi que
cada paso de flecha y cada release dibujaban un aro alrededor de la caja
entera (medido en vivo: 544x40). Un aro lee como feedback en un boton y
como borde parasito en un rail ancho y transparente. Exencion acotada en
slider.css — el patron que gradient-builder.css ya practicaba. Alcanza a
todo slider embebido (waveform, media-player, color-picker, time-picker)
porque todos llevan [data-slider]. `animation` es la palanca, no
`box-shadow`: las declaraciones de keyframe ganan a las normales, asi que
solo reiniciar el NOMBRE de la animacion detiene la pintura.
2) EL MISMO ANILLO EN EL PLAYER: commit-complete / commit-fail apuntan al
provider -> borde alrededor de los ~700px del reproductor. Misma exencion
en media-player.css. Los botones conservan su aro pequeno (20x36), que es
donde el feedback pertenece.
3) SALTAR AL FINAL ANUNCIABA UNA COMPLECION FALSA. Los botones de salto no
tienen evento propio: seek() delega a proposito (D-AP2.13, "el release del
scrubber ya ES el commit-set del Slider") — cierto para el scrubber y
VACIO para los botones, que no tienen Slider detras. Al saltar mas alla del
final el elemento dispara `ended` -> commit-complete en el provider ->
sonido (es audible por D-AP2.7) + anillo, mientras el boton de retroceso
quedaba mudo. Saltar al final no es completar: es abandonar la obra en el
final. seek()/scrubTo() marcan el salto que aterriza en el final y `ended`
no anuncia; un `play` posterior limpia la marca, asi que el final natural
se sigue anunciando. Los dos botones quedan simetricos y mudos, que es la
doctrina del transporte.
4) MEDIASESSION SIN OVERLAY DEL SO: el modo audio YA funcionaba (metadata
completa, medida en vivo); el roto era VIDEO, el modo por defecto de la
demo. <AudioPlayer> deriva metadata de title/artist/artwork; el
<MediaPlayer> compositivo no puede leer su hijo Title sin volverse
data-driven, asi que la declara el consumidor — y la demo no lo hacia.
El arreglo es de la demo; el art se comporta como D-SR.6/D-AP2.5 lo firmo.
Guard nuevo (no anunciar compleccion al saltar al final; si al final
natural) VISTO EN ROJO antes de darlo por bueno.
Nota de doctrina, tras leer el corpus a peticion del usuario: la exencion
acotada en la receta ES la puerta, no un olor. motion.md §4 fija que la
firma es generica por decision estructural ("un commit asienta igual en
todo el sistema"), eidos es el UNICO dueno del canal visual, los packs de
sema solo transportan sound/haptic, y el VisualChannel no consulta
activeChannels — no hay palanca declarativa para callar solo lo visual.
Queda anotado, menor: commit-settle escribe 3px a pelo donde sus hermanos
announce-pulse-* usan var(--focus-ring-width).
Verificado: check 75 = baseline, 0 propios; 11/11 media-player;
motion.test.ts verde (la firma global intacta); sonda en Chrome vivo —
slider real -> animation none, elemento no-slider con el mismo sello ->
sigue commit-settle, aro de foco intacto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **`lint.test.ts` 1 rojo: `audio-player` sin morfo** — deuda de NUESTRO F5 del
player (`c4d64ba98`): es la capa de variantes GATED sobre el morfo del
media-player (su css engancha solo `data-media-player-*` + `data-live` ), no
una violación morfo-first, pero el test no tiene categoría para «variant
layer sobre morfo hermano». Decisión pendiente (¿nueva categoría con
comentario vs KNOWN_MISSING_MORFO?) — es un guard compartido, no se tocó.
- Demo del slider: `sliderMorfo.parts[2].keyboard` roto por el desplazamiento
de índices al nacer SecondaryRange (G-2) — 1 error de `check` preexistente;
chip de tarea lanzado con el fix (seleccionar la part por kebab, patrón de la
demo del waveform).
- Prettier: `component-visual-attrs.test.ts` , `lib/recipes/base.ts` y
`scripts/eidos-lint-all.ts` ya fallaban `--check` EN HEAD (¿CRLF de otra
sesión?); mis líneas siguen el estilo del entorno y no se reformateó el
fichero entero (sería cascada).