A-1 de la auditoría (`PLAN-sound-engine` §16): `analyze.ts` hacía `new Ctor()`
propio para decodificar el `.wav` importado. Medido inyectando un WAV real por
el `input[type=file]`: **1 → 2 contextos**. Transitorio —se cerraba al acabar el
decode— pero es exactamente el antipatrón que el art existe para impedir, en la
misma página que usé como prueba de la invariante. Y el aviso de «segundo
contexto» no lo vio, porque sólo cuenta los contextos que crea el art (A-2).
No era un despiste de esa página: **la superficie del art no ofrecía la
operación**. `preload(urls)` decodifica y esconde los `AudioBuffer` en una caché
privada, así que quien quiere las MUESTRAS —un waveform leyendo picos, un
analizador midiendo un fichero— no tenía puerta y se abría la suya.
- **`EngineSound.decode(data): Promise<AudioBuffer | null>`**. Segunda puerta,
simétrica de `context`: una para quien quiere un GRAFO, otra para quien quiere
las MUESTRAS. Las dos llevan al mismo contexto.
- **No pasa por `getOrCreateContext()`**: decodificar funciona sobre un contexto
suspendido, y `resume()` fuera de un gesto puede quedarse pendiente para
siempre — un `decode()` que se espera desde la UI no puede colgar de eso. Se
añade `ensureContext()`, crear sin resumir.
- **No cachea** (los bytes crudos no tienen clave; la caché por URL es de
`preload`) y **devuelve `null` en vez de lanzar**.
- **Desviación declarada** de lo que §16.2 proponía: NO se añade el acceso a los
buffers cacheados. Hoy no tiene consumidor —`Waveform` no existe— y añadir
superficie sin consumidor es justo lo que §16.1 le reprocha al art.
- `analyze.ts` toma el motor por puerto estructural; `AnalyzePanel` lo lee de
`getActiveUix()`, que es el idioma de la casa, no prop-drilling.
MEDIDO en Chrome: importar un WAV crea ahora **1 contexto** en vez de 2, y el
earcon posterior reutiliza ése. Y sigue analizando bien: un tono de 440 Hz a
8 kHz, decodificado sobre el contexto compartido a 44,1 kHz, se mide como
**441 Hz** — el resampleo no falsea la medida.
Guards nuevos en `engine-sound.test.ts`: decodifica sobre el contexto compartido
SIN llamar a `resume()`, un segundo `decode` no crea otro contexto, y devuelve
`null` con bytes indecodificables o sin contexto.
Verificación: art + sema 17 suites / 201 tests · `check` en la baseline exacta
(73 errores, 0 propios).
Quedan abiertos A-2 (el aviso es ciego a los contextos crudos), A-3 (S-1 no está
cerrado), A-4 (opciones del canal tiradas en silencio) y A-5 (`gainScale` en el
master compartido) — los tres últimos cambian comportamiento público y esperan
decisión.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>