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
# PLAN — Reproductor de sonido (`AudioPlayer`)
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
> **⛔ GATE RECHAZADO — PLAN SUSPENDIDO (2026-07-31).** La cuarta presentación
> se presentó y el usuario la **rechazó de raíz**: el sistema de sonido sobre el
> que este plan se apoya está mal planteado — 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 del
> framework**, al nivel de `motion` / `timers`. El sonido se rediseña clean-room
> como módulo independiente ([`PLAN-sound-redesign.md`](./PLAN-sound-redesign.md))
> y los consumidores se adaptan a él. Este plan se re-planteará DESPUÉS, sobre
> el servicio nuevo; nada de lo de abajo se implementa. Las **mediciones** de F0
> (§8.5– §8.7: H-1, H-9, H-10, segmentos 1 de 6) siguen siendo hechos válidos
> del terreno y el plan nuevo las hereda como conocimiento.
>
> **El re-plan EXISTE (2026-07-31): [`PLAN-audio-player-v2.md`](./PLAN-audio-player-v2.md)**
> — anclado al servicio, con su gate D-AP2.1…13. Este fichero queda como
> ARCHIVO de los hechos y el análisis que el v2 referencia (§2 matriz · §6
> formas · §7 contrato · §8.5– §8.7 evidencia).
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
> **Tipo**: plan de creación por fases (process — efímero, NO fuente de verdad).
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **Fecha**: 2026-07-30 · **Estado**: **🔏 EN EL GATE, CONSOLIDADO (3.ª pasada)** —
> `$sound` existe y su invariante está medida ([`PLAN-sound-engine.md`](./PLAN-sound-engine.md));
> las verificaciones de F0 están **HECHAS**, con su evidencia (§8.5 · §8.6 · §8.7).
> Decisiones **D-AP.1…D-AP.13 SIN FIRMAR** — y el gate **ya no se mueve**: tres
> pasadas lo movieron tres veces (D-AP.7 corregida dos veces, D-AP.12 → v2,
> D-AP.13 al final); la cuarta presentación **se firma o se rechaza, no se
> re-excava**. No se escribe una línea de código antes de la firma.
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
> **Kickoff para sesión nueva**: *"Lee `docs/process/PLAN-audio-player.md` y
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> presenta el gate de §4 — las verificaciones ya están hechas; re-auditar antes
> de la firma está prohibido."*
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
>
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
> ### ✅ Correcciones incorporadas (sesión 2026-07-30)
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
>
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
> Las 5 correcciones que colgaban de esta cabecera **ya están en el cuerpo**:
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
>
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
> | # | Corrección | Dónde aterrizó |
> | --- | --- | --- |
> | 1 | Faltaba la reproducción de **segmentos** entera — y **Vidstack la tiene** (`clipStartTime`/`clipEndTime`), luego es **paridad pendiente, no superación** | fila nueva en la matriz §2 · **D-AP.12** · contrato §7.5 · §10 |
> | 2 | **G-2** (pista secundaria del `Slider`) sube a **BLOQUEANTE** de v1: tres consumidores (buffer · ventana de clip · capítulos) | §8 · F1 |
> | 3 | **D-AP.11** se reescribe contra el art: su premisa («no hay motor») era falsa | **D-AP.11** en §4 |
> | 4 | Dos invariantes al contrato: `prefs.sound` **nunca** toca el volumen del contenido · el silencio de UI debe alcanzar al `Slider` compuesto | §7.6 |
> | 5 | **D-AP.7** deja de ser regla de cascada y pasa a **ducking**, capacidad del motor | **D-AP.7** reescrita |
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
>
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> Y las de la **segunda pasada (F0, 2026-07-30)**, también ya en el cuerpo:
> H-1 ✅ con stamp real (→ G-4) · **H-9**: `sustain-loading` es un evento sin
> emisor (decisión en D-AP.10) · **H-10** medido y re-diagnosticado → **G-5**
> (el commit debe disparar en el RESULTADO, no en el clic) · segmentos = **1 de
> 6** → **D-AP.12 pasa a v2** · **D-AP.7 corregida dos veces** (mecanismo:
> resta de gain, no `channels`; criterio: incongruencia, no frecuencia) ·
> **D-AP.13** nueva: el scrubber duplica el contrato del `Slider` (§8.7).
>
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
> **Antes de escribir una línea de código**: presentar al usuario las decisiones
> de §4 y obtener firma. Lo de aquí son **propuestas razonadas, no decisiones
> tomadas** — regla A32 (`component-guide.md`) + §0.5 de
> [`component-audit.md`](../guides/component-audit.md) ("Get the user's sign-off
> on scope before writing any code").
>
> **Precondición cumplida**: corpus doctrinal leído completo antes de auditar
> (regla dura `feedback_read_full_doctrine_before_auditing`): README · overview ·
> active-architecture · CANON · morfo · sema · soma(+architecture) · eidos ·
> active-uix · active-app · packs · blocks · agent · TSC · recipe-contract ·
> vocabularies · component-guide · completion-checklist · component-audit ·
> demo-authoring · theming(reference/guide/notes/motion/motion-guide/channels/
> gradient-finish) · los 7 RFCs · book-deviations · design-text-effects ·
> design-chat-block · glossary · comparison · authoring · testing-and-tooling ·
> next-features · decisions · book-map · guía histórica · veredictos/_system/
> _naming/_cierre de la auditoría · `eidos/components/README.md` · `arts/README.md`.
---
## 0. Contexto — qué existe hoy y qué NO
El ecosistema **ya tiene un reproductor** : `media-player` , genérico
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
`<video>` + `<audio>` , cerrado como PASS en la matriz de aceptación. Y desde el
2026-07-30 tiene además **motor de audio propio** — el art
[`$sound` ](../../src/arts/sound/README.md ), extraído de `sema/chans/sound.ts` ,
con la invariante de un solo `AudioContext` cerrada y medida. Eso responde una
pregunta que este plan traía mal planteada (D-AP.11) y **no** añade trabajo al
v1: el player decide *cuándo* engancharse, no *dónde vive* el motor.
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
| Capa | Estado | Evidencia |
| --- | --- | --- |
| morfo | 534 líneas · 17 partes · 12 eventos · `expression: 'pack'` · `apg: 'none — …'` | [`morfo/components/media-player.ts` ](../../src/uix/morfo/components/media-player.ts ) |
| soma | provider request-driven + **puerto `MediaProvider`** + adaptador nativo + 14 wrappers + test 9/9 | [`soma/components/media-player/` ](../../src/uix/soma/components/media-player/ ) |
| sema | pack «voz de transporte contenida» (3 reglas) | [`sema/components/media-player.ts` ](../../src/uix/sema/components/media-player.ts ) |
| eidos | recipe *Lumière* (11.7 KB) + 17 wrappers + `DefaultControls` | [`eidos/components/media-player/` ](../../src/uix/eidos/components/media-player/ ) |
| demo | 9 pestañas, grupo **Media** del sidebar | `web/routes/uix/components/media-player/+page.svelte` |
**El modo audio existe, pero es un reproductor de vídeo degenerado.** Todo el
soporte «audio» del catálogo son **tres reglas CSS** :
```
media-player.css:79 [data-media='audio'] [data-media-player-media] → height: 0
media-player.css:161 [data-media='audio'] [data-media-player-title] → position: static
media-player.css:191 [data-media='audio'] [data-media-player-controls] → position: static
```
Es decir: se colapsa el vídeo a cero y se desabsolutizan dos cajas. **No hay
nada de lo que hace que un reproductor de sonido sea un reproductor de sonido**:
identidad de la pieza (carátula · título · artista), waveform, velocidad de
reproducción visible, capítulos, formas compactas, integración con el SO.
### 0.1 Hallazgos de la auditoría del estado actual (evidencia leída)
| ID | Hallazgo | Evidencia | Consecuencia |
| --- | --- | --- | --- |
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **H-1** ✅ **CONFIRMADO en navegador (F0)** | **Regla sema muerta.** El pack construye su selector sobre `provider` (`semaSelector(morfo,'provider',{eventName:'commit-toggle-play'})` → `[data-media-player][data-event='commit-toggle-play']` ) pero el evento estampa en **su propio botón** (`target: v.partRef('play-button')`). | `sema/components/media-player.ts:23` vs `morfo/…/media-player.ts:95` · **stamp medido** : `data-event="commit-toggle-play"` aterriza en `<button data-media-player-play-button>` | La firma de play/pause nunca se aplica. **1 de las 3 reglas del pack** : `commit-complete` y `commit-fail` sí apuntan a `provider` y funcionan. Fix de una línea (`'play-button'`), pero **cambia comportamiento** (empieza a sonar donde no sonaba) → G-4, tras la firma. |
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
| **H-2** | `SettingsButton` y `Captions` **declarados e inertes** ("render inertly until implemented", README soma:99-100). | `soma/…/README.md:96` | El menú velocidad/pista es *justo* lo que más pesa en audio (podcast). Deuda declarada que este plan recoge. |
| **H-3** | ** `Slider` no tiene pista secundaria.** Partes: `Provider · Range · Thumb · Tick` . El buffer del player es un `div` eidos-only que lee `--media-buffered` . | `morfo/components/slider.ts` | Un waveform-seek necesita el mismo hueco. Gap de framework (§8). |
| **H-4** | ** `Slider` no declara `aria-valuetext` .** Solo `aria-valuemin/max/now` . | `morfo/components/slider.ts:107-109` | El lector de pantalla anuncia *«127»* , no *«2:07 de 41:15»* . Fallo de nivel de referencia en el control MÁS importante del player. |
| **H-5** | **Formato de tiempo hecho a mano.** `formatTime()` con `padStart` local, fuera de `uix.format` / `$libs/days` . | `soma/…/media-player-provider.svelte.ts:34-40` | Contradice el eje de servicios (i18n · dígitos no arábigos · RTL). |
| **H-6** | **No hay `--media-progress`.** Se eliminó por código muerto; solo sobrevive `--media-buffered` . | README eidos:47 | Un waveform necesita el progreso como var continua otra vez (o su propio mecanismo). Decisión, no accidente. |
| **H-7** | **Cero integración MediaSession** en todo el repo. | `grep -rn mediaSession src/` = 0 | Sin controles de pantalla de bloqueo / teclas de medios del SO. |
| **H-8** | La carátula (`Poster`) es `position:absolute; inset:0; object-fit:cover` sobre una caja `16/9` . En audio esa caja **no existe** (media a `height:0` ). | `media-player.css:85-93` | La carátula de un álbum no tiene hogar geométrico hoy. |
Ninguno de los ocho invalida el `media-player` : son exactamente la superficie
que un modo audio de nivel de referencia obliga a cerrar.
---
## 1. Qué es —de verdad— un reproductor de sonido
No es «un player sin imagen». Cambian tres cosas de raíz:
1. **La superficie deja de ser el contenido.** En vídeo el marco *es* la obra y
el chrome se esconde encima (scrim, auto-hide). En audio **no hay marco** : la
UI es lo único visible, así que pasa de *transparente* a *persistente* , y su
trabajo cambia de «no estorbar» a «identificar la pieza».
2. **La identidad sustituye al fotograma.** Carátula, título, artista/serie,
capítulo. Ninguna existe hoy más allá de un `Title` de una línea.
3. **La línea de tiempo se vuelve legible.** En vídeo el scrubber se previsualiza
con miniaturas; en audio la referencia del sector es la **onda** (SoundCloud,
wavesurfer) o los **capítulos** (podcast). La barra desnuda es el mínimo, no
el estándar.
Y aparecen tres afordancias que en vídeo son secundarias y aquí son de primera
fila: **velocidad** (podcast/curso), ** ±15/30 s** (ya existe como `SeekButton` )
y **continuidad con el SO** (pantalla de bloqueo, auriculares, coche).
---
## 2. Comparativa contra referencias (F-1.2 exige ≥3; aquí 8 + la plataforma)
Consultado 2026-07-30. La columna «UIX hoy» = `media-player` en `media="audio"` .
| Capacidad | Vidstack (`DefaultAudioLayout`) | Media Chrome (`< media-controller audio > `) | Plyr | Red Hat `rh-audio-player` | wavesurfer.js 7 | react-h5-audio-player | UIX hoy | Veredicto v1 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| play/pause · ±seek · mute · volumen · tiempo | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| **Layout de audio propio** (no vídeo colapsado) | ✅ `DefaultAudioLayout` | ✅ atributo `audio` | ✅ preset audio | ✅ `layout` | n/a | ✅ | ❌ 3 reglas CSS | **implementar** |
| **Formas / tamaños** (full · compact · mini · barra) | ⚠️ `smallLayoutWhen` (2 tamaños) | ⚠️ composición manual | ❌ | ✅ full/compact/mini | n/a | ⚠️ | ❌ | **implementar** |
| **Identidad** : carátula · título · artista | ⚠️ `Title` +`Poster` | ⚠️ slots | ⚠️ | ✅ series/title/poster | ❌ | ✅ header | ⚠️ solo `Title` | **implementar** |
| **Velocidad de reproducción** (UI) | ✅ menú + `SpeedSlider` | ✅ `media-playback-rate-button` | ✅ settings.speed | ✅ | n/a | ❌ | ❌ (`setPlaybackRate` existe, sin UI) | **implementar** (cierra H-2) |
| **Waveform** | ❌ | ❌ | ❌ | ❌ | ✅ (el núcleo) | ❌ | ❌ | **componente propio** (D-AP.4) |
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **Segmentos** (reproducir `start…end` de una fuente) | ✅ `clipStartTime` / `clipEndTime` (recalcula el rango *seekable* ) | ❌ | ❌ (`markers` sólo marca) | ❌ | ⚠️ Regions (plugin) | ❌ | ❌ | **decisión — ver D-AP.12** |
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
| **Capítulos** | ✅ `SliderChapters` + `ChapterTitle` + radio-group | ⚠️ vía cues | ✅ `markers` | ✅ transcript/cues | ✅ Regions | ❌ | ❌ | **diferir v2** |
| **Transcripción** | ⚠️ captions | ⚠️ | ❌ | ✅ `rh-transcript` +`rh-cue` | ❌ | ❌ | ❌ | **diferir v2** |
| **Cola / playlist** | ❌ | ❌ | ❌ | ❌ | ❌ | ⚠️ skip prev/next | ❌ | **fuera** (D-AP.6) |
| **MediaSession (SO)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **superación** (D-AP.5) |
| **Ganancia >100 %** | ✅ `AudioGainSlider` | ❌ | ❌ | ❌ | ⚠️ Envelope | ❌ | ❌ | **diferir v2** |
| **Loop / descarga** | ⚠️ | ⚠️ | ✅ | ⚠️ | n/a | ✅ loop | ⚠️ nativo | **loop v1 (prop), descarga = app** |
| **Teclado completo** | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ (9 teclas) | ✅ |
| ** `aria-valuetext` hablado en el scrubber** | ✅ | ✅ | ✅ | ✅ | ❌ | ⚠️ | ❌ (H-4) | **gap de framework** (§8) |
| Motor enchufable (HLS/DASH/embed) | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ puerto `MediaProvider` | ✅ **ya por encima** |
| Semántica perceptiva (familia · intent · sonido/háptica) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ pack sema | ✅ **exclusivo** |
| Contrato declarativo verificable (morfo + guards) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ **exclusivo** |
**Lectura.** El transporte ya está a la altura (y el puerto de motor, por
encima). Lo que falta es **todo lo que distingue audio de vídeo** : layout
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
propio, formas, identidad, velocidad, onda, **segmentos** . Dos capacidades que
ninguna referencia tiene ya son nuestras (sema + morfo), y una que **ninguna** de
las ocho implementa —MediaSession— es la palanca barata de superación.
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Las celdas `?` de Segmentos se cerraron en F0 (2026-07-30), y el resultado
cambia el veredicto**: no es paridad, es **1 de 6** . Media Chrome no tiene
componente de clip en su `main` (revisados `src/js` y `src/js/experimental` ; el
`media-clip-selector` que circula en resultados de búsqueda es de una versión
0.x retirada) · Plyr sólo tiene `markers` , que **marcan** sin acotar · el
`rh-audio-player` de Red Hat no lo documenta · `react-h5-audio-player` no lo
lleva en sus props. Sólo wavesurfer se acerca, y por plugin (Regions). Detalle
y consecuencia en §8.5.
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
**Fuentes**: [Vidstack components ](https://vidstack.io/player/components/ ) ·
[Vidstack Default Layout ](https://vidstack.io/docs/player/components/layouts/default-layout/ ) ·
[Media Chrome ](https://github.com/muxinc/media-chrome ) ·
[Plyr ](https://github.com/sampotts/plyr ) ·
[Red Hat audio player ](https://ux.redhat.com/elements/audio-player/ ) ·
[wavesurfer.js ](https://github.com/katspaugh/wavesurfer.js/ ) ·
[react-h5-audio-player ](https://github.com/lhz516/react-h5-audio-player ) ·
[MDN MediaSession ](https://developer.mozilla.org/en-US/docs/Web/API/MediaSession ) ·
[web.dev Media Session ](https://web.dev/articles/media-session ) ·
[W3C WAI media players ](https://www.w3.org/WAI/media/av/player/ ).
---
## 3. La tensión arquitectónica (leerla antes de las decisiones)
Dos reglas del propio proyecto **apuntan en direcciones opuestas** :
- **P-4 (norma de pickers, `architecture/eidos.md` )**: *«There are no
per-variant components (`MonthPicker`, `HourPicker` ). Those forms are
`<X kind='month'>` »*. → un reproductor de audio es `<MediaPlayer media="audio">` .
- **Regla de admisión (packs/blocks)**: *lo que tiene superficie de contrato
propia se promueve a componente canónico y se compone.* → la onda, el menú de
velocidad y la identidad podrían querer casa propia.
La resolución no la dan las reglas sino **las dos referencias primarias, que
coinciden**: Vidstack mantiene UN `<media-player>` y cambia el **layout**
(`DefaultAudioLayout`); Media Chrome mantiene UN `<media-controller>` y cambia
la **composición** de la barra (atributo `audio` ). Ninguna bifurca el motor.
Y el catálogo ya tiene el precedente exacto para «una segunda raíz visual sobre
el mismo contrato headless»: **Toast / Toaster** — *«two independent roots (not
nested)… `Toaster` is NOT attached as `Toast.Toaster` because it is a competing
root, not a child»* (`architecture/eidos.md` §Special case). `DefaultControls`
ya es, hoy, una composición eidos-only del mismo tipo.
---
## 4. Decisiones (gate del usuario — SIN FIRMAR)
| ID | Cuestión | Opciones | Recomendación |
| --- | --- | --- | --- |
| **D-AP.1** | **Pertenencia** : ¿componente nuevo o modo del existente? | (a) **un motor, un contrato** : `media-player` conserva morfo/soma/sema y gana partes audio-only `optional: true` + un layout eidos · (b) `audio-player` nuevo que *compone* `media-player` (precedente picker/A27) · (c) fork con su propio provider | ** (a)**. Duplicar transporte + puerto + teclado + eventos es exactamente la reinvención que el §4 del component-guide prohíbe; las dos referencias primarias hacen (a); el morfo YA declara `data-media: ['video','audio']` y **ya** marca `optional: true` todas las partes no-núcleo (`PipButton`/`FullscreenButton` son video-only con ese mismo mecanismo — no hace falta inventar nada). (b) añade una capa de indirección sin contrato nuevo. |
| **D-AP.2** | **Puerta pública** : ¿cómo se pide un reproductor de sonido? | (a) `<MediaPlayer media="audio">` + `<MediaPlayer.AudioLayout/>` (espejo de `DefaultControls` ) · (b) **raíz competidora** `<AudioPlayer>` en eidos sobre el mismo soma (precedente Toast/Toaster) · (c) ambas | ** (c)**: `AudioLayout` como parte eidos-only (composición explícita, coherente con `DefaultControls` ) **y** `<AudioPlayer>` como raíz competidora exportada aparte —no como `MediaPlayer.AudioPlayer` — que la monta con `media="audio"` . El 90 % del uso es una etiqueta; el 10 % compone partes. Coste morfo/soma/sema: **cero** . |
| **D-AP.3** | **Formas y variaciones** (petición explícita del usuario) | (a) solo `size` canónico · (b) `variant` propio de componente · (c) `variant` + `size` + responsive | ** (c)**. `variant: 'card' \| 'row' \| 'bar' \| 'inline'` como **variante específica de componente** —vía legítima y con precedente escrito (`Banner: inline\|overlay\|persistent`, `Spinner: bars\|dots\|ring` , theming §19)— declarada en su `types.ts` , NO en `EIDOS_VARIANTS` . `size` = subconjunto canónico `xs..xl` (la recipe ya lo mapea). Ambas admiten `ResponsiveProp<T>` . Detalle en §6. |
| **D-AP.4** | **Waveform** | (a) componente canónico `Waveform` propio (ruta de 9 fases) · (b) parte eidos-only del player · (c) fuera | ** (a), pero en su propia iniciativa, DESPUÉS del v1**. Tiene superficie de contrato real (es un `role="slider"` con teclado, `data-*` y eventos `handle-*` ), y **sirve fuera del player** (nota de voz en `chat-message` , editor). Meterlo como parte eidos-only sería crecerle el privilegio al player — el antipatrón que la regla de admisión existe para impedir. Los picos: ** `peaks: number[]` que aporta la app** (ruta profesional: BBC `audiowaveform` /SoundCloud) con decodificador cliente **opcional** ; el renderer reutiliza `$libs/plots` (`scaleLinear` + `area` /`line`), no inventa matemáticas. |
| **D-AP.5** | **MediaSession (SO)** | (a) prop opt-in en el provider soma · (b) art nuevo `$media-session` · (c) app-land | ** (a) opt-in, apagado por defecto**, `mediaSession={{ title, artist, album, artwork }}` . Es *exactamente* el snapshot que el provider ya posee (`playbackState`, `setPositionState` , `seekto` ), y dejarlo fuera obliga a cada consumidor a re-derivarlo. **Riesgo declarado** : `navigator.mediaSession` es **singleton de documento** — dos players activos se pisan; el provider debe registrar solo mientras `!paused` y liberar en `pause` /`dispose`. (b) es sobreingeniería para una API sin estado propio. Ninguna de las 8 referencias lo trae → superación barata. |
| **D-AP.6** | **Cola / playlist** | (a) en el player · (b) block/app-land | ** (b), fuera de alcance**. Una cola es una composición de `Listbox` + estado de aplicación: cero contrato nuevo → tier `blocks` o app. El player expone `commit-complete` y ya. Se registra en `next-features.md` . |
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-AP.7** | **Firma sema en modo audio** — *reescrita 2026-07-30 (corrección 5)* | (a) heredar el pack actual · (b) silenciar el canal sonoro en audio · (c) **silencio mientras suena + ducking cuando no basta** | ** (c)**. El suelo sigue siendo (b) y su doctrina no cambia: D.7 (`book-deviations`) *«En eventos frecuentes, la prioridad es evitar fatiga. El silencio es una firma válida»* + antipatrón cap. 34 §10 «sonido decorativo» — si el contenido ES audio, un *tick* de UI compite con la obra. **MECANISMO CORREGIDO tras leer D.5/D.7** — la propuesta original decía `channels: ['haptic']` , y eso **viola la regla dura de la cascada** (`CLAUDE.md`: *«add character, never the intent's evaluative profile… to shift one, compose with `{ op: 'add', value: … }`»* ): tirar el canal entero borra también la carga evaluativa, así que un `commit-fail` dejaría de oírse justo cuando más falta hace. El catálogo ya tiene el patrón canonizado (D.5): `form.toggle.silent` y `tooltip.silent` **restan exactamente el gain de la familia** , de modo que lo cotidiano queda en 0 y los deltas de intent afloran. Aplicado aquí, con el **criterio final: INCONGRUENCIA, no frecuencia** (D.7: *«si el contenido ES audio, un tick de UI compite con la obra»* ; con frecuencia como criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea sería arbitraria): se cancela el gain de **lo que suena MIENTRAS suena la obra** — el transporte (`commit-toggle-play`, `commit-toggle-mute` , y el `commit-set` del scrubber vía la regla de descendientes de §7.6) y el `contact-activate` de los botones compuestos — y **se deja intacto lo que informa del estado de la obra misma** : `commit-complete` y `commit-fail` . De los dos, `commit-complete` es el discutible (su cadencia depende del contenido: una vez por episodio, muchas en una nota de voz en bucle) — se firma con esa nota. Dato que obliga a esa precisión: ** `risk` no añade gain** (sólo `threat` +0.1 y `fulfill` +0.05), así que una cancelación global también silenciaría el fallo de carga. Lo que cambia es que el silencio ya **no tiene que ser total ni de cascada** : `$sound` es dueño del earcon, así que **atenuar la UI mientras suena la obra no necesita que el player consuma el grafo** — basta con que el provider baje el master (`uix.sound.setMasterGain`) mientras `!paused` y lo restaure al pausar. **Regla de reparto** : la cascada decide *cuándo callar* , el motor decide *cuánto* . Lo que **sí** exige el grafo (D-AP.11(b), diferido) es lo contrario: agachar **el contenido** bajo un earcon, o hacerlo con precisión de muestra. **A firmar con la decisión** : el master es global, así que hay que decidir **quién lo restaura** y qué pasa con dos players a la vez — si no se resuelve, el v1 se queda en (b) puro, que ya es correcto. |
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
| **D-AP.8** | **Capítulos + transcripción** | (a) v1 · (b) v2 · (c) fuera | ** (b) v2**, registrado con disposición en el README (F-1.4). Capítulos = pista secundaria del scrubber → **depende de D-AP.4/H-3** ; transcripción = superficie de contenido con su propia a11y (candidata a componente propio, patrón Red Hat). |
| **D-AP.9** | **Responsive del layout** | (a) `ResponsiveProp` (mecanismo del framework) · (b) container queries del TSC · (c) ambos | ** (a) en v1, (b) evaluado en F6**. El TSC ya tiene el eje `container: {}` documentado como ** «a themeable axis with 0 consumers today — an open cage»** (`canon/tsc.md`): un player que vive dentro de un aside, una tarjeta o un pie de página es el **primer consumidor natural** del catálogo, y resolvería mejor que el `smallLayoutWhen` de Vidstack (que depende del contenedor, no del viewport). Abrir la jaula es un entregable con valor propio — pero no debe bloquear el v1. |
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-AP.10** | **Alcance del v1** | (a) paridad estricta con las referencias · (b) paridad + superación (MediaSession + silencio sema + container) | ** (a) + MediaSession + silencio sema**; container, waveform y —tras §8.5— **segmentos** como iniciativas hermanas. Criterio: el usuario pidió *«como mínimo al mismo nivel»* — la paridad es el suelo, no el techo, y las dos superaciones baratas caben. **Añadido en F0** : hay que decidir además qué se hace con `sustain-loading` (H-9), que hoy es un evento declarado sin emisor — cablearlo o retirarlo. |
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
| **D-AP.11** | ** ¿El player consume `$sound` ?** — *escrita 2026-07-30 contra el art ya existente; su premisa anterior («no hay motor, ¿dónde lo ponemos?») era falsa* | (a) **no consumirlo en v1** : el player sigue sobre `<audio>` nativo y `$sound` sólo suena la UI · (b) **consumirlo desde v1** : colgar el `MediaElementAudioSourceNode` de `uix.sound.context` · (c) consumirlo sólo cuando el consumidor lo pida (prop opt-in) | ** (a) en v1, (b) como iniciativa hermana con `Waveform` .** El art existe y la invariante de contexto único está cerrada y medida ([`PLAN-sound-engine.md` §15](./PLAN-sound-engine.md)), así que la pregunta ya **no** es *dónde vive el motor* sino *cuándo engancharse* . Enganchar `<audio>` a Web Audio no es gratis: `createMediaElementSource` **redirige el audio del elemento al grafo de forma irreversible** , y con una fuente cross-origin sin CORS lo deja MUDO. Un v1 cuyo trabajo es *layout, identidad, formas, velocidad y segmentos* no necesita el grafo; quien lo necesita es la onda, el visualizador, la ganancia >1 y **el ducking del contenido** — todos diferidos. **Consecuencia declarada** : con (a), el ducking de D-AP.7 sólo puede atenuar la **UI** (`setMasterGain`), nunca la obra; y el punto de bucle del clip (D-AP.12) sigue atado a `timeupdate` . Lo que **sí** entra en v1 es la regla escrita: si algún día se engancha, es a `uix.sound.context` , jamás a un `new AudioContext()` propio. |
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-AP.12** | **Segmentos** (`clip`) — *fila nueva 2026-07-30; * *recomendación CAMBIADA tras verificar §8.5**: se presentó como «paridad, Vidstack los trae» y la medición dice **1 de 6** , no paridad* | (a) **v2, con el diseño de §7.5 congelado** · (b) `clip = { start, end, loop? }` en el v1 · (c) `regions` genéricas | ** (a) v2** — cambio de criterio con los datos delante: sólo Vidstack lo trae (Media Chrome, Plyr, Red Hat y react-h5 **no** ; wavesurfer por plugin), así que no es el suelo que justificaba meterlo en un v1 ya grande; su única implementación arrastra un bug móvil abierto que **no podemos reproducir aquí** ; y la capa de proyección absoluto↔relativo toca el `Time` , el `aria-valuetext` y el dominio del slider — superficie de bug en el control más importante. **Diferir es barato y no acorrala** : sin `clip` , el dominio es `0…duration` y el tiempo es el de la fuente; añadirlo luego es un modo nuevo, no un cambio de significado para quien ya lo usa. Si tienes un caso de uso concreto hoy, la opción (b) sigue en pie con este diseño: capa de proyección **absoluto↔relativo** (dominio del slider `0…end− start` , `timeBase: 'clip' \| 'source'` en `Time` , **misma base** en `aria-valuetext` ) · **clamp en el provider, no en el puerto** (el puerto `MediaProvider` no se contamina: sigue hablando de la fuente entera) · `commit-complete` dispara en el fin del **clip** — mismo evento, frontera movida · **con `loop` NO hay `commit-complete`** : un bucle esperado no es una ocurrencia (regla B.5). **Limitación declarada** : `timeupdate` llega ~4 veces/s, así que sin Web Audio el punto de bucle es impreciso (~±125 ms) — se documenta, no se disimula. **Riesgo a probar en F0** : bug abierto de clip en Chrome/Safari móvil (vidstack#1195). (c) queda **descartado** y `chapters` **v2** , los dos por depender de G-2. |
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-AP.13** | **El scrubber duplica el contrato del `Slider`** — *fila nueva tras resolver la postura de participación (§8.7)* | (a) status quo: cada arrastre emite `commit-set` (Slider) **y** `commit-set-time` (player) · (b) **retirar `commit-set-time` del morfo y delegar en el `commit-set` del `Slider` embebido** · (c) callar el del Slider | ** (b)**. Son el mismo suceso declarado dos veces: `onValueCommit` → `seek()` → `trigger('commit-set-time')` ([time-slider:45](../../src/uix/soma/components/media-player/components/media-player-time-slider.svelte) + [provider:322 ](../../src/uix/soma/components/media-player/media-player-provider.svelte.ts ) — dos líneas leídas; el `commit-set` del Slider, medido en navegador). Es literalmente el rationale de radio-cards (*«declararlos aquí duplicaría el contrato»*, doctrina de participación, postura 3), y **el propio provider ya practica el patrón** : `setVolume` no emite — *«the perceptual signal is the composed Slider's»* . (c) es imposible sin degradar el `Slider` para todo el catálogo. Coste: retirar el evento del morfo en F2, con su test — cambio de contrato de un componente PASS, declarado. |
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
---
## 5. Arquitectura propuesta (tras firma de D-AP.1/2)
```
morfo/components/media-player.ts +6 partes optional (audio-only) · +2 eventos · +1 texto
│ (contrato ÚNICO — no se bifurca)
soma/components/media-player/ +sub-providers de las partes nuevas
│ +MediaSession opt-in en el provider raíz
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
│ +clip (§7.5) SÓLO si D-AP.12(b) — recomendación: v2
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
│ +formatTime → uix.format (H-5)
sema/components/media-player.ts +regla de silencio en audio (D-AP.7) · fix H-1
│
eidos/components/media-player/ +audio-layout.svelte (composición, espejo de DefaultControls)
│ +recipe: variantes card/row/bar/inline
eidos/components/audio-player/ raíz competidora < AudioPlayer > (Toast/Toaster)
```
**Lo que NO se toca**: el puerto `MediaProvider` (ya es el acierto del
componente), el modo vídeo, el recipe Lumière existente, la demo de
`media-player` .
**Dogfooding obligatorio** (§4 component-guide): la barra compone `IconButton`
(ya lo hace), los sliders componen `Slider` (ya), el menú de velocidad compone
`DropdownMenu` , la carátula compone `Image` + `AspectRatio` , la identidad
compone `Text` /`Heading`, el estado vacío compone `EmptyState` . **Cero
primitivas inline.**
---
## 6. Formas y variaciones (el punto 3 del encargo, en detalle)
Cuatro formas, un solo contrato. La variante decide **qué partes monta el
layout y cómo se disponen**, nunca qué puede hacer el player.
| `variant` | Forma | Partes que monta | Contexto de uso | Referencia del sector |
| --- | --- | --- | --- | --- |
| ** `card` ** | Tarjeta vertical: carátula grande arriba, identidad, scrubber, transporte centrado | Artwork · Title · Artist · TimeSlider · Time× 2 · Play · Seek± · Mute · Volume · Rate | Página de episodio, ficha de álbum, landing | Apple Podcasts / Bandcamp |
| ** `row` ** | Fila horizontal: carátula pequeña a la izquierda, identidad + scrubber apilados | Artwork(sm) · Title · Artist · TimeSlider · Play · Seek± · Rate | Lista de episodios, resultado de búsqueda, feed | SoundCloud track |
| ** `bar` ** | Barra persistente a ancho completo (composición pensada para `sticky` /pie) | Artwork(xs) · Title · Play · Seek± · TimeSlider · Time · Mute · Volume · Rate | Reproductor global de la app | Spotify bottom bar |
| ** `inline` ** | Mínima, en línea con el texto: play + scrubber + tiempo | Play · TimeSlider · Time | Nota de voz en un chat, cita de audio en un artículo | mensaje de voz de WhatsApp |
Ejes ortogonales que **componen** con la variante:
- **`size`** (`xs..xl`, `ResponsiveProp` ) — densidad; la recipe ya mapea el
bundle `--size-{k}-*` . Regla dura: `md` no cambia de significado por viewport
(theming §5); lo que cambia es **qué size se elige** .
- **`color`** — el sistema completo (rol / intent / 33 escalas / CSS crudo =
`ComponentColorProp` ), sin subconjuntos: decisión «abrir la jaula del color»
(theming §25, guard en `recipe-css-contract.test.ts` ).
- **`variant` responsive** — `variant={{ base: 'inline', md: 'row', lg: 'card' }}` .
- **`data-depth`** — la barra persistente estampa su plano (`raised`/`overlay`),
no inventa sombras (contrato de recipe R-4.1).
**Regla de forma** (espejo de la que rige `blocks` ): la variante **no puede
añadir comportamiento**. Si una forma necesitase un evento, un `data-*` que el
CSS deba seleccionar o una obligación a11y nueva, eso se promueve al contrato
—no se le crece el privilegio al layout.
---
## 7. Contrato propuesto (borrador para F1 — se cierra con la firma)
### 7.1 Partes nuevas (todas `optional: true`, audio-only)
| Parte | kebab | archetype | Elemento | Rol |
| --- | --- | --- | --- | --- |
| `Artwork` | `artwork` | `image` | `img` | Carátula. Compone `Image` +`AspectRatio`; `alt` real (no decorativa como `Poster` , porque en audio **es** la única representación visual de la obra). |
| `Artist` | `artist` | `description` | `div` | Autoría / serie. |
| `RateButton` | `rate-button` | `trigger` | `button` | Abre el menú de velocidad (compone `DropdownMenu` ) o cicla el preset. Cierra H-2 junto a `SettingsButton` . |
| `Identity` | `identity` | `group` | `div` | Agrupador de Artwork+Title+Artist (le da a la variante un bloque que mover). |
| `Transport` | `transport` | `group` | `div` | Agrupador de los botones de transporte, hermano de `Controls` . |
| `LiveIndicator` | `live-indicator` | `indicator` | `div` | Emisión en directo (`data-live` ya existe en el provider). |
### 7.2 Eventos nuevos
| Evento | Familia · verbo | Intent | Target | `sequence` | Por qué |
| --- | --- | --- | --- | --- | --- |
| `commit-set-rate` | `commit` · `set` | `neutral` | `rate-button` | `post` | Fijar velocidad **es** un valor aplicado (`commit.set`, BOOK_CANON A.5: *«un valor, criterio o parámetro ha quedado aplicado»* — mismo caso que sort/slider). |
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| ~~`sustain-end`~~ **DESCARTADO en F0** | `sustain` · `end` | — | `provider` | `coincident` | La pregunta estaba mal planteada: ** `sustain-loading` no se limpia ni deja de limpiarse porque NUNCA SE EMITE**. No hay un solo `trigger('sustain-loading')` en el provider (sólo `commit-complete` , `commit-fail` , `commit-toggle-play` , `commit-set-time` , `commit-toggle-mute` , `shift-*` , `commit-toggle-captions` ), y el README de soma afirma que «clears on `canplay` » un ciclo que no existe. No se puede cerrar un proceso que no se abre → **H-9** . |
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
Nada más. `loop` es prop nativa sin evento (B.5). El cambio de carátula/título es
dato, no ocurrencia.
### 7.3 Teclado
Se hereda el contrato del provider (9 teclas). Se añaden, alineadas con el
sector (`j`/`l` de YouTube, `<` /`>` de velocidad):
| Tecla | Acción | Nota |
| --- | --- | --- |
| `j` / `l` | seek ∓ `seekStep` | alias de ←/→, convención del sector |
| `<` / `>` | bajar / subir velocidad | requiere `RateButton` montado |
| `0` – `9` | saltar al 0– 90 % | **solo cuando `duration` es finita** (no en directo) |
### 7.4 A11y — el listón
- **`aria-valuetext` hablado en el scrubber** (cierra H-4): *«2 minutos 7
segundos de 41 minutos»*, no «127». Es **gap de framework** (§8).
- `Artwork` con `alt` real; el resto de la identidad como texto real (nunca
`aria-label` sobre `div` s vacíos).
- `commit-fail` ya declara `requiresLiveRegion` + `reducedMotionFallback: 'text'`
— se conserva.
- Contraste: la barra `bar` sobre superficie arbitraria consume `data-depth` , no
color inventado (R-2.1/R-4.6).
- Sin robo de foco al cambiar de pista.
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
### 7.5 Segmentos (`clip`) — el contrato (D-AP.12)
| Pieza | Regla |
| --- | --- |
| Prop | `clip={{ start, end, loop? }}` en el **provider** soma. Ausente = fuente entera (comportamiento de hoy, sin cambios) |
| Puerto | ** `MediaProvider` no se toca**: sigue hablando de la fuente entera. El clamp y la proyección viven en el provider |
| Dominio del slider | `0…end− start` . Un scrubber que empieza en 0 es la única lectura honesta para quien no sabe que hay una fuente mayor detrás |
| `Time` | `timeBase: 'clip' \| 'source'` — y `aria-valuetext` usa **la misma base** que el `Time` visible. Dos bases distintas en la misma pantalla es un fallo de a11y, no una opción |
| Fin | `commit-complete` dispara al llegar a `end` — el mismo evento, la frontera movida |
| `loop` | **NO** dispara `commit-complete` : un bucle esperado no es una ocurrencia (B.5) |
| Pista secundaria | La ventana del clip se pinta en la pista secundaria de `Slider` — **G-2** , uno de sus tres consumidores |
| Limitación | `timeupdate` llega ~4 veces/s: sin Web Audio el punto de bucle es impreciso (~±125 ms). Se declara en el README, no se disimula |
### 7.6 Dos invariantes que este componente es el primero en necesitar
1. ** `prefs.sound` NUNCA toca el volumen del contenido.** Gobierna el sonido de
**UI** (sema: earcons, `off` / `reduce` ). Un reproductor de sonido es el
primer componente del catálogo donde «sonido» significa dos cosas, y
confundirlas silenciaría la obra al bajar una preferencia de interfaz. El
volumen del contenido es `volume` / `muted` del provider, y nada más.
2. **El silencio de UI en audio (D-AP.7) tiene que alcanzar al `Slider`
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
compuesto** — ✅ **CONFIRMADO en F0** . El `TimeSlider` compone
`Slider.Provider` ([media-player-time-slider.svelte:37](../../src/uix/soma/components/media-player/components/media-player-time-slider.svelte)),
y el provider del slider adjunta `resolveSliderDragSound(...)` como override
**en cada `handle-drag`** ([slider-provider.svelte.ts:231](../../src/uix/soma/components/slider/slider-provider.svelte.ts)).
Stamp medido arrastrando el scrubber: `handle-drag` y `commit-set` sobre
`[data-slider]` . Con el sonido activo, el scrubber sonoriza **por encima de
la obra**. La regla `[data-media-player][data-media='audio'] [data-slider]`
es correcta y funciona: `Element.matches()` evalúa descendencia.
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** : la mecánica citada cambió el 2026-08-06 — el slider ya no
adjunta `resolveSliderDragSound` (RETIRADO,
[`AUDIT-docs-code-ledger.md` §D10 ](./AUDIT-docs-code-ledger.md )) sino el
nombre `'step'` ; la conclusión (el silencio de UI alcanza al Slider
compuesto) se sostiene igual, hoy por `channels: ['haptic']` (D-AP2.7 v2).
test(sema): un botón compuesto suena DOS veces, medido (H-10)
En F0 observé que el botón de play estampa dos señales por clic. Afirmé de ahí
que sonaría dos veces, y eso era un salto: el motor tiene un árbitro de
dominancia precisamente para esos casos. Medido: **4 osciladores, dos earcons**.
Por qué el árbitro no los colapsa. `occurrenceRank = evaluable×10 + activación`:
`contact-activate` del `Button` es estructural y sin intent → **0**;
`commit-toggle-play` del player es evaluable con intent `neutral` → **10**. La
regla silencia a un recién llegado sólo si una ocurrencia YA ACTIVA lo supera, y
aquí llegan en orden 0 → 10: cuando entra el segundo, el activo no lo supera; y
el primero ya había sonado. El modelo asume que la señal de más rango llega
antes o sola — **el caso inverso, a microsegundos, no está arbitrado**. Tampoco
hay pack de `button`, así que `contact-activate` usa la base de familia (800 Hz,
60 ms) y se solapa con el commit de 100 ms que entra encima.
**No es del media-player**: es el patrón de composición del catálogo — la
primitiva acusa el contacto, el componente confirma el valor. Cualquiera que
componga `Button` y dispare su propio evento suena dos veces con el sonido
activo. El test lo pinea donde se puede oír el camino entero
(`sound-e2e.test.ts`), y la decisión —regla nueva en el árbitro, canon que
declare el solape intencionado, o packs que silencien `contact` bajo componentes
que hablan por sí mismos— queda fichada en el plan como materia de
`book-deviations.md`, no de una sesión.
Para el reproductor cambia una cosa y es la que importa para D-AP.7: el silencio
en modo audio tiene que cubrir las DOS señales, y ninguna vive en el provider.
sema + art 17 suites / 205 tests · `docs:check` 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
3. **Y no basta con el `Slider`** (hallazgo nuevo de F0, **H-10** ): el botón de
play emite **dos** señales por clic — `contact-activate` del `Button`
compuesto **y** `commit-toggle-play` del player, ambas estampadas en el
propio `<button>` . Una regla de silencio que seleccione el provider no
alcanza a ninguna: el silencio en audio debe cubrir **los descendientes** .
Y no son dos estampas con un solo sonido — **medido: dos earcons**
([`sound-e2e.test.ts`](../../src/uix/sema/chans/sound-e2e.test.ts), 4
osciladores). Detalle en §8.6.
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
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
---
## 8. Gaps de framework a promover ANTES (regla de admisión)
Se construyen **primero** , en el canon, y el player los compone. Ninguno es un
parche local del player.
| # | Gap | Dónde | Justificación 2-de-3 | Coste |
| --- | --- | --- | --- | --- |
| **G-1** | `aria-valuetext` en el morfo de `Slider` (`v.propRef('valueText')`) + prop `valueText` en soma | `morfo/components/slider.ts` + provider | `aria` es campo morfo por definición (soma emite, eidos puede seleccionar) | bajo |
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
| **G-2** 🔴 **BLOQUEANTE de v1** | Pista secundaria («buffered»/«secondary range») en `Slider` | `morfo` + `soma` + recipe | Lo consumen soma (valor) y eidos (pintura) → 2-de-3 ✓. Y ya tiene **tres** consumidores, no uno: el **buffer** , la **ventana del clip** (D-AP.12) y los **capítulos** (v2). Hoy lo simula un `div` eidos-only en media-player y lo simularía otro en `Waveform` — dos copias = la señal de que falta la pieza; tres = que bloquea | medio |
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
| **G-3** | `formatDuration` en `$libs/days` (o `uix.format` ) | `$libs/days` | Regla A23: si es puro y reutilizable, va a la lib — nunca reimplementar dentro de soma | bajo |
fix(docs): H-10 mal diagnosticado — el canon separa las dos voces, el player las junta
Cerré H-10 hace un commit diciendo que el doble earcon era «el patrón de
composición del catálogo» y que la decisión —regla nueva en el árbitro, o canon
que declarase el solape intencionado— era materia de `book-deviations.md`. Es al
revés: **el canon ya lo prescribe, y quien lo incumple es el media-player.**
El README del `Button` lo dice citando el libro (cap. 22 §8 y §10):
`contact-activate` es `sequence: 'pre'` y sólo acusa la RECEPCIÓN del gesto; la
consecuencia evaluativa la dispara el consumidor **cuando el resultado
aterriza** — *«the celebration sound plays at the moment the save actually
resolves, not when the click is received»*. Dos sonidos, sí, pero **separados en
el tiempo** y con dos significados. En el diseño no hay solape.
El solape lo mete el player: `togglePlay()` dispara `commit-toggle-play`
síncronamente dentro del handler del clic, no cuando el medio arranca — y el
resultado real llega después, en el evento `play`/`pause` que el provider YA
recibe (ahí arranca el bucle de progreso, y `commit-complete` ya cabalga `ended`
con ese patrón exacto). Mismo defecto en `toggleMute()` y `seek()`;
`toggleFullscreen` y `togglePip` lo hacen bien, esperando a la plataforma.
Consecuencias:
- H-10 deja de ser materia de canon y pasa a **G-5** en §8: mover el commit del
clic al evento de resultado, aplicando el patrón que el propio fichero usa.
- **El árbitro de dominancia se queda como está.** Nunca tuvo que arbitrar dos
ocurrencias que el diseño no quiere simultáneas; proponer una regla nueva era
arreglar el síntoma en la capa equivocada.
- El test conserva la medición (dos ocurrencias en el mismo tick = dos earcons,
4 osciladores) porque el dato es correcto y vale como guardia; lo que se
reescribe es su lectura.
Para D-AP.7 no cambia lo demás: ninguna de las dos señales vive en el provider,
así que el silencio en modo audio tiene que cubrir descendientes.
sema e2e 5/5 · `docs:check` 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **G-4** | ~~Verificar~~ ✅ / arreglar **H-1** (selector del pack sema, confirmado con stamp) | `sema/components/media-player.ts` | fix de una línea: `'provider'` → `'play-button'` | trivial |
| **G-5** | **H-10** — el commit se dispara en el clic, no cuando el resultado aterriza, así que se solapa con el `contact-activate` del `Button` y suenan dos earcons | `soma/…/media-player-provider.svelte.ts` | El libro (cap. 22 §8) prescribe la separación temporal y `commit-complete` **ya** cabalga el evento `ended` : es aplicar el patrón que el propio fichero usa, a `togglePlay` / `toggleMute` / `seek` | bajo |
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
`Waveform` (D-AP.4) **no** está en esta tabla: no es un gap del player sino una
iniciativa hermana con su propia ruta de 9 fases.
---
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 8.5 F0 · verificaciones ejecutadas (2026-07-30)
La mitad de F0 que no necesita firma, hecha antes de pedirla — porque tres
resultados cambian lo que hay que firmar.
| # | Qué se pidió verificar | Resultado |
| --- | --- | --- |
| 1 | **H-1** , con stamp real | ✅ **Confirmado.** `data-event="commit-toggle-play"` estampa en `<button data-media-player-play-button>` ; el selector del pack apunta al provider y no puede casar. Muerta **1 de 3** reglas |
| 2 | ¿Sonoriza el ** `Slider` compuesto** al arrastrar? | ✅ **Sí** , por composición: `TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada `handle-drag` . Stamp medido sobre `[data-slider]` |
| 3 | ¿Se auto-limpia ** `sustain-loading` **? | ⚠️ **La pregunta era inválida: no se emite nunca** (H-9). `sustain-end` queda descartado |
| 4 | Celdas `?` de **Segmentos** | ⚠️ **1 de 6** , no paridad. Sólo Vidstack; wavesurfer por plugin |
| 5 | Reproducir **vidstack#1195** | ⚠️ **No reproducible aquí** (requiere iOS real). Caracterizado abajo |
### H-9 · `sustain-loading` es superficie de contrato MUERTA
Declarado en el morfo ([media-player.ts:176](../../src/uix/morfo/components/media-player.ts)),
verificado por el test del morfo, documentado en el README de soma como
«Drives the spinner + `sustain-loading` » y **jamás emitido** : `grep` de
`sustain-loading` en todo el repo no encuentra ni un `trigger` . El spinner sí
funciona — lo mueve `data-buffering` , que es estado, no señal.
Es un evento declarado sin emisor. Antes de decidir `sustain-end` hay que
decidir qué hacer con él: **cablearlo** (el provider ya tiene `waiting` , así que
son dos líneas + el `clear` en `canplay` ) o **retirarlo del morfo** . Va al gate
como parte de D-AP.10, no lo decido yo.
test(sema): un botón compuesto suena DOS veces, medido (H-10)
En F0 observé que el botón de play estampa dos señales por clic. Afirmé de ahí
que sonaría dos veces, y eso era un salto: el motor tiene un árbitro de
dominancia precisamente para esos casos. Medido: **4 osciladores, dos earcons**.
Por qué el árbitro no los colapsa. `occurrenceRank = evaluable×10 + activación`:
`contact-activate` del `Button` es estructural y sin intent → **0**;
`commit-toggle-play` del player es evaluable con intent `neutral` → **10**. La
regla silencia a un recién llegado sólo si una ocurrencia YA ACTIVA lo supera, y
aquí llegan en orden 0 → 10: cuando entra el segundo, el activo no lo supera; y
el primero ya había sonado. El modelo asume que la señal de más rango llega
antes o sola — **el caso inverso, a microsegundos, no está arbitrado**. Tampoco
hay pack de `button`, así que `contact-activate` usa la base de familia (800 Hz,
60 ms) y se solapa con el commit de 100 ms que entra encima.
**No es del media-player**: es el patrón de composición del catálogo — la
primitiva acusa el contacto, el componente confirma el valor. Cualquiera que
componga `Button` y dispare su propio evento suena dos veces con el sonido
activo. El test lo pinea donde se puede oír el camino entero
(`sound-e2e.test.ts`), y la decisión —regla nueva en el árbitro, canon que
declare el solape intencionado, o packs que silencien `contact` bajo componentes
que hablan por sí mismos— queda fichada en el plan como materia de
`book-deviations.md`, no de una sesión.
Para el reproductor cambia una cosa y es la que importa para D-AP.7: el silencio
en modo audio tiene que cubrir las DOS señales, y ninguna vive en el provider.
sema + art 17 suites / 205 tests · `docs:check` 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 8.6 · H-10 — un botón compuesto suena DOS veces, y no es cosa del player
Empezó como una observación del stamp y acabó siendo una medición: un clic en el
botón de play produce **dos earcons** , no dos estampas con una voz.
**Por qué no lo colapsa el árbitro de dominancia.** Los rangos son
`occurrenceRank = evaluable× 10 + activación` :
| Señal | Familia | Intent | Rango |
| --- | --- | --- | --- |
| `contact-activate` (del `Button` ) | `contact` → estructural | ninguno | **0** |
| `commit-toggle-play` (del player) | `commit` → evaluable | `neutral` | **10** |
La regla silencia a un recién llegado **sólo si una ocurrencia ya activa lo
supera**. Aquí llegan en orden 0 → 10: cuando entra el segundo, el activo (0) no
lo supera, así que pasa; y el primero ya había sonado. El modelo asume que la
señal de más rango llega antes o sola — **el caso inverso, a microsegundos, no
está arbitrado**. Y no hay pack de `button` que module `contact-activate` : usa
la base de familia (800 Hz, 60 ms, gain 0.25), que se solapa con el commit de
100 ms que entra encima.
fix(docs): H-10 mal diagnosticado — el canon separa las dos voces, el player las junta
Cerré H-10 hace un commit diciendo que el doble earcon era «el patrón de
composición del catálogo» y que la decisión —regla nueva en el árbitro, o canon
que declarase el solape intencionado— era materia de `book-deviations.md`. Es al
revés: **el canon ya lo prescribe, y quien lo incumple es el media-player.**
El README del `Button` lo dice citando el libro (cap. 22 §8 y §10):
`contact-activate` es `sequence: 'pre'` y sólo acusa la RECEPCIÓN del gesto; la
consecuencia evaluativa la dispara el consumidor **cuando el resultado
aterriza** — *«the celebration sound plays at the moment the save actually
resolves, not when the click is received»*. Dos sonidos, sí, pero **separados en
el tiempo** y con dos significados. En el diseño no hay solape.
El solape lo mete el player: `togglePlay()` dispara `commit-toggle-play`
síncronamente dentro del handler del clic, no cuando el medio arranca — y el
resultado real llega después, en el evento `play`/`pause` que el provider YA
recibe (ahí arranca el bucle de progreso, y `commit-complete` ya cabalga `ended`
con ese patrón exacto). Mismo defecto en `toggleMute()` y `seek()`;
`toggleFullscreen` y `togglePip` lo hacen bien, esperando a la plataforma.
Consecuencias:
- H-10 deja de ser materia de canon y pasa a **G-5** en §8: mover el commit del
clic al evento de resultado, aplicando el patrón que el propio fichero usa.
- **El árbitro de dominancia se queda como está.** Nunca tuvo que arbitrar dos
ocurrencias que el diseño no quiere simultáneas; proponer una regla nueva era
arreglar el síntoma en la capa equivocada.
- El test conserva la medición (dos ocurrencias en el mismo tick = dos earcons,
4 osciladores) porque el dato es correcto y vale como guardia; lo que se
reescribe es su lectura.
Para D-AP.7 no cambia lo demás: ninguna de las dos señales vive en el provider,
así que el silencio en modo audio tiene que cubrir descendientes.
sema e2e 5/5 · `docs:check` 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**CORRECCIÓN (misma sesión, tras leer el contrato del `Button` ).** Escribí que
esto era «el patrón de composición del catálogo» y que la decisión era del
canon. **Es al revés: el canon ya lo prescribe, y es el player quien lo
incumple.**
El provider del `Button` lo documenta citando el libro
([README:72-91](../../src/uix/soma/components/button/README.md), cap. 22 §8 y §10):
`contact-activate` es `sequence: 'pre'` y sólo acusa **la recepción del gesto** ;
la consecuencia evaluativa la dispara el consumidor **cuando el resultado
aterriza** — *«the celebration sound plays at the moment the save actually
resolves, not when the click is received»*. Es decir: **dos sonidos separados en
el tiempo, con dos significados distintos**. No hay solape en el diseño.
El solape lo produce el player: `togglePlay()` dispara `commit-toggle-play`
**síncronamente dentro del handler del clic**
([media-player-provider.svelte.ts:319](../../src/uix/soma/components/media-player/media-player-provider.svelte.ts)),
no cuando el medio arranca. El resultado real llega después, en el evento `play`
/ `pause` del elemento — que **el provider ya recibe** (ahí mismo arranca el
bucle de progreso, y `commit-complete` ya cabalga `ended` con ese patrón
exacto).
Mismo defecto en `toggleMute()` (`commit-toggle-mute`) y en `seek()`
(`commit-set-time`). `toggleFullscreen` y `togglePip` **sí** lo hacen bien:
esperan la promesa de la plataforma antes de disparar.
Así que **H-10 no es materia de canon ni del árbitro de dominancia** : es un fix
en el provider del player — mover el commit del clic al evento de resultado, que
es donde el libro lo pone. Entra como **G-5** en §8, y el árbitro se queda como
está: nunca tuvo que arbitrar dos ocurrencias que el diseño no quería
simultáneas.
Para **D-AP.7** sigue en pie lo demás: ninguna de las dos señales vive en el
provider, así que el silencio en modo audio tiene que cubrir descendientes.
test(sema): un botón compuesto suena DOS veces, medido (H-10)
En F0 observé que el botón de play estampa dos señales por clic. Afirmé de ahí
que sonaría dos veces, y eso era un salto: el motor tiene un árbitro de
dominancia precisamente para esos casos. Medido: **4 osciladores, dos earcons**.
Por qué el árbitro no los colapsa. `occurrenceRank = evaluable×10 + activación`:
`contact-activate` del `Button` es estructural y sin intent → **0**;
`commit-toggle-play` del player es evaluable con intent `neutral` → **10**. La
regla silencia a un recién llegado sólo si una ocurrencia YA ACTIVA lo supera, y
aquí llegan en orden 0 → 10: cuando entra el segundo, el activo no lo supera; y
el primero ya había sonado. El modelo asume que la señal de más rango llega
antes o sola — **el caso inverso, a microsegundos, no está arbitrado**. Tampoco
hay pack de `button`, así que `contact-activate` usa la base de familia (800 Hz,
60 ms) y se solapa con el commit de 100 ms que entra encima.
**No es del media-player**: es el patrón de composición del catálogo — la
primitiva acusa el contacto, el componente confirma el valor. Cualquiera que
componga `Button` y dispare su propio evento suena dos veces con el sonido
activo. El test lo pinea donde se puede oír el camino entero
(`sound-e2e.test.ts`), y la decisión —regla nueva en el árbitro, canon que
declare el solape intencionado, o packs que silencien `contact` bajo componentes
que hablan por sí mismos— queda fichada en el plan como materia de
`book-deviations.md`, no de una sesión.
Para el reproductor cambia una cosa y es la que importa para D-AP.7: el silencio
en modo audio tiene que cubrir las DOS señales, y ninguna vive en el provider.
sema + art 17 suites / 205 tests · `docs:check` 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 8.7 · La postura de participación, resuelta — no hay abismo bajo el gate
La última pregunta de la sesión —¿es correcta `expression: 'pack'` para un
compuesto de `Button` s y `Slider` s?— la responde la **doctrina de
participación** ([`sema.md` §«When a component deserves a pack»](../architecture/sema.md))
sin tocar la premisa de D-AP.1:
| Evento del player | Veredicto |
| --- | --- |
| `commit-complete` · `commit-fail` · `shift-*` · `commit-toggle-captions` · `commit-toggle-play` (el **resultado** ; el `Button` sólo posee el contacto) | **Suyos** — ningún hijo los declara. `'pack'` correcto; el guard S11d además lo **exige** mientras exista `sema/components/media-player.ts` |
| `commit-set-time` | **La única duplicación real** : mismo suceso que el `commit-set` del `Slider` embebido. El rationale de radio-cards aplica literal → **D-AP.13** |
| El timing de play/mute | No es de participación: es **G-5** (el patrón correcto ya vive en `commit-complete` / `ended` ) |
Y el propio provider ya practica la delegación correcta **dos veces** :
`setVolume` (*«the perceptual signal is the composed Slider's»*) y `scrubTo`
(scrub en vivo, sin señal). El player es un **híbrido legítimo** con una
duplicación y un defecto de timing — no un componente sobre premisas rotas.
Consecuencia para el gate: **D-AP.1 queda intacta** ; se añade D-AP.13; nada más
se reabre.
docs(process): F0 del reproductor — las verificaciones, hechas antes de pedir la firma
La mitad de F0 que no necesita firma, ejecutada primero, porque tres de los
cinco resultados cambian lo que hay que firmar. Sin código del player.
- **H-1 confirmado con stamp real.** `data-event="commit-toggle-play"` aterriza
en `<button data-media-player-play-button>`; el pack selecciona el provider y
no puede casar. Es **1 de las 3 reglas** —`commit-complete` y `commit-fail`
apuntan bien—, y el fix de una línea **cambia comportamiento** (empieza a
sonar donde no sonaba), así que espera a la firma como G-4.
- **El `Slider` compuesto sí sonoriza**, por composición y sin excepción:
`TimeSlider` → `Slider.Provider` → `resolveSliderDragSound` en cada
`handle-drag`. Stamp medido sobre `[data-slider]`. La regla propuesta funciona
porque `Element.matches()` evalúa descendencia.
- **Hallazgo nuevo**: el botón de play emite **dos** señales por clic —
`contact-activate` del `Button` compuesto y `commit-toggle-play` del player,
ambas en el propio `<button>`. Una regla de silencio anclada al provider no
alcanza a ninguna: el silencio en audio tiene que cubrir los DESCENDIENTES.
- **H-9 — `sustain-loading` es superficie de contrato muerta.** La pregunta de
F0 («¿se auto-limpia?») era inválida: **no se emite nunca**. Está declarado en
el morfo, verificado por su test y documentado en el README de soma como si
viviera; no hay un solo `trigger`. El spinner lo mueve `data-buffering`, que
es estado. `sustain-end` queda descartado, y qué hacer con el evento —cablearlo
o retirarlo— entra en el gate.
- **Segmentos: 1 de 6, no paridad.** Media Chrome no tiene componente de clip en
su `main` (revisados `src/js` y `experimental`; el `media-clip-selector` que
circula en buscadores es de una 0.x retirada), Plyr sólo `markers`, Red Hat no
lo documenta, react-h5 no lo lleva. Sólo wavesurfer, por plugin.
**D-AP.12 cambia de recomendación: v2**, con el diseño de §7.5 congelado.
Diferirlo no acorrala —sin `clip` el dominio es `0…duration`, añadirlo luego
es un modo nuevo, no un cambio de significado.
- **vidstack#1195 caracterizado, NO reproducido**: abierto, sin respuesta,
iOS 17.3.1 en Chrome y Safari móviles, escritorio sano, `webkit blob resource
error 1`. Apunta a fuentes `blob:` + su forma de acotar el rango seekable, no
a la idea de clip; nuestro diseño no toca la fuente. Pero *probable* no es
*verificado*: exige un iOS real, y este entorno no lo tiene.
Correcciones de método aplicadas al propio trabajo: `gh` no existe en este
entorno y un `|| echo` lo convirtió en un falso negativo sobre media-chrome —
rehecho por la API. Y la recomendación de D-AP.12 se cambia con los datos
delante en vez de defender la que ya estaba escrita.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### vidstack#1195, caracterizado
Abierto y sin respuesta del mantenedor, que pidió reproducción. iPhone 15 /
iOS 17.3.1, Chrome y Safari móviles; **escritorio no afectado** . El error es
`webkit blob resource error 1` y desaparece quitando `clipStartTime` /
`clipEndTime` .
Lectura para nosotros: el modo de fallo apunta a **fuentes `blob:` en iOS**
combinadas con la forma en que Vidstack acota el rango *seekable* , no a la idea
de clip en sí. Nuestro diseño **no toca la fuente ni el rango seekable** —
acota en el provider (§7.5)—, así que probablemente no aplica. Pero *probable*
no es *verificado* : **queda como riesgo abierto que exige un iOS real antes de
dar el clip por cerrado**, y este entorno no puede darlo.
---
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
## 9. Fases (tras el gate)
| Fase | Contenido | Verify |
| --- | --- | --- |
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F0 — Verificación y firma** · **verificaciones ✅ HECHAS (§8.5)** , firma PENDIENTE | ~~Confirmar H-1~~ ✅ · ~~¿sonoriza el `Slider` compuesto?~~ ✅ · ~~¿se auto-limpia `sustain-loading`?~~ ✅ (no se emite — H-9) · ~~celdas `?` de Segmentos~~ ✅ (1 de 6) · ~~vidstack#1195~~ ⚠️ caracterizado, **no reproducible sin un iOS real** · queda: leer `DefaultControls` , `Slider` provider y la recipe del scrubber ANTES de diseñar (regla `feedback_study_real_recipes_before_building` ) · presentar §4 y §6 y **firmar D-AP.1…D-AP.13** | tabla de decisiones firmada en el chat; este plan actualizado |
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
| **F1 — Gaps de framework** | G-1 · **G-2 (bloqueante)** · G-3 · G-4, cada uno con su test | `check` 0 propios · vitest del slider verde · `component:audit --only slider` PASS · `morfo:vocabulary` exit 0 |
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F2 — Morfo** | 6 partes + 1– 2 eventos + `texts` + catálogo `langs/components/media-player.ts` · **retirar `commit-set-time` (D-AP.13) y ejecutar lo firmado sobre `sustain-loading` (D-AP.10 / H-9)** | `validateMorfo` · test del morfo · `morfo:vocabulary` · `translations:check` |
| **F3 — Soma** | sub-providers de las partes nuevas · MediaSession opt-in (con guarda de singleton) · ** `clip` + proyección + clamp (§7.5) — sólo si D-AP.12(b)** · `formatTime` →G-3 · tests del provider | test del provider verde (incl., si D-AP.12(b): dominio del clip, `commit-complete` en `end` , silencio con `loop` ) · `check` 0 propios |
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
| **F4 — Sema** | regla de silencio en audio (D-AP.7) + fix H-1; `expression` sigue `'pack'` | `morfo:vocabulary` (coherencia pack↔expression, guard S11d) · stamp verificado en navegador |
docs(process): el gate del reproductor, consolidado — se presenta una vez y no se mueve
La sesión perdió convergencia: tres presentaciones del gate lo movieron tres
veces (D-AP.7 corregida dos veces, D-AP.12 → v2, D-AP.13 apareció al final), y
de sus commits los últimos corrigieron a los anteriores. Este pase consolida
TODO en el cuerpo del plan para que la sesión siguiente firme en frío, sin
re-excavar. Consolidación pura: sin código y sin hallazgos nuevos.
- **D-AP.7, criterio final: incongruencia, no frecuencia** (D.7: «si el
contenido ES audio, un tick de UI compite con la obra»). Con frecuencia como
criterio, play/pausa —2 a 10 veces por escucha— quedaría fuera y la línea
sería arbitraria. Se cancela el gain de lo que suena MIENTRAS suena la obra;
se conserva lo que informa del estado de la obra (`commit-complete`,
`commit-fail`), con `commit-complete` anotado como el discutible de los dos.
- **D-AP.13, nueva**: `commit-set-time` duplica el `commit-set` del `Slider`
embebido (`onValueCommit` → `seek()` → `trigger`, dos líneas leídas; el del
Slider, medido en navegador). El rationale de radio-cards aplica literal, y
el propio provider ya practica la delegación en `setVolume`. Recomendación:
retirar y delegar; el coste va a F2 como cambio de contrato declarado.
- **§8.7 — la postura de participación, resuelta**: `expression: 'pack'` es
correcta (los eventos de ciclo de vida no los posee ningún hijo, y el guard
S11d la exige mientras exista el pack); el player es un híbrido legítimo con
UNA duplicación (D-AP.13) y UN defecto de timing (G-5). La premisa de D-AP.1
queda intacta — la pregunta que la última pasada dejó como abismo tenía suelo
en la doctrina.
- El clip queda condicionado a D-AP.12(b) en §5, F3 y F5, coherente con la
recomendación v2; F2 recoge D-AP.13 y la ejecución de H-9.
- La cabecera fija la regla de cierre: la cuarta presentación **se firma o se
rechaza, no se re-excava** — y el kickoff de sesión nueva lo dice explícito.
- El handoff (`CONTINUE-sound-engine.md`) queda en «firmar, sólo firmar», con
la trampa que costó la sesión escrita donde las demás: **una medición no es
un diagnóstico** — leer el contrato de la pieza (README → CANON §8 →
book-deviations → código) antes de acusarla.
`docs:check` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F5 — Eidos** | `audio-layout.svelte` + raíz `<AudioPlayer>` + recipe de las 4 variantes + **la pista secundaria de G-2 pintando el buffer** (la ventana del clip, si D-AP.12(b)) + tokens `--audio-player-*` en `lib/recipes/base.ts` | `generate:eidos-css` (TSC) · `recipe-css-contract` · `eidos-lint` 0 inválidos · `component-api-contract` |
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
| **F6 — Formas** | responsive de `variant` /`size`; evaluar container queries (D-AP.9) | prueba visual de las 4 variantes × light/dark × RTL × densidad, **con captura y mirada** (regla dura `feedback_verify_visually_before_showing` ) |
| **F7 — Demo + dossier + cierre** | demo v2 de 9 pestañas (cada prop = control vivo, paridad de chips D-7.4) · README eidos (Baseline/Comparativa≥3/Decisiones/Gaps con disposición) · README soma actualizado · `data-perm-step` | `component:audit --only media-player` **PASS 0 errores** · `smoke` · `perm:check` · `check` 0 propios · `docs:check` |
**Método**: no-cascada — una pieza, verificar, la siguiente (método canonizado
en [`_cierre.md` ](../audit/components/_cierre.md )). Gates completos al cerrar
cada fase.
---
## 10. Fuera de alcance
- **Cola / playlist** (D-AP.6) → `blocks` o app; se registra en `next-features.md` .
- **Waveform** (D-AP.4) → iniciativa hermana, ruta de 9 fases propia.
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
- **`regions`** (n segmentos con estado propio) → **descartado** , no diferido: es
el modelo de wavesurfer y trae su propio contrato (selección, arrastre,
solapamiento). `clip` (D-AP.12) resuelve el caso real —reproducir un trozo— sin
abrirlo.
- **Enganchar el player a `uix.sound.context` ** (D-AP.11) → iniciativa hermana,
con `Waveform` y el visualizador. El v1 no toca Web Audio.
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
- **Capítulos, transcripción, ganancia >100 %, ecualizador, grabación** → v2,
cada uno con disposición escrita en el `## Gaps` del README.
- **Vendorizar motores** (hls.js, Howler, wavesurfer) → el puerto `MediaProvider`
ya es la respuesta; doctrina de cero dependencias.
- **Visualizador/espectro** → si algún día se hace, su hogar es `$scene`
(`EngineScene` ya resuelve frame-loop, pausa fuera de vista, cap de DPR,
reduced-motion obligatoria, pérdida de contexto y presupuesto) o el tier
`packs` — **nunca** un canvas suelto dentro del recipe del player.
---
## 11. Riesgos declarados
| Riesgo | Mitigación |
| --- | --- |
| El morfo de `media-player` ya es el mayor del catálogo; crece más | Todas las partes nuevas `optional: true` ; si tras F2 el fichero pasa de ~700 líneas, reabrir D-AP.1(b) con datos, no con intuición |
| MediaSession es singleton de documento | Registro solo mientras suena + liberación en `pause` /`dispose`; documentado en el README como limitación conocida |
| La variante `bar` vive sobre fondos arbitrarios | `data-depth` + el layer de estado; nunca color crudo |
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
| **El clip depende de `timeupdate`** (~4/s) | El bucle es impreciso (~±125 ms) y **se declara** en el README; la precisión de muestra necesitaría Web Audio, que el v1 no engancha (D-AP.11) |
| **Bug abierto de clip en Chrome/Safari móvil** (vidstack#1195) | Reproducirlo en F0 antes de firmar D-AP.12; si se confirma, el clamp del provider necesita defensa propia y se escribe como limitación conocida |
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
| Sobrecarga de alcance | El v1 es la columna «implementar» de §2 y nada más; todo lo demás lleva disposición escrita |