|
|
# PLAN — Rediseño del sonido: `$sound` como ORQUESTADOR
|
|
|
|
|
|
> **Tipo**: plan de diseño + ejecución por fases (process — efímero, NO fuente de verdad).
|
|
|
> **Fecha**: 2026-07-31 · **Estado**: **GATE FIRMADO (D-SR.1…D-SR.12) · Entrega 1 en curso**.
|
|
|
> La firma: el usuario aprobó el plan completo el 2026-07-31, con D-SR.4 y D-SR.7
|
|
|
> respondidas explícitamente (transporte provisto por el servicio · dos entregas).
|
|
|
> **Kickoff para sesión nueva**: _"Lee `docs/process/PLAN-sound-redesign.md` y
|
|
|
> continúa por la fase abierta — el gate YA está firmado, no se re-presenta."_
|
|
|
|
|
|
---
|
|
|
|
|
|
## 1. El mandato (2026-07-31)
|
|
|
|
|
|
El usuario rechazó **de raíz** el gate del reproductor (D-AP.1…13,
|
|
|
[`PLAN-audio-player.md`](./PLAN-audio-player.md), suspendido): el sistema de
|
|
|
sonido es un frankenstein — nació de los diseños de otros módulos (sema primero,
|
|
|
media-player después) y se parchea en función de lo que ya había. Nunca partió de
|
|
|
su verdadera función: **orquestador y gestor de TODOS los aspectos del sonido en
|
|
|
el framework**, al nivel de `motion` / `timers` / `scene`. Los módulos que usan
|
|
|
sonido (players, semántica, estudio) **usan el servicio y se adaptan a su
|
|
|
filosofía, no al revés**.
|
|
|
|
|
|
Método mandado: **clean-room** — el diseño parte de la función, sin que los
|
|
|
consumidores actuales lo muevan; ellos se adaptan después.
|
|
|
|
|
|
**Qué se conserva y qué cae.** Los HECHOS medidos no se repudian: la invariante
|
|
|
de UN `AudioContext` por documento (medida en Chrome en los dos modos de
|
|
|
arranque), `decode` sin segundo contexto, los hallazgos del player (H-1, H-9,
|
|
|
H-10/G-5, los gaps G-1/G-2/G-3 del `Slider`). Lo que cae es **doctrina**:
|
|
|
`PLAN-sound-engine.md` §16.1 («la voz es del art, calibrada para sema; partir en
|
|
|
dos cuando llegue un segundo consumidor») queda **REVOCADA** — la calibración es
|
|
|
DATO del consumidor y el art es el orquestador desde ya; y el planteamiento del
|
|
|
player («el player decide cuándo engancharse») se invierte: el player consume el
|
|
|
transporte que el servicio provee.
|
|
|
|
|
|
## 2. La función — los 7 dominios (clean-room)
|
|
|
|
|
|
1. **Sustrato** — el `AudioContext` único: creación, unlock por gesto,
|
|
|
suspend/resume, SSR no-op, costuras de test, disposal. _(Existe y está
|
|
|
verificado — se conserva.)_
|
|
|
2. **Mezcla** — master → **buses** `ui` (earcons/semántica) y `content` (la
|
|
|
obra). Palancas por bus: gain, mute, retenciones de duck (refcount, gana la
|
|
|
más fuerte). Invariante estructural: `prefs.sound` gobierna SOLO `ui`; el
|
|
|
volumen de la obra es de la fuente + política del bus `content`.
|
|
|
3. **Voces** — la síntesis es MAQUINARIA del servicio; la **calibración es DATO
|
|
|
del consumidor** (espejo de motion: los consumidores registran presets, el
|
|
|
motor los ejecuta). `registerVoice(name, spec)`; sema registra la suya. Las 4
|
|
|
constantes perceptivas (quinta 1.5×/0.3 · clamps 15 %/40 % · ±400 cents ·
|
|
|
mapeo AM) dejan de ser del art. Samples/`preload`/caché y `decode` se
|
|
|
conservan como puertas.
|
|
|
4. **Ciudadanía de media** — las fuentes de contenido pasan por el servicio: el
|
|
|
transporte (play/pause/seek/volume/rate/snapshot/subscribe — la forma exacta
|
|
|
del puerto `MediaProvider` del player) lo PROVEE el servicio (D-SR.4), que
|
|
|
así sabe qué suena, aplica foco, proyecta MediaSession y gobierna el ducking.
|
|
|
5. **Política / arbitraje** — foco entre fuentes (`mixed` / `exclusive` /
|
|
|
`duck`, modelo AVAudioSession/AudioFocus) y la relación UI↔contenido: «la UI
|
|
|
calla mientras suena la obra» (incongruencia, D.7) pasa de parche de cascada
|
|
|
a **relación entre buses** (opt-in). Sema conserva su doctrina (firmas,
|
|
|
tunings, cascada, migración de significado con `off`).
|
|
|
6. **Percepción** — `decode` (existe); analyser por bus/fuente y picos para
|
|
|
`Waveform` = v2 con disposición escrita.
|
|
|
7. **SO** — MediaSession es singleton del documento → vive en el orquestador
|
|
|
(proyecta la fuente ACTIVA); los players aportan metadata.
|
|
|
|
|
|
Fuera, con disposición: spatial · cadenas DSP · grabación/captura · MIDI. El
|
|
|
háptico NO entra (otro canal, D.8 — asimetría ya documentada).
|
|
|
|
|
|
## 3. Referencias del sector
|
|
|
|
|
|
Modelos (conocimiento estable; el cross-check API-fino se hace al abrir la
|
|
|
Entrega 2, que es donde esas APIs muerden):
|
|
|
|
|
|
| Modelo | Aporta |
|
|
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
|
| iOS `AVAudioSession` | UNA autoridad por app: categorías, mixWithOthers/duckOthers, interrupciones |
|
|
|
| Android AudioFocus | las fuentes PIDEN foco (`GAIN`/`TRANSIENT`/`MAY_DUCK`); el árbitro aplica pérdidas |
|
|
|
| FMOD / Wwise | jerarquía de buses (master → music/sfx/voice), ducking entre buses |
|
|
|
| Howler.js | el singleton web: master volume/mute, autoUnlock, un contexto |
|
|
|
| Tone.js | contexto compartido + `Destination` |
|
|
|
| Plataforma web | cap de contextos, unlock por gesto, MediaSession API, trampa `createMediaElementSource` (irreversible; CORS → mudo) |
|
|
|
|
|
|
Ninguna referencia web une mezcla por buses + foco + MediaSession + voces
|
|
|
semánticas bajo una autoridad. Esa unión ES «gestor de todos los aspectos del
|
|
|
sonido» — superación, no paridad.
|
|
|
|
|
|
## 4. La filosofía, en espejo de los arts hermanos
|
|
|
|
|
|
- Como `timers`: **nadie** hace `new AudioContext()` ni Web Audio crudo fuera
|
|
|
del art; todo pasa por `uix.sound`.
|
|
|
- Como `motion`: el motor ejecuta, los consumidores REGISTRAN sus datos
|
|
|
(presets ⇒ voces). Sema deja de estar horneada dentro del art.
|
|
|
- Como `scene`: la ciudadanía (unlock, visibilidad, presupuesto, degradación,
|
|
|
teardown) se hace UNA vez y se hereda.
|
|
|
- Contrato de arts: `Engine*` sin runas · puertos `SoundDom`/`SoundTimers` ·
|
|
|
**diagnostics con catálogo tipado** (se adopta ahora — la decisión «espejo de
|
|
|
scene, sin diagnostics» se revierte, declarado en D-SR.9).
|
|
|
|
|
|
## 5. La API (v2)
|
|
|
|
|
|
```ts
|
|
|
const sound = createEngineSound({ dom, timers, logger, audioContextFactory, fetcher, masterGain });
|
|
|
|
|
|
// Sustrato (se conserva)
|
|
|
sound.prime();
|
|
|
sound.suspend();
|
|
|
sound.resume();
|
|
|
sound.dispose();
|
|
|
sound.state;
|
|
|
sound.context;
|
|
|
|
|
|
// Mezcla (nuevo)
|
|
|
sound.master.setGain(v); // política de documento (antes setMasterGain)
|
|
|
sound.bus('ui').setGain(v);
|
|
|
sound.bus('ui').setMuted(true);
|
|
|
const hold = sound.bus('ui').duck(0.2); // refcount; gana la más fuerte; hold.release()
|
|
|
|
|
|
// Voces (nuevo)
|
|
|
sound.registerVoice('sema', voiceSpec); // calibración = dato del consumidor
|
|
|
await sound.play(sig, { bus: 'ui', voice: 'sema', gainScale });
|
|
|
|
|
|
// Muestras / percepción (se conserva)
|
|
|
await sound.preload(urls);
|
|
|
await sound.decode(bytes);
|
|
|
|
|
|
// Ciudadanía de media (Entrega 2)
|
|
|
const media = sound.media(el, { metadata, focus: 'may-duck' });
|
|
|
// → implementa el puerto MediaProvider; foco mixed/exclusive/duck;
|
|
|
// duckUiWhileContent opt-in; MediaSession de la fuente activa; el grafo NO se
|
|
|
// engancha por defecto (trampa CORS declarada); visibilidad content-aware
|
|
|
// (jamás suspender el contexto con obra sonando — E-3, ahora estructural).
|
|
|
```
|
|
|
|
|
|
Sema tras la adaptación: resuelve firma + nivel (doctrina intacta) y llama
|
|
|
`play(sig, { bus: 'ui', voice: 'sema', gainScale })`. Nada más.
|
|
|
|
|
|
## 6. Decisiones — FIRMADAS 2026-07-31
|
|
|
|
|
|
| ID | Decisión firmada |
|
|
|
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| D-SR.1 | `$sound` = EL orquestador (los 7 dominios). §16.1 revocada. Nombre/alias se quedan |
|
|
|
| D-SR.2 | Buses `master → ui + content` (set fijo v1); gain/mute/duck por bus; `prefs.sound` ↔ `ui` SOLO |
|
|
|
| D-SR.3 | Voces registradas; default del servicio = calibración actual (cero cambio audible); sema registra la suya encima |
|
|
|
| D-SR.4 | **El servicio PROVEE el transporte** (el adaptador nativo se muda al servicio; el player consume el handle). El elemento DOM sigue siendo del componente; grafo no enganchado por defecto |
|
|
|
| D-SR.5 | Foco default `mixed`; `exclusive`/`duck` config; `duckUiWhileContent` opt-in |
|
|
|
| D-SR.6 | MediaSession del servicio (fuente activa, opt-in con metadata) |
|
|
|
| D-SR.7 | **Dos entregas**: E1 = núcleo (F0–F2) con escucha A/B antes de seguir; E2 = media + MediaSession (F3–F4); F5 repartido |
|
|
|
| D-SR.8 | Sin `ActiveSound` v1; handles con `subscribe`; disposición escrita |
|
|
|
| D-SR.9 | Diagnostics contrato completo de arts (revierte «espejo de scene») |
|
|
|
| D-SR.10 | Player suspendido; re-plan posterior sobre el servicio (hereda hechos D-AP) |
|
|
|
| D-SR.11 | `prime/play/preload/decode/suspend/resume/state/context` se conservan; `setMasterGain` → `master.setGain` con barrido en el mismo pase (sin shim); `play` gana `{bus, voice}` con defaults que preservan el comportamiento |
|
|
|
| D-SR.12 | `sounds.ts` de sema intacto: `SOUND_LIBRARY` = recursos, `SOUND_TUNINGS` = canon (D.7) |
|
|
|
|
|
|
## 7. El terreno — destino de cada pieza
|
|
|
|
|
|
| Pieza | Destino |
|
|
|
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| Gobierno de contexto + guard de 2.º contexto + `decode`/`preload`/caché/fallback | **Se conserva** (verificado) |
|
|
|
| `gainScale` en la envolvente (A-5) | **Se conserva** — es la separación política/reproducción que los buses generalizan |
|
|
|
| Las 4 constantes de la voz | **Se mudan** a `SoundVoice` (default = valores actuales; sema registra su voz) |
|
|
|
| `setMasterGain` | → `master.setGain` (barrido: art + tests + fake del port; ningún consumidor de producción fuera) |
|
|
|
| `autoSuspend` | Entrega 2: content-aware (no suspender con obra sonando) |
|
|
|
| `SoundChannel` | Adelgaza: resolve → `play({bus:'ui', voice:'sema', gainScale})`; registra la voz de sema al construirse (idempotente); unión A/B intacta |
|
|
|
| Asserts que cambian | `sound.test.ts` (harness de 1 nodo → nodos rastreados; `createGain` 1×→3×) y `sound-port.test.ts` (fake del engine + args de `play`) — **cambio de contrato declarado**, lo contrario del criterio value-preserving de la extracción, a propósito |
|
|
|
| `sound-e2e.test.ts` | Sin cambios: pinea osciladores/contextos, no el árbol de gains |
|
|
|
| `contracts.ts` fila `sound` | **Sin tocar en E1**: no pinea métodos y sus promesas siguen verdaderas; se revisará en E2 (el fichero además está contaminado por otra sesión) |
|
|
|
| Estudio `/temas/sema` | Sin cambios en E1 (no usa `setMasterGain`); se revisita en E2 |
|
|
|
| `/temas/orquesta` | Legal (forma B); deuda de esa demo |
|
|
|
|
|
|
## 8. Fases
|
|
|
|
|
|
| Fase | Contenido | Verify |
|
|
|
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| **F0 — Documento** ✅ | Este plan · `CONTINUE-sound-engine.md` reescrito (pivote) · cabecera de `PLAN-audio-player.md` (suspensión, ya hecha) | docs:check limpio en lo propio |
|
|
|
| **F1 — Motor v2 (E1)** ✅ **HECHA 2026-07-31** | Buses (master→ui/content, gain/mute/duck refcount, política pre-contexto) + voces (`registerVoice`, default = calibración histórica, aviso una vez por voz desconocida) + `play({bus,voice})` (defaults `ui`/default) + `master.setGain` (barrido de `setMasterGain`: art+tests+fake; ningún consumidor de producción fuera) + `diagnostics.ts` (catálogo tipado, D-SR.9); invariantes de contexto INTACTAS | ✅ suite del art **19/19** (14 originales — 2 asserts de contrato declarados: `createGain` 1×→3× y harness multi-nodo — + 5 nuevos de buses/voces) · ✅ `check` **73 = baseline exacta, 0 propios** |
|
|
|
| **F2 — Sema se adapta (E1)** ✅ **HECHA 2026-07-31** | `SEMA_SOUND_VOICE`(+NAME) en `sounds.ts` (tipo importado de `$sound`, dirección permitida) · el canal registra la voz al construirse (idempotente; en AMBAS formas A/B) y toca `play({bus:'ui', voice:'sema', gainScale})` · `sound-port.test.ts` (fake al contrato nuevo + pin de registro) · `sound.test.ts` (harness multi-nodo + `createGain` 3×, declarado) · e2e y `active-uix` SIN tocar · docs de la entrega: README del art reescrito (identidad orquestador) + fila/grafo de `arts/README.md` + `architecture/sema.md` §SoundChannel | ✅ **18 suites / 237 tests verdes** (sema completa + art + active-uix; e2e de contexto único intacto) · ✅ `check` 73/0 propios · ✅ **escucha A/B del usuario: «suena igual» (2026-07-31) — ENTREGA 1 CERRADA** |
|
|
|
| **F3 — Ciudadanía de media (E2)** ✅ **HECHA 2026-07-31** | `src/arts/sound/media.ts`: `sound.media(el, {focus?, metadata?})` → handle con la forma EXACTA del puerto `MediaProvider` (adaptador nativo mudado: flag `waiting`, `bufferedAhead`, clamps; `kind` por `tagName`, no `instanceof` — cross-realm-safe) · foco `mixed`/`exclusive`/`duck` (servicio + override por fuente; duck sobre `el.volume` con `snapshot().volume` = volumen del DUEÑO) · `duckUiWhileContent` (hold refcount sobre el bus `ui`) · visibilidad content-aware (fuente sonando veta el suspend) · `attach()` opt-in al bus `content` (irreversible, CORS declarado) · manager perezoso (sin media no se paga) · cross-check MediaSession contra MDN hecho al abrir | ✅ 10 tests nuevos con elementos fake (transporte · exclusive · duck+restore · override · duckUi pre-contexto · visibilidad · attach al contexto COMPARTIDO · sesión · sin-metadata no toca · dispose) · ⚠️ la medición en Chrome del escenario mixto queda para el re-plan del player: **ninguna página consume `media()` aún** — no hay nada real que medir |
|
|
|
| **F4 — MediaSession (E2)** ✅ **HECHA 2026-07-31** (dentro de `media.ts`) | Proyección de la fuente ACTIVA (opt-in por `metadata`): `MediaMetadata` + `playbackState` + `play`/`pause`/`seekto` (un try por acción — un nombre no soportado no tumba al resto) + `setPositionState` en timeupdate/duration/rate · liberación en destroy/dispose (metadata null, handlers null) · promoción a otra fuente sonante con metadata al morir la activa · feature-detected, todo absorbido con diagnóstico | ✅ tests con `navigator.mediaSession` fake (proyección, tecla pause → handle, liberación, opt-in) · ⚠️ prueba manual de teclas de medios: necesita una página consumidora — llega con el re-plan del player |
|
|
|
| **F5 — Barrido + docs** ✅ **HECHA 2026-07-31 (con 2 exclusiones declaradas)** | `defineEngineSound(options)` acepta la rodaja de política (`EngineSoundPolicyOptions`) · README del art (media citizenship) · `arts/README.md` · docblock del motor · **`contracts.ts` NO tocado** (no pinea métodos, sus promesas siguen verdaderas, y el fichero está contaminado por otra sesión) · **estudio sin cambios** (no usa la superficie nueva) | ✅ **27 suites / 301 tests verdes** (sema+sound+active-uix+active-app) · ✅ `check` **73 = baseline, 0 propios** (verificado con el patrón `\\\\` correcto — el filtro anterior no casaba, la trampa documentada) |
|
|
|
| Después (iniciativa aparte) | Re-plan del reproductor sobre el servicio | su propio gate |
|
|
|
|
|
|
Método no-cascada. Sin commits salvo petición expresa (árbol compartido).
|
|
|
|
|
|
## 9. Auditoría post-entrega (2026-07-31) — AU-1…AU-9, TODAS RESUELTAS
|
|
|
|
|
|
Auditoría clean-room de las dos entregas (pedida por el usuario; orden de cierre
|
|
|
firmado con «ok»). 9 hallazgos, ninguno invalidó el rediseño; resoluciones:
|
|
|
|
|
|
| AU | Hallazgo | Resolución |
|
|
|
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| **AU-1** | El foco `duck` era starter-céntrico: una fuente que arrancaba DESPUÉS del ducker no quedaba atenuada; divergía del modelo de referencia (duckOthers persistente) sin documentar | ✅ **Semántica reescrita y FIRMADA: estado derivado del conjunto sonante** (`refreshDucking`): mientras suene un ducker, toda otra fuente sonante queda atenuada — llegadas tardías incluidas; entre duckers simultáneos, el más reciente retiene la palabra. `exclusive` sigue siendo acción del starter (sin re-reclamo, documentado). **Test nacido en ROJO** contra el código viejo y verde tras el fix |
|
|
|
| **AU-2** | `media()`/`attach()` sin guard post-dispose: attach podía abrir un `AudioContext` HUÉRFANO en un engine disposed; `preload()` (preexistente v1) igual de desguardado | ✅ Guard `disposed` en `ensureContext()` (cierra TODAS las vías) + en `preload()` · `dispose()` limpia el manager FUERA del guard de idempotencia (un segundo dispose libera un registro post-dispose) · comportamiento post-dispose documentado en `types.ts` · test: attach post-dispose → `null`, factory jamás llamada |
|
|
|
| **AU-3** | Doble registro del mismo elemento: listeners duplicados + auto-duck | ✅ `byElement`: re-registrar RETIRA la inscripción obsoleta (un remonte nunca apila); el elemento destruido sale de la mezcla al volumen del DUEÑO. Test |
|
|
|
| **AU-4** | La invariante «prefs.sound ↔ bus ui» enunciada como mecanismo, sin productor (la reducción sigue siendo `gainScale` del canal) | ✅ **Texto corregido** (README del art + `sema.md`), patrón A-2: lo garantizado por construcción es el NEGATIVO (nada de UI toca content); el bus `ui` es la palanca que una app PUEDE cablear. El cableado directo queda como patrón de app, no del framework |
|
|
|
| **AU-5** | Duplicación estructural handle↔puerto `MediaProvider` sin guard de deriva hasta el re-plan | ✅ `soma/components/media-player/media-provider-drift.test.ts` (lado soma — la dirección de import permitida): `Exact` en snapshot+eventos, asignabilidad unidireccional del handle. Falla `check` si derivan |
|
|
|
| **AU-6** | Verifies prometidos sin ejecutar: `docs:check` (F0) y `lint` | ✅ Medidos: `docs:check` **0 errores/555 docs** · prettier fallaba en 5 ficheros míos → formateados, `--check` limpio |
|
|
|
| **AU-7** | Verify de F3 reescrito al marcarlo (cross-check solo MediaSession) · promesa «media tree-shakeable + medir» incumplida | ✅ Declarado aquí: AVAudioSession/AudioFocus quedan como modelos de diseño (no hay código contra ellas) · media es alcanzable estáticamente A PROPÓSITO (es la puerta del servicio) · **bundle MEDIDO**: art completo **16,2 KB min · 5,6 KB gzip** (era 2,2 gz; la ciudadanía entera cuesta ~3,4 KB gz). La fila de riesgo de §10 queda enmendada por esta |
|
|
|
| **AU-8** | Guards nuevos sin ver fallar (regla dura del hilo) | ✅ Tres verificados: persistencia del duck (test nacido rojo) · veto de visibilidad (mutación → rojo → revertida) · `duckUiWhileContent` (mutación → rojo → revertida) |
|
|
|
| **AU-9** | Declaraciones a medias: standalone sin acceso a política; acciones MediaSession omitidas sin disposición | ✅ Disposiciones escritas en el README: standalone se cablea cuando exista el primer consumidor (no crecer superficie sin uso); `stop`/`seekforward`/`seekbackward`/tracks llegan con el re-plan del player si su UX las pide |
|
|
|
|
|
|
Notas menores registradas sin acción (deliberado): fallbacks defensivos
|
|
|
inalcanzables en `play()`/`attach()` · carrera teórica de `volumechange` async
|
|
|
(irrelevante en fakes, ventana microscópica en navegador) · `attach()`+
|
|
|
`autoSuspend` puede silenciar el elemento adjunto con pestaña oculta (combinación
|
|
|
opt-in+opt-in, se documentará si aparece el caso) · `preload()` puede pender en
|
|
|
`resume()` fuera de gesto (preexistente v1, hoy además guardado por disposed) ·
|
|
|
`exclusive` no re-arranca a los desplazados (modelo AudioFocus, documentado).
|
|
|
|
|
|
**Gate tras la auditoría**: 28 suites / **305 tests** verdes (incl. drift guard)
|
|
|
· `check` **73 = baseline, 0 propios** (verificado con patrón `\\\\`; los 4 de
|
|
|
`uix\soma` que caza el filtro ampliado son de `file-upload`, ajenos y dentro de
|
|
|
la baseline) · `docs:check` 0 · prettier limpio en lo propio.
|
|
|
|
|
|
## 10. Riesgos
|
|
|
|
|
|
| Riesgo | Acotación |
|
|
|
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
|
| Regresión perceptiva (los tests no oyen) | Voz default = calibración actual → cero cambio audible en E1; escucha A/B del usuario = verify de F2 |
|
|
|
| Churn de tests | Solo asserts cuyo contrato cambió, declarados fichero a fichero (§7) |
|
|
|
| Trampa `createMediaElementSource` | El grafo no se engancha por defecto; attach opt-in con limitación declarada |
|
|
|
| Bundle | Núcleo 2,2 KB gz hoy; media en módulo tree-shakeable; medir en F5 |
|
|
|
| Sobre-alcance | 7 dominios diseñados, implementación escalonada; percepción avanzada v2 con disposición |
|