You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/AUDIT-sema-2026-08-05.md

665 lines
144 KiB

# AUDIT — sistema semántico completo (2026-08-05)
> **Crónica.** Registra lo hallado en la fecha indicada; no es doctrina viva.
> Encargo del autor: «auditar todo el sistema de sonido semántico, e incluso toda
> la parte semántica respecto a lo que es el módulo, tipos, adopciones,
> componentes de todo el ecosistema», tras confirmarse que los toggles llevaban
> silenciados desde mayo sin que ningún guard lo viera.
**Método.** 8 frentes en paralelo (samples · motor · cascada · tipos · packs ·
morfo↔sema · canales · doctrina), cada hallazgo sometido después a un agente
adversarial que intentaba refutarlo leyendo el código. 53 agentes, 1.790
llamadas a herramientas. Base: HEAD `d05ecc1aa`.
**Resultado: 44 hallazgos confirmados, 1 refutado.**
Severidad tras verificación: **high** 1 · **medium** 22 · **low** 21
Censo de referencia (medido en runtime, no por grep): **166 morfos**, 91 con
eventos, **248 eventos** · **71 packs** con **212 reglas** de cascada.
---
## Veredicto
El motor **no está corrompido**. Los cinco `.wav` son PCM válido (cabecera RIFF
coherente con el tamaño real en los cinco), la ruta de sample se recorre de punta
a punta, los 248 eventos cumplen el vocabulario cerrado de familia y verbo, el
orden de capas es el documentado y el bug histórico de fuga de nivel está
realmente cerrado.
El daño está concentrado en tres sitios:
1. **Un peligro de emparejamiento** (`closest()`) que convierte toda regla de pack
en regla de ancestro — con una fuga audible reproducible.
2. **La capa 5 pisando la capa 2**: los tunings fijan `gain` como número desnudo y
`applyOverride` los aplica en modo `replace`, borrando el perfil evaluativo del
intent. Es justo lo que la doctrina prohíbe.
3. **17 de 212 reglas** que no disparan o disparan mal, en 13 packs.
---
## Severidad ALTA
### S-01 · `closest()` convierte toda regla de pack en regla de ancestro: el aviso `untilFix` del Form pisa la firma de todos sus descendientes
- **Dónde:** src/uix/sema/resolver.ts:208-215 (esp. :211)
- **Defecto:** `safeMatches` acepta el match por ANCESTRO, y como los `data-event-*` viven toda la ventana de hold (o indefinidamente con `untilFix`/`untilAction`/`stateBound`), cualquier señal de un descendiente hereda las reglas de cascada del ancestro. Ninguna regla del canon depende de este comportamiento a propósito.
- **Impacto:** Tras un submit fallido, CADA click y CADA commit dentro del formulario reproduce la muestra de error hasta el siguiente submit. El usuario oye un error mientras corrige el error. Misma clase de fuga bajo cualquier overlay durante su hold. Es silencioso: los 256 tests de sema pasan con la fuga presente.
- **Arreglo propuesto (NO aplicado):** Eliminar el fallback: dejar `return target.matches(selector);`. Verifiqué que NINGUNA regla embarcada lo necesita — el scoping por ancestro del media-player (`media-player.ts:58,69-70`) y el matcher `ancestor` de `semaSelector` (`morfo/selectors.ts:116-121,184`) emiten un combinador de descendencia que `matches()` resuelve nativamente. La única regla que alguna vez intentó apoyarse en `closest()` (navigation-menu, analizada en `docs/process/AUDIT-blocks-ledger.md:528`) partía de una premisa falsa —un selector compuesto exige que UN MISMO elemento lleve las dos condiciones— y se corrigió hoy. Si más adelante se quiere alcance por ancestro, que se declare con `ancestor:` y sea visible en el selector.
- **Verificación adversarial:** Verificado y reproducido. resolver.ts:211 es exactamente `target.matches(selector) || target.closest(selector) !== null`. La cadena completa se sostiene: morfo/components/form.ts:34-42 declara signal-warn-invalid con target=provider y persistence 'untilFix'; soma/runtime.svelte.ts:736-737 reenvía la persistence al signal; engine.ts:411-412/467-469 sólo hace auto-cleanup si es 'transient', así que la proyección data-event-* queda viva en el <form> hasta clearTarget (form-provider.svelte.ts:129, siguiente submit). Medido con jsdom + packs reales + resolver real: un contact-activate de un <button data-button> dentro de ese form resuelve a {pitch:520, centroid:1400, roughness:0.45, contour:'descending', gain:0.1, sampleUrl:'/sounds/error.wav'} + haptic.kind 'warning', frente al baseline de contact (800/flat/0.25/tick). Idéntico para commit-toggle-check. Dialog durante el hold de emerge: gain…
## Severidad MEDIA
### S-02 · El pack proof-of-human canoniza samples en la familia `commit` y borra las intent deltas — D.7 lo prohíbe literalmente
- **Dónde:** src/uix/sema/components/proof-of-human.ts:35-45 · src/uix/sema/resolver.ts:271-275 · docs/decisions/book-deviations.md:390,393
- **Defecto:** `sound('notification.ping')` / `sound('alert.error')` se aplican a `commit-confirm` y `commit-fail` (familia `commit`, no `signal`), y como el resolver aplica los overrides de cascada en modo `replace` sobre TODAS las claves, la firma del sample sustituye por completo la firma ya modulada por intent (capa 2). El comentario del propio pack afirma lo contrario.
- **Impacto:** `risk` y `threat` suenan exactamente igual en proof-of-human: la modulación por intent —lo que D.7 protege— desaparece, y no sólo en la reproducción del sample sino en la FIRMA, así que el fallback sintético (cuando el .wav falla) también suena ciego al intent. Un `fulfill` pierde su +300 Hz / ascending / +0.05. El propio estudio de sema ya anota esta regla al exportar packs (web/routes/temas/sema/_lib/draft.ts:240-241), o sea que el framework la enseña y el framework la incumple.
- **Arreglo propuesto (NO aplicado):** O bien sustituir esas dos reglas por tunings de familia + deltas de carácter (`soundTuning('commit.soft', { … })`, como hace form.ts para `commit-submit`), o bien —si el autor quiere conservar la marca cultural del ping/error— componer con `{ op: 'add' }` sobre las claves y NO tocar pitch/contour/gain, dejando sólo `sampleUrl`. Tercera vía: declararlo excepción explícita en D.7 («se documenta explícitamente», book-deviations.md:390). Decisión del autor; no la tomo.
- **Verificación adversarial:** Verificado en el código real y confirmado por construcción, no sólo por el harness. (1) proof-of-human.ts:37,44 aplica sound('notification.ping') / sound('alert.error') a commit-confirm y commit-fail, ambos family 'commit' según morfo/components/proof-of-human.ts:50-95. (2) La pieza que cierra el caso y que el auditor no explicitó: sounds.ts:226-239 — sound() devuelve una SoundSignature COMPLETA (resolveSoundDefinition esparce definition.fallback entero + sampleUrl), no un delta parcial. (3) resolver.ts:271-275 aplica applyLeaf(current, delta, 'replace') y applyLeaf:363-367 recurre por cada clave del delta conservando 'replace', así que las 9 claves pisan la firma ya modulada por intent. (4) Aritmética reproducible a mano desde sema-map.ts: base commit = pitch 700/flat/gain 0.3 (:114-125), fulfill = +300/ascending/+0.05 (:255-266) → 1000/ascending/0.35; con pack queda 960/arc/0.12, el fa…
### S-03 · La reproducción de samples no tiene un solo test: playSample y engine.preload están sin cubrir
- **Dónde:** src/arts/sound/engine-sound.test.ts (19.100 bytes, 0 coincidencias de sample/wav/fetch fuera de `peaks`) · src/uix/sema/chans/sound-port.test.ts:191-197
- **Defecto:** El único test relacionado con samples verifica que el canal DELEGA `preloadSamples` en un motor falso; ni el fetch, ni el decode, ni la caché por URL, ni el fallback a síntesis, ni `preload` tienen prueba.
- **Impacto:** Es la razón mecánica de que los dos defectos anteriores (fallback mudo, preload que no precarga) pudieran entrar y quedarse: la clase entera de la ruta de sample es invisible al CI. Un cambio en `playSample` no rompe nada hoy.
- **Arreglo propuesto (NO aplicado):** Tres casos con `audioContextFactory` + `fetcher` falsos, que es como ya se testea el resto del motor: (1) fetch OK → se crea `createBufferSource` y NO oscillators; (2) `{ok:false}` → se crean oscillators (fallback) y se emite el diagnóstico nuevo; (3) `preload` con contexto suspendido → el fetch se emite igualmente. Los tres corren en el proyecto `server` sin navegador (verificado: mi harness los reproduce en tsx).
- **Verificación adversarial:** Confirmado en el código real, sin contradicción encontrada. (1) `playSample` existe en src/arts/sound/engine-sound.ts:643-683 (fetch → response.ok → decodeAudioData → sampleCache, `return false` para que play() caiga a synthesize en :238-242) y `preload` en :248-265; ninguno tiene test. (2) El grep del auditor se reproduce exacto: en engine-sound.test.ts (19.100 bytes) "sample|wav|fetch" sólo devuelve :316,:317,:325, comentarios del test de `peaks()`. Ampliando el patrón a decodeAudioData sólo aparecen :294/:295 y :352, que pertenecen a los tests de `decode()` — puerta distinta que no toca sampleCache, fetcher ni el fallback. (3) sound-port.test.ts:191-198 es literalmente lo descrito: motor falso (createFakeEngine, `preload: vi.fn()` en :60), verifica la delegación, no la implementación. Refutaciones intentadas y fallidas: `grep -rn createEngineSound --include=*.test.ts` da sólo 4 ficher…
### S-04 · preload() se cuelga en resume(): el pre-decodificado nunca ocurre y el contexto se abre en el boot
- **Dónde:** src/arts/sound/engine-sound.ts:248-251 y 402-426 · disparado desde src/uix/sema/engine.ts:329-335 · src/uix/sema/components/chat-log.ts:22 · web/routes/uix/+layout@.svelte:125-129
- **Defecto:** `preload()` obtiene el contexto con `getOrCreateContext()`, que hace `await ctx.resume()`. Fuera de un gesto de usuario esa promesa queda pendiente indefinidamente, así que el fetch+decode de las muestras NO se ejecuta — justo el peligro que el propio motor documenta para justificar `ensureContext` en `decode()`. Y como `EngineSemantic` lanza el preload en su constructor, el contexto se crea en el arranque de la página, sin gesto.
- **Impacto:** Doble daño: (1) el preload no cumple su única función — la primera reproducción sigue pagando fetch+decode, y en Chrome no decodifica hasta que llega el primer gesto, momento en que ya suena el primer earcon; (2) toda app que incluya el pack `chat-log` (el sitio de docs lo hace, `web/routes/uix/+layout@.svelte`) abre el AudioContext en la hidratación, fuera de gesto, con la promesa flotante `void ...` que nunca liquida. Rompe además la invariante publicada del art: «no context, no listeners until prime()/play()».
- **Arreglo propuesto (NO aplicado):** `preload()` debe usar `ensureContext()` (decodificar funciona en contexto suspendido — es literalmente lo que dice el comentario de `decode`). Opcionalmente diferir el preload del constructor de `EngineSemantic` al primer `prime()`; y dar un diagnóstico al `catch` vacío por URL (engine-sound.ts:260-262), hoy un 404 de un .wav es invisible para siempre.
- **Verificación adversarial:** No refutado: el código está donde dice y la lectura es correcta. `preload()` (engine-sound.ts:248-251) abre con `await this.getOrCreateContext()`, que en :417-423 hace `await ctx.resume()` con un `catch` que sólo cubre rechazo, no promesa pendiente — exactamente el peligro que el propio motor documenta en :428-437 para justificar que `decode()` use `ensureContext()`. Cadena de disparo verificada: `createActiveUix` construye `EngineSemantic` eager con `soundEngine: uix.sound` (active-uix.svelte.ts:142-155) → rama `opts.soundEngine !== undefined` → `void soundChannel.preloadSamples(...)` en el constructor (engine.ts:329-335) → `engine.preload()` sobre el motor COMPARTIDO; `soundSampleUrls(['notification.ping'])` devuelve `['/sounds/ping.wav']` (sounds.ts:119,245-251), y el layout del sitio de docs registra `chatLogSema` con `sound: true` (+layout@.svelte:119-129). Reproducido ejecutando un…
### S-05 · EngineSemantic descarta en silencio las opciones de motor propio cuando hay motor compartido
- **Dónde:** src/uix/sema/engine.ts:299-319 (bifurcación) frente a src/uix/sema/chans/sound.ts:91-104 y 119-128 (el aviso que debía cazarlo)
- **Defecto:** `SoundChannel` avisa cuando le llegan opciones de construcción junto a un `engine` inyectado. Pero `EngineSemantic` filtra esas claves ANTES de construir el canal: en la rama de motor compartido sólo reenvía `{ engine, preferences, logger }`. Como `active-uix` siempre inyecta `soundEngine`, `masterGain` / `dom` / `timers` / `audioContextFactory` / `fetcher` pasados como `events.sound` se pierden sin una sola palabra — exactamente el fallo que el aviso del canal existe para impedir, reintroducido un nivel más arriba.
- **Impacto:** `createActiveUix({ events: { sound: { masterGain: 0.5 } } })` compila, es la forma natural de configurar el volumen maestro desde la raíz, y no hace absolutamente nada. Silencio total: ni error de tipos (la forma B es válida por sí sola), ni warn, ni efecto.
- **Arreglo propuesto (NO aplicado):** En esa rama, detectar las claves de construcción presentes en `soundOptions` y (a) avisar con el mismo mensaje `IGNORED_ENGINE_OPTIONS`, o (b) mejor, aplicar las que sí tienen sentido sobre el motor compartido (`masterGain` → `engine.master.setGain(...)`) y avisar sólo de las que no.
- **Verificación adversarial:** Confirmado en código y en ejecución. engine.ts:299-319 tiene las tres ramas y la del motor compartido reenvía sólo {engine, preferences, logger}; active-uix.svelte.ts:150 y define-engine-semantic.ts:51 inyectan siempre soundEngine, así que esa rama es la única que toman las apps reales. EngineSemanticOptions no excluye por tipos `soundEngine` + `sound:{formaB}` (types.ts:77 acepta el objeto entero), a diferencia de SoundChannelOptions, que sí lo rechaza con @ts-expect-error en sound-port.test.ts:34. Probe ejecutado: ownFactoryCalls 0, shared.master.gain 1, warnCalls 0 — el warn de sound.ts:119-128 nunca puede dispararse por el camino de la raíz porque las claves se filtran un nivel antes. Ningún test cubre la rama (sound-e2e.test.ts:194 usa `sound: true`); PLAN-sound-engine.md §16.5 documenta la precedencia como deliberada pero declara «tipo + aviso» como defensa, y ninguna alcanza aquí.…
### S-06 · La AM se suma al gain del envelope: la cola nunca llega a silencio y el earcon se corta con amplitud no nula
- **Dónde:** src/arts/sound/engine-sound.ts:557-580 y 584-589 · umbral en src/arts/sound/consts.ts:114-119 · base afectada en src/uix/sema/sema-map.ts:140
- **Defecto:** El modulador de AM se conecta a `envelope.gain`, que es un AudioParam automatizado: su aportación se SUMA a la automatización, no la multiplica. La rampa final lleva la automatización a 0.0001, pero el modulador sigue añadiendo hasta ±(roughness × depthScale × gainScale). Los osciladores se paran en `now + durationSec` con la señal en amplitud no nula → discontinuidad (click) al final de la nota.
- **Impacto:** Aritmética para la familia `signal` (gain base 0.4, roughness 0.3): profundidad = 0.3 × 0.5 = 0.15, freq AM = 30 + 0.1×150 = 45 Hz, duración 180 ms → 8,1 ciclos, la nota se corta con la AM en ~0.09 de amplitud, ≈22 % del pico. Es un clic audible al cierre de cada earcon de signal (los que más importan: alerta, error, notificación). Todo el trabajo de clamping del envelope (líneas 519-525) para evitar envolventes cuadradas queda parcialmente anulado por este offset.
- **Arreglo propuesto (NO aplicado):** Enveloparse la propia AM: llevar `modulatorGain.gain` por la misma rampa que el envelope (o encadenar un segundo gain multiplicativo tras el envelope en vez de sumar sobre su param), de modo que la profundidad de AM decaiga a 0 con la nota.
- **Verificación adversarial:** CONFIRMADO. El codigo existe donde se dice y la lectura es correcta. engine-sound.ts:557-564 automatiza envelope.gain (0 -> peakGain -> peakGain -> 0.0001 en now+durationSec); :566-580 conecta modulatorGain a envelope.gain, y por spec Web Audio un nodo conectado a un AudioParam se SUMA al valor de automatizacion (no lo multiplica); :584-589 para osc1/osc2/modulator en now+durationSec sin fade ni cola. Aritmetica verificada: consts.ts:114-119 da threshold 0.2, baseHz 30, slopeHz 150, depthScale 0.5 -> freq AM 45 Hz, profundidad 0.15; sema-map.ts:140 da signal roughness 0.3 > 0.2, gain 0.4, duration 180 -> ~0.088 de ganancia residual al cortar.
Busque los cuatro mecanismos neutralizadores posibles y NINGUNO existe: (1) Voz propia de sema: src/uix/sema/sounds.ts:22-27 SEMA_SOUND_VOICE es identica valor-por-valor a DEFAULT_SOUND_VOICE, incluido am.threshold 0.2 (registrada en chans/sound.ts…
### S-07 · Las tunings de escalera REEMPLAZAN el `gain` y colapsan el perfil evaluativo del intent
- **Dónde:** src/uix/sema/sounds.ts:171-192 · src/uix/sema/resolver.ts:271-275 · contradicho por src/uix/sema/components/dialog.ts:26-29 y toast.ts:27-30
- **Defecto:** Las tunings de escalera de `SOUND_TUNINGS` fijan `gain` como número desnudo, y `applyOverride` aplica los primitivos en modo REPLACE — así que la capa 5 borra la delta de gain que la capa 2 (intent) acababa de aportar. Es exactamente lo que la doctrina de CLAUDE.md y las cabeceras de los propios packs prohíben.
- **Impacto:** Un diálogo o un toast con intent `threat`/`risk`/`fulfill` suena con exactamente la misma sonoridad que uno neutro. Se pierde la diferencia evaluativa que es la razón de existir de la capa 2. Afecta a las 14 claves usadas por los packs: `commit.subtle` ×53, `commit.soft` ×42, `emerge.soft` ×14, `emerge.exit.soft` ×10 — es decir, prácticamente todo el corpus.
- **Arreglo propuesto (NO aplicado):** Expresar la escalera como delta relativa: `'emerge.strong': { gain: { op: 'add', value: -0.05 } }` etc., igual que ya hacen `commit.silent`/`emerge.silent`/`contact.silent`. Y extender `sounds-grammar.test.ts` con un test que prohíba `gain` numérico desnudo en `SOUND_TUNINGS` (el guard ya sabe leer el gain base de la familia desde `SEMA_MAP`, :51-53).
- **Verificación adversarial:** MECANISMO CONFIRMADO, con dos correcciones importantes a la evidencia y una rebaja del impacto.
CONFIRMADO (leido y MEDIDO ejecutando resolveSignature con las cascadas reales):
1. El codigo esta donde dice. sounds.ts:171-192 fija gain como numero desnudo en 9 tunings ('emerge.soft' 0.08, 'emerge.medium' 0.1, 'emerge.strong' 0.15, 'emerge.exit.soft' 0.05, 'emerge.dismiss.passive' 0.06, 'commit.soft' 0.05, 'commit.subtle' 0.03, 'commit.select.soft' 0.04, 'commit.medium' 0.1). resolver.ts:271-275 aplica `applyLeaf(current, delta, 'replace')` y applyLeaf:341 devuelve el delta crudo en modo replace. Los packs son capa 5a, posterior a la capa 2 (resolver.ts:156-161).
2. Medicion Dialog EXACTA: `open` family emerge intent 'threat' resuelve gain 0.15 CON el pack de dialog y 0.30000000000000004 SIN el. El +0.1 de threat se borra. Lo confirma que dialogMorfo declara `intent: { fromProp: 'intent',…
### S-08 · `fallbackTarget` pisa el `target` declarado por el morfo y deja reglas de pack sin casar (NavigationMenu `emerge-open`/`emerge-close`)
- **Dónde:** src/uix/soma/runtime.svelte.ts:757 · src/uix/sema/components/navigation-menu.ts:45,49 · src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts:151-154,166-169
- **Defecto:** `opts.fallbackTarget` GANA sobre el part declarado en el morfo, pese al nombre. Cuando un proveedor lo pasa, el estampado cae en un elemento distinto del que el pack seleccionó con `semaSelector(morfo, partDeclarado, …)`, y la regla deja de casar en silencio.
- **Impacto:** Las dos reglas `emerge` de NavigationMenu están muertas: abrir y cerrar un panel del nav suena con la base de familia `emerge` (gain 0.2) en vez de `emerge.soft` (0.08) / `emerge.exit.soft`. Es la MISMA clase de fallo que la cabecera del pack (`navigation-menu.ts:20-26`) declara haber corregido hoy para `commit-select`: se arregló una regla de tres y quedaron dos. Hay 185 llamadas con `fallbackTarget` en soma, así que el riesgo de más instancias es alto. AVISO: `sema/components/navigation-menu.ts` aparece modificado en el worktree por un agente concurrente; verifiqué el estado actual del fichero, pero puede cambiar.
- **Arreglo propuesto (NO aplicado):** (a) Renombrar/reordenar: si de verdad es un *fallback*, debe ser `targetReg?.ref?.current ?? opts.fallbackTarget`. Si es un override legítimo, llamarlo `targetOverride` y que el morfo lo declare. (b) Guard de censo que, para cada `Sema`, compruebe que el part del selector coincide con el `target` declarado del evento homónimo en el morfo — es mecánico y habría cazado este caso y el de 2026-08-05.
- **Verificación adversarial:** CONFIRMADO empíricamente, pero el encuadre del auditor sitúa el defecto en el sitio equivocado.
LO QUE SE SOSTIENE
1. `soma/runtime.svelte.ts:757` y `:829` dicen literalmente `opts.fallbackTarget ?? targetReg?.ref?.current ?? null` — el fallback gana. Verificado.
2. `morfo/components/navigation-menu.ts` declara `emerge-open` y `emerge-close` con `target: v.partRef('content')`; el pack (`sema/components/navigation-menu.ts:45,49`) selecciona `content`; el proveedor (`navigation-menu-provider.svelte.ts:150-154,162-169`) pasa `fallbackTarget: triggerEl`. Las tres piezas están donde dice.
3. El matcher real es `safeMatches` (`sema/resolver.ts:211`): `target.matches(sel) || target.closest(sel) !== null`. El razonamiento del auditor sobre `closest()` es correcto en mecanismo: verifiqué en el demo real (`web/routes/uix/components/navigation-menu/+page.svelte:138-160`) que `Content` es HERMANO d…
### S-09 · `applyMapOverrides` se traga las erratas de ruta y crea ramas fantasma
- **Dónde:** src/uix/sema/resolver.ts:388-415
- **Defecto:** `applyMapOverrides` no valida las rutas: un segmento inexistente se CREA en vez de fallar, así que una errata en una clave de `overrides.runtime` es un no-op silencioso que además contamina el mapa con una rama fantasma.
- **Impacto:** Un integrador que afina el mapa desde la raíz de composición no tiene forma de saber que su override no se aplicó. El síntoma es «el sonido no cambia», sin ninguna pista sobre la causa.
- **Arreglo propuesto (NO aplicado):** Validar cada segmento contra el objeto de origen antes de descender y lanzar `SemaConfigError` (ya existe, `errors.ts`) con la ruta y el segmento que falló. Alternativa más fuerte: tipar las rutas con template literal types derivados de `SemaMap`.
- **Verificación adversarial:** Confirmado en el código real y ejecutando applyMapOverrides con tsx sobre SEMA_MAP. resolver.ts:388-420: applyPathOverride desciende llamando cloneContainer(readChild(...)); cloneContainer devuelve {} para cualquier valor que no sea record ni array (undefined incluido) y readChild devuelve undefined para clave inexistente — no hay comprobación de existencia en ningún punto de la ruta. engine.ts:55 tipa runtime como Record<string, DeltaValue>, sin tipo de ruta template-literal, así que tampoco falla en compilación. Reproducido: (a) 'families.commmit.base.sound.pitch':850 deja commit.pitch en 700 y crea families.commmit={"base":{"sound":{"pitch":850}}}; (b) '...sound.pitchh':850 añade la hoja fantasma junto a pitch:700 intacto; (c) NO reportado por el auditor y PEOR: '...sound.pitch.deep':1 DESTRUYE la hoja real (sound.pitch pasa a ser {"deep":1}) — corrupción silenciosa, no simple no-op, …
### S-10 · `visual: { dom | projector | timers }` se pisa con los valores del engine (posiblemente undefined) y revienta el constructor
- **Dónde:** src/uix/sema/engine.ts:277-287 · src/uix/sema/chans/visual.ts:70-77
- **Defecto:** La rama visual hace spread de las opciones del canal y DESPUÉS sobrescribe incondicionalmente `dom`, `projector` y `timers` con los del engine. Si el llamante configuró el proyector en la bolsa anidada y no a nivel raíz, se le asigna `undefined` y `VisualChannel` lanza `SemaConfigError`. Es la asimetría exacta contra sound/haptic/announce, que sí usan `canal ?? engine`.
- **Impacto:** VERIFICADO EN RUNTIME (vitest, spec temporal ya borrada): `new EngineSemantic({ visual: { projector } })` lanza `SemaConfigError`; `new EngineSemantic({ visual: { dom } })` también; con la misma opción a nivel raíz ambos construyen sin error. El tipo `VisualChannelOptions` (visual.ts:29-38) publica los tres campos, así que el compilador bendice una configuración que hace estallar el boot. `contracts.test.ts:835-838` afirma el contrato `requiresOneOfForVisualProjection: ['dom','projector']` — que sólo se cumple por la ruta raíz.
- **Arreglo propuesto (NO aplicado):** Alinear la rama visual con las otras tres: `dom: visualOptions.dom ?? opts.dom`, `projector: visualOptions.projector ?? opts.projector`, `timers: visualOptions.timers ?? opts.timers`. Añadir al test de contratos un caso por la bolsa anidada.
- **Verificación adversarial:** CONFIRMADO en código y reproducido en runtime; no encontré nada que lo neutralice. (1) El código existe donde dice: engine.ts:280-285 construye `new VisualChannel({ ...(opts.visual ?? {}), dom: opts.dom, projector: opts.projector, timers: opts.timers })` — sobrescritura incondicional sin `??`, y visual.ts:70-77 lanza SemaConfigError cuando no hay ni projector ni dom. (2) La lectura es correcta y el tipo lo bendice: `EngineSemanticOptions.visual` (engine.ts:66) es `false | VisualChannelOptions | Channel`, y VisualChannelOptions (visual.ts:29-38) publica projector/dom/timers. (3) La asimetría es literal: sound (engine.ts:314-316), announce (346) y haptic (360-362) usan todos `anidado ?? raíz`. (4) Verificado en vitest con spec temporal (7/7, ya borrada): `new EngineSemantic({ visual: { projector } })` LANZA SemaConfigError; `{ visual: { dom } }` LANZA; las mismas opciones en raíz construye…
### S-11 · `SemaSignatureOverride` / `SoundOverride` desactivan TODA comprobación de claves y valores por la rama `Record<string, DeltaValue>`
- **Dónde:** src/uix/sema/channels.ts:109-119 · src/uix/sema/sounds.ts:4 y 221 · src/uix/sema/chans/sound.ts:176
- **Defecto:** Cada slice de canal es `Partial<SemaChannelSignatures[K]> | Record<string, DeltaValue>`. Como `DeltaValue` incluye `number | string | boolean | null`, la segunda rama traga cualquier clave con cualquier primitivo: ni el nombre del campo ni su tipo se comprueban. `SOUND_TUNINGS`, tipado `as const satisfies Record<string, SoundOverride>`, no valida nada de sus valores.
- **Impacto:** Un `gain` string llega intacto hasta el audio: `applyLeaf` (resolver.ts:344-346) devuelve el string tal cual, el corte nuevo `if (typeof sig.gain === 'number' && sig.gain * gainScale <= 0) return;` (sound.ts:176) lo deja pasar por el `typeof`, y `engine.play(sig, …)` (sound.ts:181) recibe `gain: 'loud'`. Por el lado morfo tampoco hay red: el esquema sium de `eventSemanticSchema` (morfo/schema.ts:260-281) NO declara `channels` ni `overrides`, y `object()` usa `unknownKeys: 'strip'` por defecto (arts/sium/core/object-combinator.ts:30) mientras `validateMorfo` devuelve el objeto CRUDO (morfo/schema.ts:757-768). El guard `sounds-grammar.test.ts` vigila la FORMA de las claves del catálogo, nunca sus valores.
- **Arreglo propuesto (NO aplicado):** Estrechar la rama abierta a los `DeltaOp` sobre campos conocidos: `{ [P in keyof S]?: S[P] | DeltaOp }` en vez de `Record<string, DeltaValue>`, dejando `Record<string, DeltaValue>` sólo para ids de canal SIN slice tipado. Alternativa mínima: tipar `SOUND_TUNINGS` contra `Partial<Record<keyof SoundSignature, …>>`.
- **Verificación adversarial:** CONFIRMADO en el código real y verificado con tsc, no por lectura. channels.ts:109-119 define SemaSignatureOverride como `{channels?} & {[K in keyof SemaChannelSignatures]?: Partial<SemaChannelSignatures[K]> | Record<string, DeltaValue>}`, y DeltaValue (74-81) incluye number|string|boolean|null, así que la rama con index signature traga cualquier clave con cualquier primitivo. sounds.ts:4 y :221 encadenan SoundOverride y `as const satisfies Record<string, SoundOverride>`. Ejecuté `npx tsc --noEmit -p tsconfig.json` sobre un fichero scratch en src/uix/sema/ (borrado después): el ÚNICO error emitido fue mi caso de control (`gain: {bad: () => 1}`, una función, fuera de DeltaValue). Compilaron limpios `{'commit.oops': {gian: 0.05}, 'commit.bogus': {contour: 'sideways'}} satisfies Record<string, SoundOverride>`, `{sound: {gain: 'loud'}}` y `{sound: {gain: 'loud', roughness: true}}`. El hallaz…
### S-12 · El pack de TextArea NUNCA casa: sus 2 reglas apuntan al `provider` y el estampado cae en el `input`
- **Dónde:** src/uix/sema/components/textarea.ts:14-15,21,26 · src/uix/morfo/components/textarea.ts:38,58 · src/uix/soma/components/textarea/textarea-provider.svelte.ts:227,257 · src/uix/sema/resolver.ts:211 · src/uix/soma/runtime.svelte.ts:757
- **Defecto:** Las dos únicas reglas del pack se construyen sobre la parte `provider` (`[data-textarea]`), pero el morfo declara ambos eventos con `target: v.partRef('input')` y el proveedor los dispara SIN `fallbackTarget`, así que `data-event*` se estampa siempre en el `<textarea data-textarea-input>`. Ningún nodo lleva a la vez `[data-textarea]` y `[data-event=...]`, luego la capa 5a no se aplica jamás.
- **Impacto:** `commit-submit` suena con la base de familia commit (gain 0.30, sema-map.ts:114-133) en vez del `commit.soft` 0.05 elegido → ~6× más fuerte. El aviso de desbordamiento suena con la base signal (gain 0.40) + deltas de `risk` en lugar de 0.03 → ~13× más fuerte, y el háptico pasa de `tap` a `pulse` 0.7/40ms con patrón de warning. La firma perceptiva escrita para el componente no existe en runtime.
- **Arreglo propuesto (NO aplicado):** Construir las dos reglas sobre la parte `input` (`semaSelector(textareaMorfo, 'input', …)`), que es lo que el morfo declara y donde el runtime estampa. Alternativa (peor): pasar `fallbackTarget: this.opts.ref.current` en el proveedor, pero eso contradice el `target` del morfo.
- **Verificación adversarial:** CONFIRMADO empíricamente, no refutado. (1) El código existe tal cual se cita: sema/components/textarea.ts:14-15,21,26 construye ambas reglas con semaSelector(textareaMorfo,'provider',…); morfo/components/textarea.ts:38,58 declara ambos eventos con target: v.partRef('input'); textarea-provider.svelte.ts tiene SOLO dos trigger() (227, 257) y ninguno pasa fallbackTarget; runtime.svelte.ts:757 resuelve target = opts.fallbackTarget ?? targetReg?.ref?.current; resolver.ts:211 es matches()||closest(). (2) partMarkerAttr (compile.ts:286-290) mapea provider→data-textarea e input→data-textarea-input, así que los selectores compilan a [data-textarea][data-event="commit-submit"] y [data-textarea][data-event="signal-warn-count-overflow"]. (3) Probe empírico en jsdom con morfo/pack/projector/resolver REALES: el DOM estampado es <div data-textarea><textarea data-textarea-input data-event="commit-submit…
### S-13 · TreeView: la selección por teclado no emite NADA — las dos reglas de `commit-select` son inalcanzables sin ratón
- **Dónde:** src/uix/soma/components/tree-view/tree-view-provider.svelte.ts:166-182,286-292 · src/uix/sema/components/tree-view.ts:52-61
- **Defecto:** `select(value, fromEl?)` sólo dispara el evento si recibe elemento (`if (fromEl)`), y el manejador de teclado del root llama `this.select(value)` sin pasarlo, teniendo `activeEl` en la mano. Enter/Espacio sobre un item no emite `commit-select`: no hay sonido, ni háptico, ni estampado `data-event-*` (luego tampoco reacciona el CSS de eidos ni el anuncio ligado al evento).
- **Impacto:** Asimetría teclado/ratón en un componente de navegación: seleccionar con el teclado es perceptivamente mudo. Las reglas #2 y #3 del pack (`commit.subtle` + tap sobre item y branch) sólo existen para el puntero.
- **Arreglo propuesto (NO aplicado):** Pasar `activeEl` en la llamada del teclado (`this.select(value, activeEl)`) o, mejor, resolver el elemento dentro de `select()` por `data-value` como hace TreeGrid, y eliminar la guarda `if (fromEl)`.
- **Verificación adversarial:** Verificado en el código real y no refutado. select(value, fromEl?) (tree-view-provider.svelte.ts:166-182) sólo dispara runtime.trigger('commit-select') dentro de `if (fromEl)`, y la rama ENTER/SPACE (:286-292) llama this.select(value) sin pasar `activeEl`, que tiene disponible desde :246. Los únicos call sites de producción son los tres citados (:291 teclado sin elemento, :430 branch-control con elemento, :595 item con elemento). No existe neutralizador: (a) Item/Branch/BranchControl renderizan <div>, no <button>, y además el handler hace e.preventDefault(), así que no hay click nativo sintético en Enter/Espacio; (b) el keyboard[] del morfo (morfo/components/tree-view.ts:85-94) es declarativo y el dispatch runtime.keydown no se usa aquí — el provider monta su propio onkeydown; (c) el fallback de trigger al ref registrado (runtime.svelte.ts:757) es irrelevante porque nunca se llama a trig…
### S-14 · GradientPicker: la regla `commit-reset` cuelga de un evento que el proveedor nunca dispara
- **Dónde:** src/uix/sema/components/gradient-picker.ts:16,40-45 · src/uix/soma/components/gradient-picker/gradient-picker-provider.svelte.ts:135,143 · src/uix/morfo/components/gradient-picker.ts:36
- **Defecto:** El morfo declara `commit-reset` y el pack le dedica una regla completa (canal + sample + háptico), pero el proveedor sólo emite `commit-save` y `commit-remove`; `'commit-reset'` no aparece en ningún fichero de soma/eidos de gradient-picker.
- **Impacto:** La acción Clear no emite nada del GradientPicker; lo único que suena es el `commit-reset` del Picker genérico compuesto (picker-provider.svelte.ts:201), que estampa sobre `[data-picker…]` y activa OTRA firma (la del pack `picker`). Regla muerta + documentación que miente.
- **Arreglo propuesto (NO aplicado):** Emitir `commit-reset` en la acción Clear del proveedor (con `fallbackTarget` si procede) o borrar la regla y la fila del README.
- **Verificación adversarial:** NO REFUTADO — las tres piezas existen exactamente donde dice y la lectura es correcta.
VERIFICADO:
1. `src/uix/morfo/components/gradient-picker.ts:36` declara el evento `commit-reset` (family `commit`, verb `reset`, target `v.partRef('provider')`, intent `neutral`).
2. `src/uix/sema/components/gradient-picker.ts:40-45` le dedica una regla completa (`onProvider({ eventName: 'commit-reset' })`, canales sound+haptic), con la doctrina en el comentario de cabecera (línea 16).
3. `src/uix/soma/components/gradient-picker/gradient-picker-provider.svelte.ts` sólo dispara `commit-save` (:135, en `savePreset`) y `commit-remove` (:143, en `deletePreset`). Grep exhaustivo sobre TODO el árbol soma+eidos de gradient-picker (12 ficheros): `commit-reset` aparece únicamente en los dos README (`soma/.../README.md:65` y `eidos/.../README.md:92`), en ningún `.ts`/`.svelte`.
NO HAY MECANISMO QUE LO NEUTRALI…
### S-15 · navigation-menu: las dos reglas de `emerge-*` añadidas hoy no pueden casar NUNCA — el selector apunta a `content` y el proveedor estampa en el `trigger`
- **Dónde:** src/uix/sema/components/navigation-menu.ts:44-51 (working tree, sin commitear) · src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts:150-154 y :162-169 · src/uix/soma/runtime.svelte.ts:757 · src/uix/sema/resolver.ts:208-215
- **Defecto:** Las reglas 2 y 3 del pack compilan a `[data-navigation-menu-content][data-event="emerge-open"]` y `[…][data-event="emerge-close"]`, pero el proveedor emite ambos eventos con `fallbackTarget: triggerEl`, y `fallbackTarget` GANA sobre el registro del part. El estampado cae en el `<button data-navigation-menu-trigger>`, que es HERMANO — no ancestro — del `<div data-navigation-menu-content>`. Ningún nodo cumple las dos condiciones del selector compuesto.
- **Impacto:** El panel del mega-menú suena con la base de familia `emerge` (gain 0.2, sema-map.ts:169) en vez de con el 0.08 de `emerge.soft` al abrir y el 0.05 descendente de `emerge.exit.soft` al cerrar: 2,5× y 4× más fuerte de lo diseñado, y sin la inversión de contorno que distingue apertura de cierre. Es EXACTAMENTE el defecto que la cabecera del mismo fichero declara reparado hoy (navigation-menu.ts:20-26: «⚠️ Until 2026-08-05 this pack was ONE rule matching `item` while the runtime stamped on the trigger, so the selector NEVER matched»): se arregló la regla de `commit-select` y se reintrodujo la misma enfermedad en las dos reglas nuevas del mismo commit.
- **Arreglo propuesto (NO aplicado):** O el pack apunta a `'trigger'` en esas dos reglas (que es donde el proveedor estampa, y es lo que `menubar.ts:23-33` hace bien), o el proveedor deja de pasar `fallbackTarget` para `emerge-open`/`emerge-close` y deja que el runtime resuelva el `content` que el morfo declara (`target: v.partRef('content')`, morfo navigation-menu.ts:41 y :50) — pero ojo: en `emerge-open` el content aún no ha montado su ref en ese tick, que es la razón por la que se puso el fallback. La opción sana es apuntar el pack al trigger. Y a nivel de framework: un guard que compruebe, por cada regla de pack, que el part del selector coincide con el part donde el proveedor estampa (morfo `target` + `fallbackTarget`) mataría esta clase entera — ya ha reincidido dos veces en el mismo fichero.
- **Verificación adversarial:** CONFIRMADO, y verificado empíricamente. No he encontrado ningún mecanismo que lo neutralice.
1) El código existe donde dice. `src/uix/sema/components/navigation-menu.ts:44-51` (working tree, sin commitear) construye `[data-navigation-menu-content][data-event="emerge-open"]` y `[…][data-event="emerge-close"]` — lo verifiqué imprimiendo el objeto compilado del pack, no sólo leyendo la fuente. El proveedor (`navigation-menu-provider.svelte.ts:150-154` y `:162-169`) emite ambos con `fallbackTarget: triggerEl`, y `runtime.svelte.ts:757` es literal: `const target = opts.fallbackTarget ?? targetReg?.ref?.current ?? null` — el fallback GANA sobre el ref del part registrado.
2) La lectura del anidamiento es correcta. `navigation-menu-content.svelte` renderiza su `<div>` como HIJO del `<li data-navigation-menu-item>`, hermano del `<button data-navigation-menu-trigger>`; el propio README lo dice …
### S-16 · navigation-menu.emerge-close declara `allowedFamilies` pero ningún proveedor concreta jamás — capacidad polimórfica muerta
- **Dónde:** src/uix/morfo/components/navigation-menu.ts:46-55 · src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts:166-169
- **Defecto:** De los 5 eventos polimórficos del corpus, 4 (dialog.close, drawer.close, popover.close, float-panel.close) tienen proveedor que pasa `opts.semantic`; el quinto no. El único call site de `emerge-close` no pasa `semantic`, así que la familia siempre se queda en `emerge` y las tres familias declaradas como capacidad no se ejercen nunca.
- **Impacto:** El morfo publica un contrato que el runtime nunca ejerce: `isPolymorphicSemantic(action.semantic) && opts.semantic` (runtime.svelte.ts:711) es siempre falso para este evento. Un consumidor que lea el morfo esperará poder cerrar el mega-menú como `commit` (selección que navega) o `signal` (fallo), y no hay camino. Nada lo detecta: ni `morfo:vocabulary` ni el runtime avisan de capacidad declarada sin uso — sólo hay error en el sentido contrario (`SomaRuntimePolymorphicError` cuando se pasa una familia fuera del allowlist).
- **Arreglo propuesto (NO aplicado):** O el proveedor concreta (un `DISMISS_CAUSES` como el de popover para distinguir cierre-por-navegación de cierre-por-blur), o se quita `allowedFamilies` del morfo y se deja el evento concreto. Y añadir al censo de morfos una comprobación de que cada `allowedFamilies` tiene al menos un call site que pasa `semantic`: es barato (grep tipado sobre `runtime.trigger(name, { semantic`) y convierte la capacidad declarada en capacidad verificada.
- **Verificación adversarial:** CONFIRMADO tras verificación adversarial completa, con una advertencia de proceso importante: el fichero del morfo fue EDITADO CONCURRENTEMENTE durante mi verificación (mtime 2026-08-05 23:21:38). Mi primera lectura devolvió una versión previa con UN solo evento (`commit-select` sobre `item`) y sin `allowedFamilies` en ningún sitio — eso habría refutado el hallazgo de plano. Una sonda de runtime (vite ssrLoadModule) reportó cuatro eventos, y la relectura mostró el fichero reescrito. El hallazgo es correcto contra el árbol de trabajo ACTUAL, que está SIN COMMITEAR (` M` en morfo, pack de sema y provider). Es decir: es una revisión de trabajo en vuelo (disposición A-34 del AUDIT-blocks-ledger.md), no de código consolidado.
Comprobaciones contra el estado actual, todas positivas:
1) `src/uix/morfo/components/navigation-menu.ts` líneas 46-55 son literalmente lo citado; `allowedFamilies: ['e…
### S-17 · El desestampado es CIEGO a la ocurrencia: la limpieza de una señal borra el estampado de otra que sigue viva
- **Dónde:** src/uix/sema/stamp.ts:38-46 y :59-68 · src/uix/sema/projection/dom.ts:19-21 y :62-71 · src/uix/sema/engine.ts:453-460
- **Defecto:** `clearEventAttrs()` retira TODA la superficie `data-event-*` del target sin mirar de quién es; `unstampEventAttrs` recibe el signal y lo ignora (`_signal`). Como el engine llama al cleanup de cada emit en su propio `finally`, la señal MÁS ANTIGUA (la que cumple su hold primero) arrasa el estampado de la señal MÁS RECIENTE que todavía está dentro de su hold sobre el mismo elemento.
- **Impacto:** Dos daños comprobables. (a) TRUNCACIÓN: al soltar el knob, el estampado de `commit-set` (t≈1000) lo borra el cleanup de un `handle-drag` anterior ~24 ms después, y la reacción CSS `[data-knob-control][data-event-family='commit'][data-event-phase='active']` (eidos/components/knob/knob.css:218) muere a los 24 ms en vez de a los 240 — exactamente el antipatrón que `VisualChannel.awaitExpression` existe para evitar. Durante el arrastre los `data-event-*` parpadean (presentes ~2/3 del tiempo, hueco de ~48 ms cada 72 ms). (b) SEÑAL PERSISTENTE FANTASMA: un `signal.warn + risk` (`persistence: 'untilFix'`, holds.ts:94) queda estampado indefinidamente; cualquier señal transitoria posterior sobre ese mismo target (un `contact-activate` del propio campo) le borra los attrs al terminar su hold, mientras `engine.active` sigue reteniendo la entrada — `hasActive(id)` sigue devolviendo `true` (engine.ts:467-469, 519-521) y `clear(id)` ya no tiene nada que retirar: el aviso desaparece de la pantalla y el engine cree que sigue vivo.
- **Arreglo propuesto (NO aplicado):** Hacer el cleanup condicional a la propiedad de la ocurrencia: `unstampEventAttrs` ya recibe el signal — comparar `target.getAttribute('data-event-id') === signal.id` (o guardar el id en el handle) y retirar SÓLO si coincide; si no coincide, el estampado ya pertenece a otra ocurrencia y el cleanup debe ser no-op. Corregir además la afirmación falsa de projection/dom.ts:19-21 y añadir el test del orden peligroso (A.cleanup() con B vivo).
- **Verificación adversarial:** NO REFUTADO en el mecanismo — todas las citas son exactas y reproduje los tres comportamientos con el EngineSemantic real (4/4 tests verdes; fichero en el scratchpad).
CONFIRMADO literalmente:
- stamp.ts:38-46 clearEventAttrs() borra los 5 attrs incondicionalmente; :59-68 unstampEventAttrs recibe `_signal` y lo descarta. El handle de projection/dom.ts:62-71 sólo cierra sobre `target` — el comentario de :19-21 («retira EXACTAMENTE los attrs que escribió») es FALSO tal cual.
- engine.ts:453-460 limpia todos los prepare handles en su `finally`; apply.ts:34-38 undefined→removeAttribute.
- knob.ts:183-252: los 5 eventos con target: v.partRef('control'); knob-provider:64 DRAG_SIGNAL_MS=72; holds.ts:105-107 handle._default brief=240; onRelease (160-167) dispara handle-drop + commit-set sobre el mismo control.
- projection/dom.test.ts:66-86 sólo prueba orden LIFO y su propio comentario admite e…
### S-18 · La atenuación por señal del háptico es inalcanzable justo para el vocabulario evaluativo (patrón explícito + kinds de patrón constante)
- **Dónde:** src/uix/sema/chans/haptic.ts:127-129 y :147-174 · src/uix/sema/engine.ts:187-199 y :545-548 · src/uix/sema/sema-map.ts:218-243 y :247-266
- **Defecto:** El engine atenúa el háptico escalando `haptic.intensity`, pero el canal no lee `intensity` en dos de sus tres caminos: cuando hay `pattern` lo reenvía tal cual, y los kinds `success` / `warning` / `error` devuelven patrones CONSTANTES que ignoran `duration` e `intensity`. Los intents que cargan esos casos son precisamente threat, risk, affirm y fulfill.
- **Impacto:** BK-FREQ-MEMORY no se cumple en el háptico para affirm/fulfill/risk (threat ya está exento por diseño): el aviso número 50 vibra EXACTAMENTE igual que el primero. Y `masterIntensity`, documentado en haptic.ts:49 como «Master intensity multiplier applied on top of every signature's intensity», no tiene efecto alguno en esas señales: un `new EngineSemantic({ haptic: { masterIntensity: 0.1 } })` sigue disparando [40,60,40,60,40] completo en un threat. El delta `fulfill.haptic.intensity {op:'add', value:0.2}` que el mapa declara explícitamente es, hoy, inexpresable. El propio canal SABE atenuar un patrón (`reduceHapticPattern`, haptic.ts:83-90, sí lo aplica a arrays y a pulsos sueltos) — sólo que la atenuación por señal no tiene por dónde entrar.
- **Arreglo propuesto (NO aplicado):** Un único punto de entrada de escala en el canal: aplicar el factor (intensity·masterIntensity) sobre el patrón FINAL, sea explícito o sintetizado, reutilizando la mecánica de `reduceHapticPattern` (escalar los índices pares, respetar las pausas y `KIND_MIN_MS`). Alternativa mínima: que `attenuateNonVisual` escale también `haptic.pattern` cuando exista, y que `kindToPattern` module los tres kinds constantes por `scale` en vez de devolver literales.
- **Verificación adversarial:** Verificado en el código real; no hay refutación. (1) haptic.ts:127-129 confirmado literal: la rama `sig.pattern` reenvía `[...sig.pattern]` sin multiplicar por intensity ni masterIntensity; sólo kindToPattern recibe `sig.intensity * this.masterIntensity`. (2) haptic.ts:158-173 confirmado: success→[10,40,20], warning→[30,50,30], error→[40,60,40,60,40] son constantes; `dur` (que incorpora scale=clamp(intensity,0.3,1) en :155-156) sólo entra en tick/tap/pulse/thud. (3) engine.ts:187-199 y 545-548 confirmados, y FREQ_FLOOR=0.25 (engine.ts:175) garantiza que la memoria de frecuencia NUNCA alcanza el camino `factor<=0` — el único que funciona con patrones porque saca haptic de activeChannels; ese camino sólo lo usa la dominancia (engine.ts:592). (4) Los deltas SÍ llegan: resolver.ts:156-161 aplica intent deltas con independencia de la familia y applyChannelDeltas (287-304) sólo exige base+cana…
### S-19 · El cableado de announce que la doctrina prescribe no compila, y ninguna raíz de composición enchufa el canal
- **Dónde:** src/uix/sema/chans/announce.ts:34 · src/uix/active-uix/types.ts:223 · docs/architecture/sema.md:762 · src/uix/active-uix/active-uix.svelte.ts:144-156 · src/uix/sema/define-engine-semantic.ts:43-54
- **Defecto:** `AnnounceFn` pide la prioridad en un objeto de opciones; `uix.announce` la toma posicional. La inyección literal que documenta la doctrina — `{ announce: uix.announce }` — es un error de tipos, y no existe adaptador. Además ninguno de los dos modos de arranque pasa jamás la opción `announce` al engine.
- **Impacto:** El camino «una sola región viva compartida» que la doctrina declara canónico (AUX-1/SEM-1) es hoy inalcanzable sin un cast. Quien active el canal cae por fuerza en el fallback autopropietario (announce.ts:109-131), que añade un SEGUNDO par de regiones `role=status`/`role=alert` al documento junto a las de `uix.announce` (active-uix.svelte.ts:548-583): dos sumideros vivos, que es justo lo que la doctrina «ONE sink, two emitters» prohíbe. Si alguien fuerza el cast, `priority` llega como objeto y `this.liveRegionIds[priority]` es `undefined`: todo cae en la región polite y un threat deja de interrumpir.
- **Arreglo propuesto (NO aplicado):** Alinear la firma (o enviar un adaptador de una línea desde la raíz): que `AnnounceFn` sea `(message: string, priority: AnnouncePriority) => void`, o que active-uix pase `announce: (m, { priority }) => this.announce(m, priority)` al construir el `EngineSemantic`. Y corregir sema.md:762 para que muestre el cableado que de verdad compila.
- **Verificación adversarial:** No hay contradicción: las tres afirmaciones se sostienen. (1) La incompatibilidad de tipos es real y la reproduje con un probe tsc propio e independiente: AnnounceFn (announce.ts:34) pide `opts: { priority }` y lo invoca así en :82, mientras `uix.announce` (types.ts:223, impl. active-uix.svelte.ts:548) es posicional `(message, priority?, timeout?)`. Declaré el probe como METODO de interfaz (bivarianza) y aun asi falla: TS2322, «Types of parameters 'priority' and 'opts' are incompatible» — no es artefacto de strictFunctionTypes, no encaja en ninguna dirección. (2) No existe adaptador: el único puente `announce` del framework (soma.svelte.ts:99, metrics/onion-menu/menu-dial) sirve al puerto POSICIONAL de soma (runtime.svelte.ts:165), otra forma distinta; esa asimetría entre los dos puertos es la raíz del defecto. (3) Ninguna raíz enchufa el canal: active-uix.svelte.ts:142-156 y define-engi…
### S-20 · D.7 prohíbe samples en packs que no sean `signal`, y proof-of-human los usa en dos eventos `commit` — aplastando los deltas de intent
- **Dónde:** src/uix/sema/components/proof-of-human.ts:12-13,37,44 · docs/decisions/book-deviations.md:385-393 · src/uix/morfo/components/proof-of-human.ts:67-87 · src/uix/sema/sounds.ts:119-148 · src/uix/sema/resolver.ts:250-279
- **Defecto:** D.7 fija: «Los packs componen tunings, nunca samples directos», con UNA excepción — la familia `signal`— y remata «No se canonizan samples en packs de commit, emerge, contact, handle, shift, sustain, delegate». El pack de proof-of-human usa samples en dos eventos de familia `commit`, y su propia cabecera afirma lo contrario de lo que hace.
- **Impacto:** El veredicto (confirm/fail) pierde la modulación por intent que D.7 existe para preservar: cambiar `intent` no cambia nada audible porque suena el WAV. Además el comentario miente al siguiente que edite el fichero, que es el mecanismo exacto por el que la deriva se propaga. Colateral del mismo eje: `SOUND_LIBRARY` está declarado «recurso, NO se usa en packs canónicos» (book-deviations.md:387) y hoy aparece en 31 reglas de packs (slider.ts:26, drawer.ts:37/56/66, knob.ts:21/34, splitter.ts:38/51, rotate-align.ts:28/41, path-trace.ts:31/43, gradient-*.ts, color-picker.ts:35/45, css-field.ts:37, number-field.ts:33, drag-drop.ts:25/31, float-panel.ts:24/29/40) — la doctrina D.7 es letra muerta a escala de catálogo, no un desliz puntual.
- **Arreglo propuesto (NO aplicado):** Decidir y ESCRIBIR: o proof-of-human compone tunings (`commit.soft`/`commit.subtle` + `{op:'add'}`) y deja los samples fuera, o D.7 se amplía con la excepción real («familias sin `base.sound` — handle — y verdictos con marca cultural»), con su razón y su fecha. En cualquier caso corregir la cabecera del pack, que hoy afirma lo contrario del código.
- **Verificación adversarial:** Confirmado en el código real, sin mecanismo neutralizador. (1) proof-of-human.ts:37,44 usa sound('notification.ping') y sound('alert.error'); ambos son sample() en sounds.ts:119 y :139. (2) El morfo declara esos eventos como family 'commit' con intent 'fulfill' / 'risk' (morfo/components/proof-of-human.ts:67-74, 82-89) — exactamente la familia que D.7 nombra como prohibida (book-deviations.md:393), cuya única excepción es 'signal' (:390). (3) El orden del pipeline es decisivo y verificado: intent (capa 2, modo 'add') en resolver.ts:156-161, packs (capa 5a, modo 'replace') en :175-180; applyLeaf en 'replace' sobrescribe toda clave presente en el delta, y sound() devuelve la firma COMPLETA (resolveSoundDefinition, sounds.ts:230-239). El fulfill compuesto (commit base 700/0.3/'flat' + fulfill +300/+0.05/'ascending' — sema-map.ts:114-124, 255-266) queda reemplazado por 960/0.12/'arc'. El com…
### S-21 · D.8 «los packs DEBEN respetar el activeChannels de la familia» está contradicha por sema.md y por dos reglas INERTES del pack de dialog
- **Dónde:** docs/decisions/book-deviations.md:442-455 · docs/architecture/sema.md:883-894 · src/uix/sema/components/dialog.ts:93-108 · src/uix/sema/sema-map.ts:156-202 · src/uix/sema/chans/haptic.ts:113-116 · src/uix/sema/components/slider.ts:22-33
- **Defecto:** D.8 declara: «Los packs DEBEN respetar el `activeChannels` del family. Añadir `haptic` a un pack que opera sobre family `emerge` es incoherente; añadir `sound` a `handle` también.» El código hace las DOS cosas: sema.md prescribe explícitamente la segunda, y la primera produce dos reglas que nunca se ejecutan.
- **Impacto:** Un lector de D.8 concluye que el catálogo respeta una regla que el catálogo abandonó; un lector de sema.md hace lo contrario de D.8 y acierta. Mientras tanto dos reglas del dialog viven en el árbol como si hicieran algo (alertdialog y sheet declaran carácter háptico que el usuario no siente jamás) — deuda invisible porque no hay guard que compare pack ↔ activeChannels.
- **Arreglo propuesto (NO aplicado):** D.8 debe re-escribirse con lo que la práctica decidió: `channels` es el mecanismo LEGÍTIMO para ampliar la activación de una familia (el continuo lo exige), y lo prohibido es declarar una firma de canal SIN activarlo. Con esa ley, las dos reglas emerge+haptic del dialog o añaden `channels: ['sound','haptic']` o se borran. Guard natural: un test que recorra los packs y falle cuando una regla declara `sound`/`haptic` que el `activeChannels` resultante no incluye.
- **Verificación adversarial:** Ambas patas se sostienen contra el código real. (a) dialog.ts:93-96 y 105-108 declaran `haptic` sobre `eventFamily: 'emerge'` sin `channels:`; resolver.ts:143 siembra activeChannels del family (['sound'], sema-map.ts:169) y applyOverride (resolver.ts:256-258) sólo reemplaza activeChannels si la regla trae `channels` — declarar `haptic` puebla `next.haptic` (rama current===undefined, líneas 265-269) pero NO activa el canal; el morfo del dialog no declara `channels` en ningún evento y sus eventos open/close son family 'emerge' (morfo/components/dialog.ts:30,87); haptic.ts:114 corta con `if (!effective.activeChannels.includes('haptic')) return`. El motor sólo quita canales, nunca añade (engine.ts:187-199) y no hay override runtime sobre families.emerge (grep 0 hits). Además resolver.test.ts:111 testea explícitamente que emerge no emite haptic: las dos reglas son inertes por diseño testado. …
### S-22 · El contrato `emit` de sema.md dice `Promise<void>`; el engine devuelve `Promise<string>` desde D.9
- **Dónde:** docs/architecture/sema.md:74-77 · src/uix/sema/engine.ts:391 · docs/decisions/book-deviations.md:531
- **Defecto:** La sección «The `emit` contract» —la firma canónica de la capa— publica una firma que el código no tiene desde que D.9 introdujo la persistencia.
- **Impacto:** La sección que un integrador lee para entender el contrato oculta el único valor que permite cerrar una señal persistente. Quien siga sema.md al pie de la letra no sabe que puede recoger el id, y `clear(id)` queda inalcanzable salvo leyendo el código.
- **Arreglo propuesto (NO aplicado):** Actualizar la firma a `Promise<string>` y añadir una línea explicando que el string es el id de la ocurrencia (puente a la sección de persistencia y a `clear`/`clearTarget`).
- **Verificación adversarial:** CONFIRMADO en las líneas exactas. docs/architecture/sema.md:76 publica `semantic.emit(signal: SemanticSignal): Promise<void>` con «One signature.» (:79); src/uix/sema/engine.ts:391 declara `async emit(signal: SemanticSignal): Promise<string>` con `return id` (:402 y :471) y `clear(id): boolean` (:485). Busqué activamente el neutralizador y no existe: `emit` se declara UNA sola vez en todo src/ (no hay port/interfaz más estrecha que justifique el `void`; `EventEngineEmitter` en soma/runtime.svelte.ts:90 es `Pick<EngineSemantic,'emit'>` y hereda el `string`), ningún test asserta la firma del doc, y docs-check —que sí incluye docs/** en el corpus— no tiene regla para firmas TS en fences (I8 imports, I9 símbolos —`emit` existe, no dispara—, I10 tablas de props). La deriva es incluso más ancha: sema.md no menciona `persistence`, `clear`, `clearTarget` ni `hasActive` en ningún punto, pese a su…
### S-23 · channels.md fija «3 canales runtime» y D.8 dice de Announce «hoy no»; el framework envía un cuarto canal, `announce`, desde 2026-07-04
- **Dónde:** docs/theming/channels.md:46-56 · docs/decisions/book-deviations.md:425-430,484 · docs/CANON.md:216-219 · src/uix/sema/chans/announce.ts:64-65 · src/uix/sema/engine.ts:81-88,338-350 · src/uix/sema/exports.ts:74-76 · docs/architecture/sema.md:127-145,743-752
- **Defecto:** Dos documentos de doctrina siguen describiendo un registro de canales que el código superó: channels.md enumera 3 canales runtime y D.8 cierra explícitamente la puerta a convertir Announce en canal («Hoy no»). El `AnnounceChannel` existe, es built-in, opt-in y exportado.
- **Impacto:** El registro «autoritativo de desviaciones» afirma una decisión que el propio framework revirtió hace un mes: quien lo consulte para saber qué es canal recibe la respuesta contraria a la que da el código. Y quien lea channels.md contará 3 canales al diseñar una app o una extensión (y, con la a11y por medio, es el canal que peor tolera quedarse fuera del mapa).
- **Arreglo propuesto (NO aplicado):** Marcar D.8 como SUPERSEDED en su propia entrada (con puntero a sema.md § Announce channel) y corregir el conteo de channels.md a 4 canales runtime (visual proyectado + sound + haptic + announce), más `chans/announce.ts` en el árbol de sema.md.
- **Verificación adversarial:** CONFIRMADO en lo esencial; dos citas del hallazgo se sobreextienden.
VERIFICADO EN CÓDIGO (todo existe donde dice):
- `src/uix/sema/chans/announce.ts:64-65` → `export class AnnounceChannel implements Channel { readonly id = 'announce';`. Fichero completo (132 líneas) con `handle()`, `dispose()`, regiones live propias de fallback.
- `src/uix/sema/engine.ts:88` → `announce?: true | false | AnnounceChannelOptions | Channel;` en `EngineSemanticOptions`, junto a `visual`/`sound`/`haptic`.
- `src/uix/sema/engine.ts:338-350` → registro real en el constructor (`this.register(announceChannel)`), simétrico al de `haptic` (352-366).
- `src/uix/sema/exports.ts:73-79` → `AnnounceChannel` + 4 tipos en la superficie pública, bajo el bloque `// ── Channels ──`.
- `engine.ts:185` (política de dominance) trata `announce` como canal de primera clase: «...accessible `announce` channel are never touched». N…
## Severidad BAJA
### S-24 · El fallback de sample es absolutamente mudo: cuatro salidas de fallo sin un solo diagnóstico
- **Dónde:** src/arts/sound/engine-sound.ts:649-663 · :238-242 · src/arts/sound/consts.ts:17-23
- **Defecto:** `playSample` devuelve `false` en cuatro caminos (sin fetcher, `!response.ok`, throw del fetch, throw del decode) y `play()` cae a `synthesize()` sin emitir nada. El catálogo `SOUND_DIAGNOSTIC_EVENTS` ni siquiera tiene un evento para el fallo de sample.
- **Impacto:** Un .wav renombrado, un 404 del static, un CORS o un códec no soportado quedan enmascarados para siempre: el usuario oye un earcon sintético plausible y nadie —ni consola, ni logger, ni test— se entera. Exactamente el riesgo que el autor sospechaba. Agrava el hallazgo 1: como la firma del sample YA borró las intent deltas, el earcon que suena en el fallback tampoco es el canónico de la familia.
- **Arreglo propuesto (NO aplicado):** Añadir `SAMPLE_FAILED: 'sound.sample_failed'` al catálogo y emitirlo (una vez por URL, `warnedSamples: Set<string>`, mismo patrón que `warnedVoices` en :393-398) en los cuatro returns, con `{ url, reason: 'no-fetcher'|'http'|'network'|'decode' }`. El fallback debe seguir existiendo (audio ornamental), pero ruidoso en dev.
- **Verificación adversarial:** CONFIRMADO en lo esencial. El código está donde dice y la lectura es correcta: engine-sound.ts:649/655/659-661 (y un quinto retorno no listado en :663) devuelven false sin emitir nada, y play() (:238-242) cae a synthesize() sin diagnóstico — el único emitSoundDiagnostic de play() está en el catch externo (:244), que un return false nunca alcanza. consts.ts:17-23 declara exactamente los cinco eventos citados y ningún fallo de sample; además diagnostics.ts:22-25 tipa SoundDiagnosticMeta como {error}|{liveContexts}|{voice}, así que ni siquiera hay forma de meta capaz de llevar la URL fallida. Tampoco hay test: engine-sound.test.ts (20 casos) no menciona fetcher/playSample/sampleUrl/preload, ni sound.test.ts ni sound-e2e.test.ts de sema. Corroboración que el auditor NO vio y que refuerza el hallazgo: la sustitución silenciosa análoga SÍ está instrumentada — VOICE_UNKNOWN avisa ("the default …
### S-25 · `preload()` no precarga nada y sí abre un AudioContext en el boot de la página
- **Dónde:** src/arts/sound/engine-sound.ts:248-265 y :402-426 (vs. el propio comentario :428-437) · src/uix/sema/engine.ts:329-334 · src/uix/active-uix/active-uix.svelte.ts:143
- **Defecto:** `preload` resuelve el contexto con `getOrCreateContext()`, que CREA el AudioContext y luego hace `await ctx.resume()`. Como sema dispara el preload dentro de su constructor y active-uix construye `EngineSemantic` de forma eager, en la carga de página el contexto está `suspended` sin gesto de usuario: el `resume()` queda pendiente y el fetch+decode nunca se emite.
- **Impacto:** Dos consecuencias: (a) se abre un AudioContext en la carga de página aunque nadie haya sonado — Chrome escupe el aviso de autoplay y `liveContexts` sube, contradiciendo el comentario de active-uix.svelte.ts:128-131 («no AudioContext, no listeners until the first `prime()`»); (b) el único preload declarado del repo (`chatLogSema.preloadSamples` → /sounds/ping.wav) no decodifica nada hasta el primer gesto, que suele ser el mismo evento que dispara el primer earcon — la latencia fetch+decode que el preload existe para eliminar sigue ahí en la primera reproducción.
- **Arreglo propuesto (NO aplicado):** Usar `ensureContext()` en `preload` (decodificar funciona con el contexto suspendido, igual que en `decode`) y, si se quiere cero-coste en boot, no crear contexto: `if (!this.audioCtx) return;` y reintentar el preload desde `prime()`. Alternativa complementaria: que sema no llame al preload en el constructor sino desde el primer `prepare()`.
- **Verificación adversarial:** No refutado: el código está donde dice y la lectura es correcta. `preload()` (engine-sound.ts:248-251) resuelve el contexto con `getOrCreateContext()` (:402-426), que CREA el AudioContext (master + 2 buses = los 3 `gain` del harness), instala el unlock listener y luego hace `await ctx.resume()`; el propio fichero documenta que ese resume fuera de gesto queda pendiente (:428-437) y por eso `decode()` usa `ensureContext()`. El disparo eager existe: sema/engine.ts:329-335 lanza `void soundChannel.preloadSamples(...)` en el constructor, y active-uix.svelte.ts:134-156 construye `EngineSound` + `EngineSemantic` eager con `soundEngine: sound`, así que el contexto abierto es el COMPARTIDO — justo el que el comentario :128-133 promete que no existe hasta `prime()`. Consumidor real confirmado: web/routes/uix/+layout@.svelte:119-129 (`sound: true` + `chatLogSema`), y chatLogSema es efectivamente el…
### S-26 · Tres de los cinco .wav son código muerto: whoosh, pop y ding no los referencia nadie
- **Dónde:** src/uix/sema/sounds.ts:99-118 y :129-138 · static/sounds/{whoosh,pop,ding}.wav
- **Defecto:** `overlay.whoosh`, `surface.pop` y `notification.ding` no aparecen en ningún pack, demo, test ni doc de uso. La única vía que los alcanzaría —`soundSampleUrls()` sin argumentos, que devuelve las 5 URLs— no se invoca en ninguna parte.
- **Impacto:** 38 KB de assets desplegados (whoosh 15.9 K + pop 8.9 K + ding 13.3 K) y tres entradas de catálogo que aparentan una cobertura sonora que no existe: quien lea `SOUND_LIBRARY` cree que overlay/surface tienen voz de sample y no la tienen. Además `overlay.*` y `surface.*` no son familias sema (ver hallazgo del guard).
- **Arreglo propuesto (NO aplicado):** Decisión del autor entre dos: (a) borrarlos del catálogo y del static —requiere instrucción explícita de borrado—, o (b) dejarlos y documentarlos como recursos de producto disponibles (D.7 los admite como «recursos de producto, tema o branding»), en cuyo caso conviene un comentario que diga que NO están cableados.
- **Verificación adversarial:** El núcleo factual SE SOSTIENE, pero el encuadre y la severidad no. VERIFICADO: sounds.ts:99/109/129 declaran overlay.whoosh, surface.pop y notification.ding exactamente donde dice; grep limpio del árbol real (el grep original estaba contaminado por .claude/worktrees/, que devolvía copias de sounds.ts como si fueran consumidores) confirma que las únicas apariciones son sus propias declaraciones. Consumidores reales: notification.ping (proof-of-human.ts:37, chat-log.ts:26) y alert.error (proof-of-human.ts:44, form.ts:22). soundSampleUrls sólo se invoca en chat-log.ts:22 con ['notification.ping']; ningún sample pasa preload:false, así que la llamada sin argumentos sí devolvería las 5 URLs. Tamaños exactos: 15920+8864+13274 = 38058 B. NINGÚN guard lo neutraliza: sounds-grammar.test.ts recorre SOUND_TUNINGS, no SOUND_LIBRARY, y sounds.test.ts sólo toca handle.*, notification.ping y emerge.exi…
### S-27 · prepare() abre el AudioContext aunque la preferencia sea `off`
- **Dónde:** src/uix/sema/chans/sound.ts:150-156 y 197-200 (llamado desde src/uix/sema/engine.ts:406-409)
- **Defecto:** El gate de `prepare` sólo mira `signal.channels`; nunca consulta `preferences.sound`. Con `sound: 'off'` el canal sigue creando el AudioContext y registrando los 5 listeners de desbloqueo en el documento en fase de captura — silencia la salida, no la adquisición de recursos.
- **Impacto:** Un usuario que silenció el canal paga igualmente un AudioContext vivo por documento (hilo de audio + dispositivo abierto, con la interacción que eso tiene con otras sesiones de audio del sistema) y cinco listeners globales. Contradice el encabezado del propio canal (líneas 20-25: «`off` silencia») y la promesa del art («an app that never makes a sound pays nothing», engine-sound.ts:687-688). El test que parece cubrirlo — sound.test.ts:113 «no-ops (never touches audio) when preferences.sound is off» — sólo ejercita `handle`, nunca `prepare`, así que el guard cree estar puesto y no lo está.
- **Arreglo propuesto (NO aplicado):** En `shouldPrime` (o antes de `this.engine.prime()`) devolver false cuando `this.preferences?.sound === 'off'`, y extender el test de 'off' para llamar también a `prepare`. La puerta por familia no puede moverse (prepare corre antes de resolver, y prime debe ser síncrono dentro del gesto); la de preferencias sí es legible en ese momento.
- **Verificación adversarial:** No refutado. Verificado en el código: sound.ts:150-156 `prepare()` → `shouldPrime(signal)` (:197-200, sólo mira `signal.channels`) → `engine.prime()`; `this.preferences` se lee ÚNICAMENTE en `handle()` (:166). `engine-sound.ts:203-220` `prime()` crea el AudioContext y llama `setupUnlockListener()` (:479-508 → 5 tipos de evento en captura + visibilitychange si autoSuspend). `engine.ts:406-409` invoca `prepare` para todo canal registrado; el único corte previo es `signal.channels.length === 0` (:401). No hay guard aguas arriba: el registro del canal (:289) depende de `opts.sound`, nunca de preferencias. Y `sound.test.ts:113` efectivamente sólo ejercita `handle` pese a titularse «never touches audio»; PLAN-sound-engine.md:116 fija RC-4 como «el factory NUNCA se llama (no se toca el audio en absoluto)», contrato documentado que el código no cumple. Correcciones a la evidencia: (a) no son cin…
### S-28 · prime() puede orfanar el contexto y filtrar el contador global de contextos vivos
- **Dónde:** src/arts/sound/engine-sound.ts:203-220 (catch), 455-477 (createContext / liveContexts++), 316-337 (dispose)
- **Defecto:** El `try` de `prime()` abarca `createContext()` (que ya incrementó `liveContexts` y dejó el contexto vivo), `setupUnlockListener()` y `resume()`. Si algo posterior a la creación lanza, el `catch` pone `audioCtx = null` sin cerrar el contexto ni decrementar el contador: el contexto queda huérfano (nadie puede cerrarlo ya, ni siquiera `dispose()`) y `liveContexts` se queda inflado para siempre en el realm.
- **Impacto:** Un dispositivo de audio del SO queda abierto sin dueño, y el diagnóstico que existe para cazar el anti-patrón de doble contexto empieza a mentir: acusa de duplicado al primer contexto legítimo posterior. Además el `catch` es completamente mudo (el art tiene catálogo de diagnósticos y este camino no emite ninguno), así que la corrupción es invisible.
- **Arreglo propuesto (NO aplicado):** Cerrar/decrementar en el catch (o mover `setupUnlockListener()` y `resume()` fuera del try, que es lo que ya hace `getOrCreateContext`: allí sólo `createContext()` está dentro), y emitir un diagnóstico en vez de tragarse el error.
- **Verificación adversarial:** NO REFUTADO — el código está donde dice y la lectura es correcta, verificada por lectura Y por ejecución.
VERIFICACIÓN DE CÓDIGO (G:\dev\svelte\vicen\src\arts\sound\engine-sound.ts):
- `prime()` 203-220: el `try` abarca `createContext()` + `setupUnlockListener()` + `ctx.resume()`; el `catch` solo hace `this.audioCtx = null; this.masterGainNode = null;`. Ni `close()`, ni `liveContexts--`, ni diagnóstico. Literal como se afirma.
- `createContext()` 455-477: asigna `this.audioCtx`, monta el árbol de buses, y `liveContexts++` (470) es de las ÚLTIMAS sentencias, seguida del `emitSoundDiagnostic(MULTIPLE_CONTEXTS)`. Así que un throw posterior deja contador incrementado + contexto vivo.
- `dispose()` 316-337: todo el cierre está bajo `if (this.audioCtx)`. Con `audioCtx` nulificado, el contexto es INALCANZABLE (el getter `context` también devuelve null). Confirmado: nadie puede cerrarlo jamás.
…
### S-29 · El motor sigue documentando como MECANISMO lo que la auditoría AU-4 ya corrigió («prefs.sound mapea al bus ui»)
- **Dónde:** src/arts/sound/engine-sound.ts:44-47 · src/arts/README.md:120 · frente a src/arts/sound/README.md:44-52 y docs/process/PLAN-sound-redesign.md:188
- **Defecto:** La corrección de AU-4 se aplicó al README del art y a sema.md, pero la cabecera del propio motor (y la tabla índice de arts) siguen enunciando el mapeo como si existiera. No hay ningún productor: nada en `src/` conecta `prefs.sound` con `bus('ui')` ni con `SemaPreferences`; el único consumidor de `prefs.sound` es la proyección DOM (`data-sound`).
- **Impacto:** Quien lee el motor (la fuente más cercana al código) cree que `prefs.sound.set('reduce')` atenúa la UI. No atenúa nada: sólo cambia `data-sound` en `<html>`. Es la misma trampa que AU-4 nombró — invariante enunciada como mecanismo sin productor — sobreviviendo en el fichero auditado.
- **Arreglo propuesto (NO aplicado):** Alinear la cabecera de `engine-sound.ts` (y la fila de `src/arts/README.md`) con el texto ya corregido del README: el bus `ui` es la PALANCA que una app puede cablear; lo garantizado por construcción es el negativo (ninguna política de UI escribe el bus `content` ni el volumen de una fuente).
- **Verificación adversarial:** CONFIRMADO en todos sus extremos, y con alcance MAYOR del declarado.
1) El texto existe donde dice. `src/arts/sound/engine-sound.ts:44-47` (bloque JSDoc del motor, viñeta **Mix**): «Policy lives per bus (gain / mute / refcounted duck holds); `prefs.sound` maps to the `ui` bus ONLY and must never touch the content's volume». `git blame` lo fecha en `2f872ef864` (2026-07-31) — el MISMO commit que introdujo el texto ya corregido del README del art, o sea que la corrección de AU-4 y la frase sin corregir conviven desde el minuto cero. `src/arts/README.md:120` repite la fórmula en la celda del art: «`prefs.sound` governs `ui` only».
2) La contradicción intra-art es literal. `src/arts/sound/README.md:44-53` dice lo contrario: «Honest about the mechanism (audit AU-4): today `prefs.sound` acts through sema's channel … and **nothing wires prefs to a bus directly**. The `ui` bus gain/mute is the…
### S-30 · Cinco reglas de pack escriben en un canal que la familia no activa: código muerto
- **Dónde:** src/uix/sema/components/dialog.ts:94-96,106-108 · editable.ts:18 · stepper.ts:18 · timeline.ts:33 · gate en src/uix/sema/chans/haptic.ts:114
- **Defecto:** Cinco reglas de pack escriben una firma en un canal que su familia nunca activa. El canal comprueba `activeChannels` y sale, así que la regla no hace nada.
- **Impacto:** El pulso táctil del alertdialog, el tap del sheet y los ticks de editable/stepper/timeline no se emiten jamás. La intención de diseño está escrita y no llega al dispositivo.
- **Arreglo propuesto (NO aplicado):** Añadir `channels: ['sound','haptic']` a esas reglas —el patrón que `slider.ts:24-31` ya usa para reactivar `sound` en la familia `handle`— o borrarlas. Y un guard de censo que cruce cada regla con `SEMA_MAP.families[familia].activeChannels`: es el mismo barrido que hice, cabe en un test.
- **Verificación adversarial:** CONFIRMADO — no encuentro contradicción; lo he medido, no sólo leído.
1) El código está donde dice. `src/uix/sema/chans/haptic.ts:114` → `if (!effective.activeChannels.includes('haptic')) return;`. `sema-map.ts:169` emerge → `activeChannels: ['sound']`; `:190` shift → `['sound']`. `dialog.ts:94-96` (alertdialog, acotada `eventFamily:'emerge'`), `dialog.ts:106-108` (data-sheet, acotada a emerge), `editable.ts:18` (evento `shift-enter-mode`, familia shift fija en `morfo/components/editable.ts:17`), `stepper.ts:18` (`shift-step`, familia shift, `morfo/components/stepper.ts:18`), `timeline.ts:33` (`emerge-reveal`, familia emerge, `morfo/components/timeline.ts:49`).
2) La lectura del resolver es correcta. `resolver.ts:143` siembra `activeChannels` desde la familia; sólo se reescribe en `:165` (`signal.channels`, capa 3) y `:257` (`override.channels`, capas 5a/5b). Una regla de pack que trae…
### S-31 · `applyOverride` clona un parcial como si fuera una firma completa y descarta deltas no-objeto
- **Dónde:** src/uix/sema/resolver.ts:264-270
- **Defecto:** Cuando el slice del canal no existe todavía en la firma, `applyOverride` clona el delta PARCIAL tal cual como si fuera una firma completa, y descarta en silencio los deltas que no son objeto.
- **Impacto:** Hoy latente, no explosivo: las tres reglas que producen parciales (`editable.ts:18`, `stepper.ts:18`, `timeline.ts:33` → `{kind:'tick'}` a secas) son además inertes por el hallazgo anterior, y las de dialog llevan `pattern`, que en `haptic.ts:127` gana sobre `kind`+`duration`. Pero en cuanto se arregle el hallazgo 4 activando el canal, `kindToPattern('tick', undefined, undefined)` (`haptic.ts:147-156`) calcula `Math.max(0.3, Math.min(1, NaN)) = NaN` y termina en `navigator.vibrate(NaN)`. Es decir: el arreglo obvio del hallazgo 4 destapa este.
- **Arreglo propuesto (NO aplicado):** Fusionar contra la base de la familia en vez de clonar el parcial, o rechazar/avisar cuando el delta no completa la firma. Mínimo: no tratar un `Partial<T>` como `T` en el tipo de retorno.
- **Verificación adversarial:** CONFIRMADO por lectura y por ejecución. El código está literalmente en src/uix/sema/resolver.ts:264-270 tal como se cita. Reproduje el caso medido: con un target realista de dialog (solo casan [data-event-family="emerge"] y [data-event-intent="threat"]), resolveSignature({name:'open',family:'emerge',intent:'threat'}) devuelve haptic:{kind:'error',pattern:[50,80,50,80,50]} — sin intensity ni duration, ambos REQUERIDOS en HapticSignature (channels.ts:35,38). EffectiveSignature.haptic?: HapticSignature (resolver.ts:104) es efectivamente unsound. No hay guard, test ni fallback que lo neutralice: validation.ts solo valida eventos/intent-bindings, nunca firmas; resolver.test.ts:111-115 cubre emerge SIN cascade, y todos los tests de packs (314-463) usan familias handle/commit/signal que sí tienen base completa — la rama del slice ausente no está cubierta.
LO QUE EL AUDITOR NO NOMBRÓ (contenció…
### S-32 · Las opciones de motor propio del SoundChannel se tiran en silencio cuando la raíz inyecta `soundEngine` — que es SIEMPRE
- **Dónde:** src/uix/sema/engine.ts:289-320 (rama 305-310) · src/uix/active-uix/active-uix.svelte.ts:150 · src/uix/sema/chans/sound.ts:91-128
- **Defecto:** Cuando `opts.soundEngine` existe, el engine promueve las opciones del canal de la forma B (motor propio) a la forma A (motor compartido) construyendo `{ engine, preferences, logger }` y descartando `masterGain`, `audioContextFactory`, `fetcher`, `dom` y `timers`. El aviso anti-contrabando de `SoundChannel` no ve nada porque el engine ya limpió el objeto antes de pasarlo.
- **Impacto:** VERIFICADO EN RUNTIME: `new EngineSemantic({ projector, logger, soundEngine: shared, sound: { masterGain: 0.5, audioContextFactory, fetcher } })` deja `channel.engine === shared`, pierde `masterGain` y `warn` NO se llama ni una vez; sin `soundEngine` las mismas opciones sí aplican (`ownsEngine === true`); y el aviso sí dispara por construcción directa. Es exactamente el fallo que el comentario de engine.ts:295-298 («they used to be accepted and dropped in silence») y la unión de sound.ts:84-88 dicen haber matado — sobrevive en la ruta que usan todas las apps.
- **Arreglo propuesto (NO aplicado):** En esa rama, detectar las claves de la forma B presentes en `soundOptions` y avisar por el logger (reutilizar `OWN_ENGINE_ONLY_KEYS` + `SOUND_CHANNEL_LOGS.IGNORED_ENGINE_OPTIONS`), o mejor: no aceptarlas en el tipo cuando `EngineSemanticOptions.soundEngine` está declarado.
- **Verificación adversarial:** Confirmado, no refutado. El código está donde dice y la lectura es exacta: engine.ts:305-310 promueve la forma B a la forma A construyendo `new SoundChannel({ engine: opts.soundEngine, preferences, logger })`, de modo que masterGain/audioContextFactory/fetcher/dom/timers se pierden; active-uix.svelte.ts:134-150 inyecta soundEngine incondicionalmente (y en attach lo hacen services.ts:90 + define-engine-semantic.ts:51), y el aviso de sound.ts:119-128 sólo mira el objeto ya recortado. Busqué neutralizadores y no hay ninguno: (a) el tipo no lo ataja — `sound` y `soundEngine` son campos independientes en EngineSemanticOptions (engine.ts:68 y :78) y ActiveUixOptions.events es la superficie entera (active-uix/types.ts:77), así que `createActiveUix({ events: { sound: { masterGain } } })` compila; la unión discriminada y el @ts-expect-error de sound-port.test.ts:34-38 sólo cierran la mezcla DENTR…
### S-33 · `intentRequirement: 'forbidden'` no prohíbe nada — degrada a 'optional' en silencio, y el docblock promete lo contrario
- **Dónde:** src/uix/sema/types.ts:25, 44-46, 90-102, 206-231
- **Defecto:** `IntentOptionalFamily = Exclude<SemaFamily, IntentRequiredFamily>`. La derivación sólo distingue 'required'; cualquier valor que no sea 'required' —incluido 'forbidden'— cae en el cubo OPCIONAL, donde `intent` está PERMITIDO. La unión no cubre el tercer valor del eje que ella misma declara.
- **Impacto:** El día que alguien endurezca la doctrina de `contact` (el propio comentario de types.ts:45-46 lo anticipa: «contact may move here if the book hardens its prohibition») creerá haber cerrado la puerta editando una línea del const, y el compilador seguirá aceptando `contact + threat` sin decir nada. `validateSemaEvent` tampoco cubre 'forbidden': validation.ts:39-46 sólo comprueba `=== 'required'`.
- **Arreglo propuesto (NO aplicado):** Derivar también `IntentForbiddenFamily` y añadir la tercera rama a `SemaEvent` (`{ family: IntentForbiddenFamily; intent?: never }`), o eliminar 'forbidden' de `IntentRequirement` hasta que exista. Añadir la rama simétrica en `validateSemaEvent` / `isSemaEvent`.
- **Verificación adversarial:** CONFIRMADO en lo técnico, REFUTADO en el impacto. Verificado línea por línea: types.ts:25 declara los tres valores; types.ts:90-94 deriva IntentRequiredFamily sólo con `extends 'required'`, así que una familia 'forbidden' produce `never` y Exclude no la quita — cae en el cubo OPCIONAL (types.ts:102). Lo repliqué con tsc real (--noEmit --strict) flipando contact a 'forbidden': exit 0. `'contact' extends IntentOptionalFamily` es true y `{family:'contact', intent:'threat'}` compila; la sonda de control (@ts-expect-error sobre `{family:'commit'}`) SÍ se consumió, luego la puerta 'required' funciona y la asimetría es real. validation.ts:39-46 sólo comprueba 'required', tal cual. Además encontré DOS sitios que el auditor no vio, ambos agravantes: (1) src/uix/sema/event.ts:99-110 `isSemaEvent` tiene el mismo agujero — si el intent está PRESENTE devuelve `isIntent(...)||isIntentBinding(...)` sin…
### S-34 · El gemelo runtime de la política de intent está copiado a mano en el esquema de morfo
- **Dónde:** src/uix/morfo/schema.ts:205-216, 260-281 · src/uix/sema/types.ts:69-84
- **Defecto:** `SEMA_FAMILY_POLICY` se declara fuente única, pero el esquema sium que valida los morfos en runtime reescribe la clasificación con literales fijos. Editar el const cambia el TIPO y deja el validador apuntando a la partición vieja.
- **Impacto:** Divergencia latente entre el gate de compilación y el de runtime. Hoy coinciden (comprobado literal a literal), así que no hay bug vivo; pero es la misma clase de deriva silenciosa que costó dos meses y medio con `form.*` — copiar el canon en vez de leerlo. Contrasta con `event.ts:14-31`, que sí lo ata con `as const satisfies readonly SemaValencedFamily[]`.
- **Arreglo propuesto (NO aplicado):** Generar ambos esquemas desde `SEMA_FAMILY_POLICY` (`Object.entries(...).filter(([, p]) => p.intentRequirement === 'required').map(([f]) => literal(f))`) o, como mínimo, un test que compare los literales del esquema contra las claves derivadas del const.
- **Verificación adversarial:** CONFIRMADO estructuralmente, con severidad deflactada.
1) El código existe donde dice, literal a literal.
- `src/uix/morfo/schema.ts:205-216`: `semaTransitionalFamilySchema` = union(emerge, shift, sustain, delegate); `semaIntentOptionalFamilySchema` = union(transitional, contact, handle); `semaIntentExpectedFamilySchema` = union(commit, signal). Se consumen en `eventSemanticSchema` (260-281): rama 1 con `intent: optional(...)`, rama 2 con `intent` obligatorio.
- `src/uix/sema/types.ts:69-84`: `SEMA_FAMILY_POLICY ... as const satisfies Record<SemaFamily, ...>`; required = commit+signal, optional = las otras 6. La partición coincide HOY exactamente con la del esquema.
2) La lectura es correcta y el desacople es total.
- `schema.ts` NO importa nada de `../sema` (imports en 31-45: `$sium`, `../intent`, `./types`, `./errors`). Cero atadura.
- En cambio `src/uix/morfo/types.ts:35-37, 416-439…
### S-35 · La forma etiqueta de `SemaActionEvent` esquiva la política de intent en tipo Y en runtime
- **Dónde:** src/uix/sema/types.ts:152, 233 · src/uix/sema/validation.ts:28-33
- **Defecto:** `SemaActionEvent = SemaEvent | SemaEventLabel` y `SemaEventLabel = SemaFamily | \`${SemaFamily}-${Intent}\``, así que el string `'commit'` es un `SemaActionEvent` válido aunque `commit` tenga `intentRequirement: 'required'`. `validateSemaEvent` remata el agujero devolviendo temprano ante cualquier etiqueta.
- **Impacto:** La misma unión discriminada que impone el intent por la rama estructurada lo regala por la rama string. `normalizeSemaEvent('commit')` devuelve `{ family: 'commit', intent: null }` (event.ts:172-176) — un commit sin carga evaluativa, justo el documento que la política declara inválido.
- **Arreglo propuesto (NO aplicado):** Estrechar la etiqueta: `SemaEventLabel` como `IntentOptionalFamily | \`${SemaFamily}-${Intent}\``, y en `validateSemaEvent` parsear la etiqueta y aplicarle la misma comprobación de `intentRequirement` en vez de retornar.
- **Verificación adversarial:** No pude refutarlo: el código está donde dice y la lectura es correcta. Verificado con tsc — el `@ts-expect-error` sobre `{ family: 'commit' }` como `SemaEvent` SE CONSUME (la rama estructurada sí cierra), mientras `const viaLabel: SemaActionEvent = 'commit'` compila limpio; y `validation.ts:29` (`if (isSemaEventLabel(event)) return`) corta antes del chequeo de política de :39-46. La asimetría tipo/runtime es real. PERO el auditor no midió la alcanzabilidad, y eso baja la severidad: (1) `SemaActionEvent` NO tiene ningún consumidor fuera de los tres ficheros del propio sema (`event.ts`, `validation.ts`, `exports.ts`) — ni morfo, ni soma, ni eidos, ni el engine, ni un solo componente. (2) La puerta real sigue intacta en los DOS ejes: `MorfoEventSemantic` (morfo/types.ts:416-439) es unión estructurada que exige intent para `IntentExpectedFamily`, y `eventSemanticSchema` (morfo/schema.ts:260-…
### S-36 · El barrel `sema/components/index.ts` omite 3 packs — y la sección blocks los pierde precisamente por derivar del barrel
- **Dónde:** src/uix/sema/components/index.ts:12-79 · src/uix/sema/components/calendar.ts:17 · src/uix/sema/components/gradient-builder.ts:30 · src/uix/sema/components/gradient-picker.ts:25 · web/routes/blocks/_lib/sema-packs.ts:1-23
- **Defecto:** Hay 71 ficheros de pack y el índice sólo re-exporta 68: faltan `calendarSema`, `gradientBuilderSema` y `gradientPickerSema`. Cualquier consumidor que siga el canon («importa del barrel») recibe 68 de 71 packs y no hay error, sólo silencio.
- **Impacto:** En toda la sección /blocks (ambas superficies), Calendar, GradientBuilder y GradientPicker emiten con las bases de familia crudas en vez de su firma autorizada. El mecanismo diseñado para ser a prueba de deriva queda derrotado por el barrel incompleto — y cualquier app externa que siga el canon hereda el mismo agujero.
- **Arreglo propuesto (NO aplicado):** Añadir las tres líneas `export { … } from './calendar' | './gradient-builder' | './gradient-picker'` y respaldarlo con un guard de censo (fichero de pack ⇒ export en el índice), que es lo único que impide que vuelva a pasar.
- **Verificación adversarial:** El defecto es REAL y no está neutralizado: 71 ficheros de pack vs 68 re-exports en src/uix/sema/components/index.ts; faltan exactamente calendar, gradient-builder y gradient-picker, y los tres exportan su Sema (calendar.ts:17, gradient-builder.ts:30, gradient-picker.ts:25). web/routes/blocks/_lib/sema-packs.ts:23 hace Object.values sobre el barrel y alimenta ambas superficies (+layout@.svelte:61, _lib/BootUix.svelte:62). Busqué activamente el neutralizador y no existe: sin import.meta.glob en sema/active-uix, sin auto-registro en engine.ts (solo `for (const pack of opts.components ?? [])`, l.266), sin test de completitud del barrel, y ninguno de los guards (packs:check, morfo:check, blocks:check) lo cubre — blocks-check.ts solo PROHIBE que un block importe sema. Pero el IMPACTO alegado está inflado en dos puntos verificados. (1) Ningún block compone Calendar, GradientBuilder ni GradientP…
### S-37 · Chronos: 2 reglas para eventos que nadie emite (`handle-drag`, `handle-resize`)
- **Dónde:** src/uix/sema/components/chronos.ts:51-58 · src/uix/morfo/components/chronos.ts (eventos declarados) · src/uix/soma/components/chronos/ + src/uix/eidos/components/chronos/ (21 ficheros, 0 ocurrencias)
- **Defecto:** El pack declara reglas para `handle-drag` sobre `event-chip` y `handle-resize` sobre `event-resize-handle`; el morfo declara ambos eventos, pero las cadenas `'handle-drag'` / `'handle-resize'` no aparecen en NINGÚN fichero .ts/.svelte de soma ni de eidos de chronos. Nunca se disparan.
- **Impacto:** La manipulación directa (arrastrar/redimensionar un evento del calendario) no produce ningún feedback perceptivo: las dos reglas son código muerto y la promesa del SPEC no está implementada. Chronos queda sin la capa háptica en flight que su propio pack describe.
- **Arreglo propuesto (NO aplicado):** O implementar la emisión en el proveedor al traducir el reuso de drag-drop a comandos, o retirar las dos reglas del pack. (Nota: chronos está excluido de escritura por política del proyecto — sólo auditoría.)
- **Verificación adversarial:** CONFIRMADO en lo esencial. El código está donde dice: sema/components/chronos.ts:51-58 declara las dos reglas (semaSelector sobre 'event-chip'/handle-drag y 'event-resize-handle'/handle-resize, haptic-only); morfo/components/chronos.ts:107-127 declara ambos eventos. El proveedor soma dispara EXACTAMENTE los 8 triggers citados (360, 452, 561, 566, 1184, 1191, 1202, 1248) y ninguno es handle-*; el grep global no encuentra 'handle-drag'/'handle-resize' en ningún .ts/.svelte de soma ni eidos de chronos, y chronos-view.svelte no tiene ni una llamada a trigger(. No hay mecanismo que lo neutralice: el selector generado exige coincidencia exacta (selectors.ts:166 → [data-event="handle-drag"]), el resolver matchea por target.matches()/closest() (sema/resolver.ts:197-211), el pack SÍ está registrado (sema/components/index.ts:17), y no existe guard, test ni grandfather que cubra "evento declarado y…
### S-38 · 5 reglas de pack escriben `haptic` sobre familias cuyo `activeChannels` nunca incluye háptico — el bloque es letra muerta
- **Dónde:** src/uix/sema/components/timeline.ts:30-34 · src/uix/sema/components/stepper.ts:12-19 · src/uix/sema/components/editable.ts:12-19 · src/uix/sema/components/dialog.ts:93-96 · src/uix/sema/components/dialog.ts:105-108 (contra src/uix/sema/sema-map.ts:169 y :190, y src/uix/sema/chans/haptic.ts:114)
- **Defecto:** Cinco reglas de cascada declaran `haptic: {...}` sobre eventos de familia `emerge` o `shift`, cuyas entradas en SEMA_MAP declaran `activeChannels: ['sound']`. El resolver INSTALA la firma háptica (resolver.ts:260-270, rama `current === undefined`) pero no toca `activeChannels`, y `HapticChannel.handle` sale por la puerta de entrada.
- **Impacto:** El diseño perceptivo escrito en cinco packs no llega nunca al usuario: el alertdialog no pulsa, la hoja móvil no da el `tap` de aparición, el paso del stepper y la entrada en modo edición no tienen tacto, y la entrada de feed del timeline tampoco. Peor que un bug silencioso: el pack DOCUMENTA la intención (timeline.ts:21) y el código la contradice, así que la lectura del pack miente al siguiente autor. Nada lo detecta — `morfo:vocabulary` no mira las reglas de cascada, y `sounds-grammar.test.ts` sólo inspecciona claves del catálogo.
- **Arreglo propuesto (NO aplicado):** Dos caminos, decisión del autor: (a) añadir `channels: ['sound','haptic']` en cada una de las 5 reglas — es el patrón ya canonizado en el repo (css-field.ts:36, drag-drop.ts:24 y :30 lo hacen exactamente así para levantar `sound` sobre la familia `handle`); o (b) borrar los bloques `haptic` si la doctrina es que emerge/shift no vibran, y decirlo en el comentario. Sea cual sea, añadir al guard un censo que cruce cada regla de pack con las familias que su selector puede ver y falle cuando escribe un canal que ningún `activeChannels` alcanzable activa: es el mismo tipo de cruce mecánico que ya hace `sounds-grammar.test.ts`, aplicado a la aplicación en vez de a la clave.
- **Verificación adversarial:** CONFIRMADO mecánicamente, pero el IMPACTO ALEGADO queda REFUTADO por doctrina que el auditor no vio.
Lo verificado ejecutando el resolver real + HapticChannel real (test aislando UNA regla por sonda, src/uix/sema/__adv-verify-haptic-dead.test.ts, 8/8 verde): las cinco reglas resuelven activeChannels ["sound"] con la firma haptic instalada y vibrate() llamado 0 veces. Controles de familia commit: ["sound","haptic"] y vibrate 1 vez ([10,40,20] y 14). El código existe donde dice y la lectura del resolver es correcta: applyOverride (resolver.ts:260-270, rama current===undefined) instala next.haptic pero jamás toca activeChannels; haptic.ts:114 corta por la puerta de entrada.
Neutralizadores descartados: ningún morfo de timeline/stepper/editable/dialog declara `channels` (sólo css-field, float-panel, number-field, virtual-list usan ese mecanismo); engine.ts:435-442 despacha a TODOS los cana…
### S-39 · 10 reglas aplican un tuning cuya cabeza de familia MIENTE sobre la base que modifica — la ley de nombrado escrita hoy no la comprueba nadie
- **Dónde:** src/uix/sema/sounds.ts:152-169 (la ley) y src/uix/sema/sounds-grammar.test.ts:60-69 (el guard) · aplicaciones: calendar.ts:40-41 · chronos.ts:34-35 · css-field.ts:46-47 · editable.ts:13-14 · file-upload.ts:19-20 y :29-30 · password-field.ts:39-40 · stepper.ts:13-14 · tags-input.ts:24-25 · textarea.ts:26-27
- **Defecto:** La ley del catálogo dice que «the FIRST segment is the sema family whose base signature the tuning modifies». Diez reglas aplican un tuning `commit.*` sobre eventos de familia `shift`, `signal` o `contact`. El guard nacido hoy sólo valida la FORMA de la clave contra `SEMA_FAMILIES`; no mira sobre qué familia se aplica, así que la cabeza puede mentir sin que nada falle.
- **Impacto:** Dos daños. El semántico: quien lee `soundTuning('commit.subtle')` sobre un `signal-warn-*` no puede saber si la ganancia resultante es 0.3−algo o 0.4−algo, que es justo la ambigüedad que el renombrado de hoy vino a matar. El perceptivo: `signal` es la familia MÁS alta del mapa (0.4, sema-map.ts:145) precisamente porque reclama atención; cinco avisos de validación quedan 8–13× por debajo de esa base porque importan la escala de `commit`. Y el riesgo latente es el que ya se materializó una vez: si mañana `commit.subtle` pasa a aditivo (como los `.silent`, que ya lo son), las diez aplicaciones se rompen en silencio — la aritmética `{op:'add'}` sí depende de la familia sobre la que cae.
- **Arreglo propuesto (NO aplicado):** No renombrar el catálogo: la clave está bien: es la APLICACIÓN la que hay que decidir. Por cada una de las diez, o se sustituye por un tuning de su propia familia (`signal.subtle`, `shift.subtle`, `contact.subtle` — hoy no existen y habría que crearlos con la ganancia deliberada de esa familia), o se escribe el override literal con su número. Y extender `sounds-grammar.test.ts` con un censo que recorra los packs, resuelva la familia que cada selector puede ver y falle si la cabeza del tuning no está entre ellas: la ley ya declara el invariante, sólo le falta el guard que lo extraiga.
- **Verificación adversarial:** Verificado línea a línea, el núcleo del hallazgo se sostiene entero. (1) La ley existe donde dice: sounds.ts:154-156 «the FIRST segment is the sema family whose base signature the tuning modifies», replicada en docs/architecture/sema.md:395-398. (2) El guard es exactamente tan ciego como se alega: sounds-grammar.test.ts:60-69 filtra `TUNING_KEYS` por `!families.has(key.split('.')[0])` — valida la FORMA de la clave del catálogo, jamás el sitio de aplicación. (3) Las 10 aplicaciones existen en las líneas citadas y las familias son las alegadas, leídas en el morfo, no en el nombre: signal-warn-count-overflow/signal-notify-caps-state/signal-warn-reject(×2)/signal-warn-invalid = family 'signal'; trigger-picker = 'contact'; shift-step/shift-navigate(×2)/shift-enter-mode = 'shift'. Bases en sema-map.ts: signal 0.4, commit 0.3, contact 0.25, shift 0.18. (4) La aritmética es correcta: resolver.ts…
### S-40 · El `verb` de la concreción polimórfica se descarta con `void` — la documentación del tipo promete que se emite
- **Dónde:** src/uix/soma/runtime.svelte.ts:722-724 y :785-795 · src/uix/morfo/types.ts:404-407
- **Defecto:** Los cuatro proveedores de overlay pasan `verb` dentro de `opts.semantic`. El runtime lo asigna a `resolvedVerb` y acto seguido lo tira; el `emit` sale con `name: action.name` (el nombre del morfo), sin verbo. El comentario del tipo dice lo contrario.
- **Impacto:** Dato muerto que viaja desde cuatro call sites, y una promesa falsa en el documento que un autor lee para escribir el quinto. En la práctica los cuatro cierres estampan el mismo `data-event="close"` y sólo se distinguen por `data-event-family` + `data-last-action`, que es de lo que dependen los packs — funciona, pero por una vía distinta de la que el tipo anuncia. El día que alguien escriba un selector `[data-event^="discard"]` confiando en el ejemplo, no casará nunca.
- **Arreglo propuesto (NO aplicado):** Decidir cuál de las dos verdades gana. Si el verbo debe llegar, añadirlo al payload del emit y estamparlo (o componer el `name` con él); si no debe, corregir el comentario de types.ts:404-407 para que diga que la concreción cubre familia e intent pero NO el verbo, y quitar `verb` de los cuatro `DISMISS_CAUSES` para que el dato deje de fingir que viaja.
- **Verificación adversarial:** Hallazgo confirmado en el codigo real. (1) runtime.svelte.ts:709/722/724 — resolvedVerb se calcula, se sobrescribe con provided.verb y se descarta con `void resolvedVerb; // currently unused at trigger-time; reserved for tooling/logging.`; grep en el fichero devuelve SOLO esas tres lineas, no hay uso posterior. (2) El emit (:785-795) no lleva verb y estructuralmente NO PUEDE: src/uix/sema/signal.ts (SemanticSignal) y sema/types.ts:221-231 (SemaEvent) no declaran campo `verb` en absoluto. (3) sema/stamp.ts:28 escribe 'data-event': signal.name, y los cuatro proveedores disparan el unico evento 'close' (dialog-provider.svelte.ts:266), luego las cinco causas estampan data-event="close" y solo se distinguen por data-event-family / data-event-intent / el prewrite imperativo data-last-action (dialog-provider.svelte.ts:261-264). La lectura del auditor es literalmente correcta. (4) Peor de lo des…
### S-41 · `preferences.sound: 'off'` no impide crear el AudioContext ni registrar los listeners globales: el gate de prepare sólo mira `signal.channels`
- **Dónde:** src/uix/sema/chans/sound.ts:150-156 y :197-200 (contrastar con :166-167)
- **Defecto:** La política de reducción por canal (BK-REDUCTIONS) se lee en `handle` pero NO en `prepare`. Con el sonido en `off`, cada emit sigue llamando a `engine.prime()`, que crea el `AudioContext`, monta el árbol de buses y engancha los listeners globales de desbloqueo.
- **Impacto:** Una app con el sonido apagado por preferencia sigue abriendo un AudioContext (recurso limitado por el navegador y compartido con `uix.sound`) y registrando `pointerdown/mousedown/click/touchstart/keydown` en el documento, en el primer gesto del usuario. Colateral del mismo hueco: una familia sin sonido en `activeChannels` (p. ej. `handle`, sema-map.ts:192-202 `activeChannels: ['haptic']`) también ceba el audio.
- **Arreglo propuesto (NO aplicado):** Mover la política al gate: `shouldPrime` debe devolver `false` cuando `this.preferences?.sound === 'off'` (y, si se quiere apurar, cuando la familia del signal no declare `sound` en su `activeChannels`), y fijarlo con un test sobre `prepare`, no sobre `handle`.
- **Verificación adversarial:** El código es exactamente el descrito. sound.ts:150-156 `prepare()` sólo consulta `shouldPrime(signal)` y llama `engine.prime()`; sound.ts:197-200 `shouldPrime` mira únicamente `signal.channels`, nunca `preferences`; la política `off` vive sólo en `handle` (sound.ts:166-167). engine-sound.ts:203-220 confirma que `prime()` crea el contexto y llama `setupUnlockListener()`, que engancha los 5 eventos en `document` en captura (:479-489). engine.ts:405-409 ejecuta `prepare` de todos los canales en cada `emit`, con el único filtro de `signal.channels: []` (:401-403). Ningún test cubre `prepare` + prefs off (sound.test.ts:113-127 y sound-port.test.ts:126-133 sólo ejercitan `handle`; sound.test.ts:171-189 sólo el caso `channels:['haptic']`). Las dos citas de doctrina existen literalmente (sema.md:647 y :691). No hay guard, fallback ni capa que lo neutralice. Lo que sí recorta el alcance, y el aud…
### S-42 · Las live regions propias del canal nunca se limpian: el anuncio queda residente en el árbol de accesibilidad
- **Dónde:** src/uix/sema/chans/announce.ts:100-107 y :121-131
- **Defecto:** `writeLiveRegion` escribe el mensaje y no programa ningún vaciado. Los otros dos sumideros de live region del framework sí caducan su texto; éste sólo lo suelta en `dispose()`.
- **Impacto:** El último mensaje («Item deleted», «Session expires in 30s») permanece indefinidamente en el árbol de accesibilidad: un usuario de lector de pantalla que navegue con cursor virtual se topa con el estado caducado fuera de contexto — el «ruido continuo» que BK-A11Y-CRITICAL pide evitar. Y la creación directa de nodos rompe la regla de capa (sema no toca DOM salvo el estampado) teniendo el vehículo sancionado disponible.
- **Arreglo propuesto (NO aplicado):** Programar el vaciado con `semaDelay(this.timers, …)` como hacen los otros dos sumideros (el canal ya recibe scheduler en el resto de casos; hoy `AnnounceChannelOptions` ni siquiera lo acepta), y construir/retirar las regiones a través del aplicador DOM inyectado en vez de `createElement`+`appendChild`.
- **Verificación adversarial:** El núcleo del hallazgo se sostiene literalmente. `announce.ts:100-107` es exactamente como se cita y no programa vaciado alguno; `AnnounceChannelOptions` (:41-52) ni siquiera admite un scheduler y `engine.ts:344-347` sólo le reenvía `announce`+`dom`, mientras `visual.ts:38` y `haptic.ts:58` sí reciben `SemaTimerScheduler` y usan `semaDelay` — el vehículo de caducidad existe DENTRO de sema y este canal no lo toma. El contraste aportado es exacto: `active-uix.svelte.ts:548-595` (timeout=5000 + `timers.schedule('uix:announce:…')` → textContent='') y `announce-provider.svelte.ts:86,106-124` (clearAfter por prioridad). No hay guard, test ni fallback que lo neutralice: `announce.test.ts:51-69` sólo verifica la escritura y la retirada en dispose(), nunca la caducidad. PERO refuto la segunda mitad y bajo la severidad. (1) La 'violación de capa' es falsa: el puerto que sema declara es `DomApplier…
### S-43 · La tabla «canónica» de holds transcrita en D.9 quedó desfasada: dos valores que D.12 corrigió 200 líneas más abajo
- **Dónde:** docs/decisions/book-deviations.md:506-527 (esp. 515, 521) · src/uix/sema/holds.ts:80-104 · docs/decisions/book-deviations.md:741-743
- **Defecto:** D.9 presenta en presente la «Tabla canónica `SEMA_HOLDS_BY_INTENT` (`src/uix/sema/holds.ts`)» con dos valores que el fichero ya no tiene.
- **Impacto:** El mismo fichero afirma dos cosas incompatibles y la primera es la que se presenta como «tabla canónica». Un autor que declare `persistence`/`hold` copiando D.9 escribe 600 ms donde el runtime resuelve 400 y 240.
- **Arreglo propuesto (NO aplicado):** Sustituir el bloque de D.9 por un puntero a `holds.ts` (o regenerarlo), y dejar la nota «corregida en D.12» en el sitio. La ley de este corpus —enlazar, no copiar— es justo la que aquí se rompió.
- **Verificación adversarial:** Confirmado literalmente, sin desviación. book-deviations.md:506 rotula en presente «Tabla canónica SEMA_HOLDS_BY_INTENT (src/uix/sema/holds.ts)» y publica en :515 `fulfill: { hold: 'noticed' }` y en :521 `loss: { hold: 'noticed' }`. El fichero real tiene holds.ts:86 `fulfill: { hold: 'settled' }` y holds.ts:103 `loss: { hold: 'brief' }`, ambos con el comentario in-situ «corrected 2026-07-06». D.12 decisión 2 (:741-744) registra el cambio (fulfill 600→400 settled; loss 600→240 brief) 200 lineas mas abajo EN EL MISMO DOCUMENTO. La cadena de impacto se sostiene: morfo/types.ts:481 y :593 declaran `persistence?` y `hold?: SemaDurationSpec` como campos del evento, resolver.ts:137-139 hace `signal.hold ?? resolveSemaDuration(resolveHoldsByIntent(...)?.hold)` (un hold declarado gana), y D.9:533 instruye explicitamente «los autores la declaran explicitamente en cada morfo». Busque neutralizadore…
### S-44 · sema.md transcribe `SEMA_VERBS` y le faltan dos verbos vivos: `commit.unselect` y `handle.zoom`
- **Dónde:** docs/architecture/sema.md:966-1013 (commit 968-992, handle 994) · src/uix/sema/verbs.ts:74,119 · docs/CANON.md:186-188 · docs/decisions/book-deviations.md:135-149
- **Defecto:** El bloque que sema.md publica como el vocabulario canónico de verbos es una copia del const que ha derivado: omite `unselect` (BOOK_CANON según A.11, aplicado en 6 componentes) y `zoom` (extensión firmada 2026-06-10).
- **Impacto:** Un autor que valide su morfo contra la tabla de sema.md concluirá que `unselect`/`zoom` no son canónicos y renombrará eventos correctos — o al revés, dudará de `morfo:vocabulary` cuando los acepte. Es exactamente la deriva que CANON.md:36-38 prohíbe («link this file — do not paste a copy. A second copy is a future drift»), cometida por el doc de arquitectura.
- **Arreglo propuesto (NO aplicado):** Borrar el bloque literal de sema.md y dejar el enlace a `verbs.ts` + `canon/vocabularies.md` (que sí se genera). Si se quiere conservar un extracto, que lo emita el generador.
- **Verificación adversarial:** Confirmado en el código real, sin contradicción. verbs.ts:74-75 declara 'select','unselect' y :119 handle:[...,'scroll','zoom']; sema.md:966-1013 imprime un bloque titulado `SEMA_VERBS = {` que omite ambos. Es copia del CONST, no de la lista base del libro: arrastra signal.inform, sustain.upload, los verbos contextuales y el propio comentario `// The delegate family (book ch. 29)` del const — extensiones ausentes de la lista-libro de CANON.md:191-198. Ningún guard lo cubre: I7-vocab (docs-check.ts:696-713) regenera y compara SOLO docs/canon/vocabularies.md; I8/I9/I10 miran imports, símbolos y tablas de props, nunca arrays enumerados contra un const; ningún test referencia architecture/sema.md. La cronología agrava: unselect entró el 2026-05-25 (f3641347d) y zoom el 2026-06-11 (7da7285d9), mientras el bloque de sema.md se redactó el 2026-07-02 (a6371ab58) — y `git log -S unselect -- docs/…
---
## Refutado
- **NavigationMenu: las dos reglas nuevas de `emerge-open`/`emerge-close` apuntan a `content` y el estampado va al `trigger`**
REFUTADO POR ESTADO ACTUAL, NO POR ERROR DE LECTURA. Verifiqué toda la cadena mecánica y el auditor la leyó bien: (1) runtime.svelte.ts:757 `opts.fallbackTarget ?? targetReg?.ref?.current` — fallbackTarget GANA sobre el target declarado en el morfo, pese al nombre; (2) resolver.ts:208-215 `safeMatches` = matches||closest sobre un selector compuesto `[data-navigation-menu-{part}][data-event="{name}"]`, y como `data-event` sólo cae en el elemento estampado, closest() nunca puede rescatar un desajuste de parte; (3) la composición es <Item><Trigger/><Content/></Item> (web/routes/uix/components/navigation-menu/+page.svelte:138-160), content es HERMANO del trigger, jamás ancestro. Al abrir esta se
---
## Lo que está bien y NO hay que tocar
### samples
LO QUE ESTÁ BIEN Y VERIFIQUÉ (no tocar):
1. **Los 5 .wav son audio válido.** Cabecera RIFF/WAVE con `fmt ` de 16 bytes, formato 1 (PCM), 44.100 Hz, 16 bit. El tamaño declarado en RIFF + 8 coincide EXACTO con el fichero en los cinco: ding 13.274 · error 57.836 · ping 7.100 · pop 8.864 · whoosh 15.920. Mono salvo error.wav (2 canales). Duraciones ≈0,075 s (ping) · 0,10 s (pop) · 0,15 s (ding) · 0,18 s (whoosh) · 0,33 s (error). Ninguno corrupto ni truncado.
2. **La ruta existe de punta a punta y SÍ se alcanza.** `sample()` (sounds.ts:57-66) → `SOUND_LIBRARY` → `sound(name)` → `resolveSoundDefinition` (:230-239) inyecta `sampleUrl` sobre el fallback → regla de cascada → `resolveSignature` conserva `sampleUrl` → `SoundChannel.handle` (chans/sound.ts:181) → `EngineSound.play` (:222-246) → `playSample` con fetch + `decodeAudioData` + caché por URL + `BufferSource`. Verificado ejecutando el motor con contexto y fetcher falsos: con respuesta decodificable se crean `buffersource,gain` y CERO oscillators; con 404 se crean `osc,osc,filter` (fallback). Hay fetch/decode real, no es fachada.
3. **Los packs con sample están cableados en la app real y sus eventos se emiten.** `web/routes/uix/+layout@.svelte:123-125` arranca con `sound: true` y la lista incluye `chatLogSema` (:129), `formSema` (:142) y `proofOfHumanSema` (:185). Los eventos se disparan de verdad desde soma: chat-log-provider.svelte.ts:190,197 (`signal-notify-new`), form-provider.svelte.ts:141 (`signal-warn-invalid`), proof-of-human-provider.svelte.ts:137,142 (`commit-confirm` / `commit-fail`). ping.wav y error.wav se alcanzan; sólo whoosh/pop/ding no.
4. **form.ts y chat-log.ts SÍ cumplen D.7.** Ambos usan sample sobre familia `signal` — la excepción documentada (book-deviations.md:390). Verificado en el morfo: `signal-warn-invalid` es `family: 'signal'` (morfo/components/form.ts:34-40) y `signal-notify-new` también (morfo/components/chat-log.ts:44-50). El comentario doctrinal de chat-log.ts:8-15 es exacto.
5. **El corte por ganancia ≤ 0 es correcto y NO daña los samples.** chans/sound.ts:176 `if (typeof sig.gain === 'number' && sig.gain * gainScale <= 0) return;`. Para un sample la ganancia es igualmente el multiplicador del envelope (engine-sound.ts:668 `envelope.gain.value = sig.gain * gainScale`), así que 0 es silencio por ambos caminos: el corte sólo ahorra el fetch. Compone bien con las otras dos vías de silencio: la dominancia con factor 0 ya retira `sound` de `activeChannels` (engine.ts:189-193) y la ataja el gate anterior (:159); `reduce` da `gainScale` 0.4, nunca 0 (:107,:168). El `typeof … === 'number'` es defensivo pero inocuo (`gain` es obligatorio en `SoundSignature`, channels.ts:17). Cubierto por test: chans/sound.test.ts:129-144.
6. **El renombrado de SOUND_TUNINGS del 2026-08-05 está COMPLETO.** 142 llamadas a `soundTuning('…')` en src/, con 14 claves distintas, y el catálogo tiene exactamente esas 14: cero huérfanas en el catálogo y cero referencias colgantes. Ningún superviviente `form.*` ni `tooltip.silent` ni `tabs.select.soft`. El guard nuevo (sounds-grammar.test.ts) verifica cabecera de familia, forma del tail y —lo mejor— que cada `.silent` reste EXACTAMENTE el `base.sound.gain` de su familia leyéndolo del mapa en vez de copiarlo (:79-106). Esa es la pieza que impide que un `.silent` deje de ser silencio si alguien retoca el mapa.
7. **Las URLs resuelven.** `svelte.config.js` no define `paths.base`, así que `/sounds/*.wav` cae en la raíz del origen, que es donde adapter-static publica `static/`.
8. **Estado de la suite.** `npx vitest run src/uix/sema src/arts/sound --project=server`: 20/21 ficheros en verde, 289/294 tests. El único fallo es `src/uix/sema/__scratch-ancestor.test.ts`, un fichero de sondeo sin trackear creado por otro agente en esta misma sesión (junto a `__audit-dim5.test.ts`, `__audit-scratch.spec.ts`, `__audit-scratch2.test.ts`) — no tiene relación con sonido y no lo he tocado. Aviso también de que `src/uix/sema/components/navigation-menu.ts` aparece modificado en el working tree por otro agente, no por mí.
NOTA DE MÉTODO: no he aplicado ningún cambio. Las tres verificaciones ejecutables (resolver con packs reales, preload con contexto suspendido, fallback con 404) corrieron desde ficheros del scratchpad importando el código del proyecto por ruta absoluta, sin escribir nada dentro de G:/dev/svelte/vicen.
### engine
VERIFICADO CORRECTO — no tocar:
1. **Sin fuga de nivel entre reproducciones.** `gainScale` viaja en el envelope de ESA reproducción y jamás toca master ni bus (engine-sound.ts:229-234 y 557). La profundidad de AM también se escala (líneas 574-577), así que atenuar mueve el volumen sin cambiar el timbre. El test sound.test.ts:146-169 fija la otra mitad (el master conserva su valor de política tras un play con `reduce`). El bug histórico A-5 (`master × 0.4` por reproducción) está realmente cerrado.
2. **Propiedad y disposal del motor compartido.** `SoundChannel.dispose()` sólo cierra el motor si lo creó (`ownsEngine`, sound.ts:184-187); las tres ramas de `EngineSemantic` (engine.ts:299-319) son ramas y no un spread, así que la unión discriminada de opciones no se rompe. `active-uix` standalone dispone lo que creó y en el orden correcto (`events` antes que `sound`, línea 630-635), y en attach sólo si `ownsSound` (línea 649). `ensureContext` está guardado por `disposed` (engine-sound.ts:438-448), así que ni `decode()` ni un `attach()` tardío pueden abrir un contexto en un motor muerto.
3. **El corte nuevo por ganancia ≤ 0 (sound.ts:170-176) es correcto y está bien colocado.** Se aplica sobre la ganancia RESUELTA (después del factor de `reduce`, antes de tocar el motor), así que: `commit.silent` / `emerge.silent` / `contact.silent` no sintetizan nada, y los deltas de intent que devuelven la ganancia por encima de 0 (threat +0.1, fulfill +0.05) siguen sonando — comprobado contra el orden real de la cascada (intent = capa 2, packs = capa 5a, resolver.ts:148-180). De propina mata las ganancias NEGATIVAS, que antes producían un envelope invertido audible. La rama `off` sigue delante del corte, así que no hay cambio de orden de política.
4. **El renombrado de SOUND_TUNINGS está limpio.** Cero residuos de `form.*` en `src/uix/sema/`; todos los `soundTuning(...)` resuelven a claves existentes (garantizado por tipos: `satisfies Record<string, SoundOverride>` + `SoundTuningName`). Las suites de sonido pasan enteras: `src/arts/sound` + `src/uix/sema/chans` + `sounds-grammar.test.ts` → 9 ficheros, 84 tests en verde.
5. **Sin dobles emisiones.** `emit` despacha cada canal exactamente una vez (engine.ts:435-442) y nadie fuera del canal llama a `engine.play` — verificado en `soma`, `blocks` y `packs`: los únicos consumidores del art son `uix.sound.media(el)` (media-player) y `decode`/`peaks` (waveform). El canal es fire-and-forget, así que el earcon nunca retrasa el commit estructural del caller.
6. **Ciudadanía media (media.ts) sólida.** Re-registrar el mismo elemento retira la registración previa en vez de apilar listeners (líneas 130-131); `destroy()` devuelve el volumen del DUEÑO al elemento, re-deriva el ducking y libera el hold de UI (426-455); el ducking es estado derivado del conjunto que suena, no acción del que arranca (289-302, defecto AU-1 cerrado); `dispose()` del motor limpia el manager FUERA del guard de idempotencia (316-322), así que un `media()` post-dispose no queda colgado. El `attach()` es explícitamente irreversible y documentado como tal.
7. **La política sobrevive a la ausencia de contexto.** `setGain`/`setMuted` antes del primer sonido se guardan y se aplican al materializar el grafo (`createContext` llama a `applyBus` por bus, líneas 463-468; el master toma `masterGainValue` en 460-461). Los ducks refcontados usan el factor MÁS FUERTE en vez de multiplicar (381-387), que es la semántica correcta.
8. **Fallback de sample → síntesis.** Un fetch/decode fallido devuelve `false` y cae a síntesis con la firma de respaldo (643-663 + 238-242): la señal perceptiva nunca se pierde en silencio.
NOTA DE MÉTODO: para no reportar nada "que parece", ejecuté cuatro comprobaciones en dos ficheros de test temporales (`src/arts/sound/__audit-scratch.test.ts` y `src/uix/sema/__audit-scratch2.test.ts`), ambos **borrados** tras la ejecución. No se modificó ningún fichero del proyecto. (Los cambios que aparecen en `git status` sobre `src/uix/blocks/**` y `docs/` son de otro hilo de trabajo, no míos.)
### cascade
**Lo que verifiqué y está BIEN — no tocar:**
1. **El orden de capas es exactamente el documentado.** `engine.ts:274` `this.cascade = [...packCascade, ...(opts.overrides?.cascade ?? [])]` + `resolver.ts:200-203` ordenación ascendente ESTABLE con desempate `a.index - b.index` y aplicación en ese orden ⇒ la regla de app gana al pack en empate de especificidad. La cabecera de `resolver.ts:5-28` y la de `engine.ts:14-22` describen el mecanismo real.
2. **La semántica add-vs-replace está implementada como se documenta.** Capa 2 (intent) usa `applyChannelDeltas(..., 'add')` (`resolver.ts:287-304`); capas 3/5a/5b usan `applyOverride` → `applyLeaf(current, delta, 'replace')` (`resolver.ts:271-275`); capa 4 usa `'replace'` en `applyPathOverride` (`resolver.ts:407`). `{op:'add'|'multiply'|'replace'}` se resuelven en `applyLeaf` (`resolver.ts:350-357`) y ganan siempre al modo por defecto.
3. **Las tuningsg `.silent` son doctrinalmente correctas Y están guardadas.** `commit.silent` (`sounds.ts:202`), `emerge.silent` (:211) y `contact.silent` (:220) usan `{op:'add', value:-base}` y restan EXACTAMENTE el gain de su familia, de modo que las deltas de intent (`threat` +0.1, `fulfill` +0.05) siguen aflorando. `sounds-grammar.test.ts:79-106` lo comprueba leyendo el gain base del mapa, sin copiarlo. Es el patrón que el resto del catálogo debería seguir (ver hallazgo 2).
4. **El renombrado de SOUND_TUNINGS de hoy está COMPLETO en código.** Cero referencias a `form.*`, `tooltip.silent` o `tabs.select.soft` en `src/`; las 14 claves usadas por los packs existen todas en el catálogo. La gramática `{familia}.{cola}` la cierra `sounds-grammar.test.ts:60-77` contra `SEMA_FAMILIES`, y la lista de excepciones está vacía y auditada (:108-111).
5. **El corte de ganancia nuevo (`chans/sound.ts:176`) está bien colocado y bien razonado.** `if (typeof sig.gain === 'number' && sig.gain * gainScale <= 0) return;` va DESPUÉS de los guards de `activeChannels` (:159) y de `sig` (:160-161) y ANTES de `engine.play` (:181); compara la ganancia RESUELTA (no la tuning) y multiplica por `gainScale` antes de comparar, así que `preferences.sound === 'reduce'` (factor 0.4) no puede cambiar el signo. No toca `prepare()`/`prime()` (:150-156), luego el desbloqueo de autoplay sigue ocurriendo dentro del gesto. Y no colisiona con las modulaciones post-resolución: `attenuateNonVisual` (`engine.ts:187-199`) escala por un factor positivo con suelo `FREQ_FLOOR` 0.25, así que nunca convierte un gain positivo en 0 ni un 0 en positivo; el caso mute usa `factor <= 0`, que elimina el canal de `activeChannels` en vez de escalar.
6. **El scoping por ancestro del media-player es correcto y NO depende de `closest()`.** `inPlayer` / `inVolume` (`media-player.ts:58,69-70`) emiten un combinador de descendencia (`[data-media-player] [data-slider][...]`), que `target.matches()` resuelve nativamente. Su especificidad (3 attrs = 30) supera limpiamente la del pack del slider (2 attrs = 20), así que el silencio del reproductor gana de forma determinista, no por orden de array. Ambas mitades salen del builder tipado, luego un rename rompe en compilación.
7. **El slider reactiva bien el canal de sonido para la familia `handle`.** `handle` declara `activeChannels: ['haptic']` (`sema-map.ts:201`); `slider.ts:24-31` añade `channels: ['sound','haptic']` explícitamente, y las primitivas del `handle-drag` llegan por capa 3 desde `slider-provider.svelte.ts:250` (`resolveSliderDragSound`). La regla sin `sound:` es correcta por diseño, no un olvido.
8. **`safeMatches` no puede lanzar.** El `try/catch` (`resolver.ts:210-214`) más el escapado propio e isomorfo de valores en `selectors.ts:50-52` y la validación de nombres de atributo (:55-63) cierran la clase de bug MOR-1.
9. **Suite en verde**: `npx vitest run src/uix/sema --project=server` ⇒ 18 ficheros / 256 tests pasando. Todo lo que reporto abajo es silencioso hoy.
### types
VERIFICADO Y CORRECTO — no tocar:
1. **El declaration merging de `SemaChannelSignatures` FUNCIONA por la ruta documentada.** Probado con tsc: con `declare module '$uix/sema' { interface SemaChannelSignatures { voice: VoiceSignature } }`, la aserción `'voice' extends keyof SemaChannelSignatures ? true : false` da `true` (y con `false` el compilador protesta, o sea el test discrimina), y `SemaSignatureOverride` gana el slice `voice` tipado. Es notable porque la interfaz vive en `channels.ts` y sólo se RE-exporta por el barrel — el patrón que en otros sitios del repo (`packs/ambient/effects/*.ts`) se hace contra el módulo declarante. Aquí funciona: `channels.ts:50`, `docs/architecture/sema.md:61` y `book-deviations.md:474` dicen la verdad.
2. **La rama estructurada de `SemaEvent` impone el intent de verdad.** `const x: SemaEvent = { family: 'commit' }` es error de compilación (el `@ts-expect-error` se consume). La derivación `IntentRequiredFamily` desde `SEMA_FAMILY_POLICY` es genuina para 'required'/'optional' — el único agujero es el tercer valor y la forma etiqueta (hallazgos aparte).
3. **`SoundChannelOptions` SÍ es una unión discriminada real.** Ambas formas declaran `engine` (`engine: EngineSound` vs `engine?: undefined`, sound.ts:57-59 / 66-71), así que el narrowing de `engine.ts:299` funciona y `masterGain` junto a un `engine` inyectado es error de compilación. El probe anti-contrabando para llamantes JS (`sound.ts:119-128`) dispara exactamente una vez por construcción directa — verificado en runtime.
4. **El corte de ganancia nuevo hace lo que dice.** Verificado en runtime: `gain: 0` no llama a `engine.play`; `gain: 0.05` sí. Y la aritmética que el docblock de `commit.silent` promete es exacta: con la familia commit (base 0.3) el resolver deja neutral en **0**, `fulfill` en **0.05** y `threat` en **0.1** — los deltas de intent siguen aflorando, que era el objetivo.
5. **La ley de nombres de `SOUND_TUNINGS` tiene un guard que no copia el canon.** `sounds-grammar.test.ts` lee las familias de `SEMA_FAMILIES` y la base de ganancia de `SEMA_MAP.families[f].base.sound.gain` en vez de duplicarlas, y comprueba que cada `.silent` reste EXACTAMENTE su base (`commit.silent` −0.3, `emerge.silent` −0.2, `contact.silent` −0.25: los tres cuadran). Deja el tail deliberadamente abierto y lo argumenta. La lista de excepciones está vacía y hay un test que la mantiene honesta.
6. **El renombrado está completo en código.** Ni una llamada a `soundTuning(...)` con clave muerta en los ~50 packs de `src/uix/sema/components/*.ts` (barrido completo de los aciertos). Las menciones a `form.*` que quedan en `book-deviations.md:336-345` son narración histórica correcta.
7. **Cero `any` en el código de producción de sema.** Los únicos casts son `as unknown as Record<string, unknown>` en el caminador genérico del resolver (resolver.ts:264-297, 385, 412) — la forma estándar para acceso dinámico por clave — y el probe deliberado de sound.ts:122. Ningún `@ts-ignore`.
8. **Todas las opciones de `EngineSemanticOptions` se leen** (visual, sound, soundEngine, haptic, announce, logger, components, overrides, projector, dom, timers, preferences, frequencyMemory, dominance) — ninguna es declarativa muerta, salvo por las rutas de descarte de los hallazgos 1 y 2.
9. **`applyMapOverrides` no muta el mapa canónico** (clona por camino, resolver.ts:376-409) y `resolveSignature` es puro. El fallback de id muerto en `applyDominance` ya se retiró con su justificación explícita (engine.ts:564-570) y la cancelación del cap de expresión en `visual.ts:120-124` cierra el timer fantasma.
10. **La separación de propiedad del motor de audio es coherente**: `ownsEngine` decide el `dispose` (sound.ts:185-187) y la raíz de composición inyecta siempre logger, dom, soundEngine y timers con `??` (active-uix.svelte.ts:145-155, define-engine-semantic.ts:43-54), así que el silencio del engine sin logger es de verdad sólo la ruta de tests.
### packs
MEDICIÓN BASE (volcado en runtime, no grep: importé los 71 packs y los 166 morfos con `import.meta.glob` y parseé los 212 selectores compilados contra las partes y eventos reales de cada morfo). HEAD `d05ecc1aa`. ⚠️ El árbol está siendo editado en paralelo por otras sesiones (navigation-menu morfo+sema+soma y form están modificados sin commitear; a las 23:21 el barrel llegó a lanzar en import por un guardado a medias) — todo lo que reporto está verificado contra los bytes de 23:22.
LO QUE ESTÁ BIEN Y HE COMPROBADO:
1) El builder tipado cumple su promesa. Los 212 selectores usan `semaSelector(morfo, parte, matchers)`; NO hay ni un solo selector escrito a mano contra atributos del morfo en los 71 packs. Un kebab o un nombre de evento inexistente revienta en el import (lo vi ocurrir de verdad: `semaSelector: morfo "NavigationMenu" has no event named "contact-activate"` durante el guardado a medias de otra sesión). Por eso NO existe la clase «parte que no existe / kebab equivocado»: el tipo la mata antes.
2) Falsos positivos del cruce estático que resultaron estar VIVOS por `fallbackTarget` (verificados uno a uno leyendo el proveedor):
· calendar — 6 reglas de `shift-navigate` sobre provider/prev-button/next-button/month-select/year-select/day: `CalendarPaginationButtonProvider.onclick` pasa `e.currentTarget` (calendar-provider.svelte.ts:670-672), los `<select>` pasan `e.currentTarget` (:778-781) y el teclado pasa la celda (:518). Escopetazo deliberado, todo alcanzable.
· pagination — las 6 reglas: los 4 botones pasan `e.currentTarget` (:240,:295,:350,:405), el item también (:472) y el provider queda para las llamadas programáticas (:166).
· rating-group (item vía `handleItemClick` :240-242) · toolbar group-item (:354) · tree-view branch/item (:180 con `fromEl`) · tree-grid (resuelve la fila por `querySelector` :168-174,:218-226) · toggle-group (`itemEl ?? this.resolveItemEl(value)` :140) · menubar (:145) · menu-dial y onion-menu (desde eidos con `asTarget(...)`) · path-trace · number-field/css-field scrubber · drag-drop · file-upload · month-grid · context-menu · dropdown-menu · listbox/select/combobox/tag-group (vía `computeListSelection`, soma/layers/list-selection.ts:86-126).
3) Partes repetidas: NO hay contaminación entre instancias. Accordion item, nav-tree group y chat-message reaction crean un runtime POR INSTANCIA (`this.soma.runtime(morfo, …)` en el proveedor hijo: accordion-provider:190-208, nav-tree-provider:304-331, chat-message-provider:391-396), así que `registrations.set(part, reg)` (runtime.svelte.ts:498) no se pisa y el sello cae en el elemento correcto aunque no haya `fallbackTarget`.
4) media-player (el pack más grande, 12 reglas) está bien construido, incluidas las cruzadas de morfo: `[data-media-player] [data-button][data-event="contact-activate"]` y las 6 de slider. Verificado que el wrapper `data-media-player-volume-slider` es un `<div>` ANCESTRO real del `<Slider.Provider>` (soma/components/media-player/components/media-player-volume-slider.svelte:38-56), que los 3 eventos de slider apuntan a `provider` y estampan ahí (slider-provider:172,:286,:320) y que la especificidad hace ganar a las reglas del player (30) sobre el pack de slider (20).
5) Solapes de reglas: 0 selectores duplicados dentro de un mismo pack. Los solapes de dialog y popover COMPONEN correctamente — `[data-…-content][data-event="close"][data-last-action="dismissed-outside"]` (gain) y `[…][data-event-family="emerge"][data-event="close"]` (contour+pitch) tocan claves distintas, y en los empates de especificidad gana el índice posterior, que es el comportamiento documentado.
6) Atributos no declarados en el morfo que SÍ existen en el DOM (falsos positivos míos, confirmados): `data-size` y `data-sheet` los pone eidos en el mismo nodo Content (dialog-content.svelte:89-91, documentados como eidos-only) y `role` lo pone el proveedor por variante (`role: this.provider.opts.variant.current`, dialog-provider:449). Y `data-last-action="dismissed-outside"` existe literalmente en los 4 proveedores de overlay (dialog:52, popover:68, drawer:66, float-panel:55).
7) Renombrado de SOUND_TUNINGS: en los packs NO queda ni una clave muerta — las 139 llamadas `soundTuning('…')` resuelven contra el catálogo vivo (si no, `keyof typeof SOUND_TUNINGS` no compilaría). Los silenciadores restan exactamente su base (`commit.silent` -0.3, `emerge.silent` -0.2, `contact.silent` -0.25) y `sounds-grammar.test.ts` lo guarda leyendo el gain del mapa, no copiándolo.
8) El corte de ganancia en `SoundChannel.handle` (chans/sound.ts:176) es correcto: mira el gain RESUELTO (`sig.gain * gainScale <= 0`), no el tuning, así que `threat` (+0.1) y `fulfill` (+0.05) siguen sonando sobre un `.silent` y sólo se ahorra la síntesis cuando el resultado es 0; `prepare()` sigue llamando a `this.engine.prime()` (:150-155), o sea que no se pierde el desbloqueo por gesto; y el factor de reducción (0.4) no puede cambiar el signo. Suite de sema en verde: 201 tests, 18 ficheros.
9) Familias imposibles: ninguna. Las reglas por familia de dialog/popover (`commit`, `signal`) son alcanzables porque `close` es polimórfico con `allowedFamilies: ['emerge','commit','signal']` y los `DISMISS_CAUSES` concretan save→commit/fulfill y fail→signal/threat. Las 5 reglas por intent de toast son alcanzables porque `announce` liga `intent: { fromProp: 'intent', default: 'neutral', supported: INTENTS }` (morfo/components/toast.ts:39-43).
DATO DE COBERTURA (no defecto, contexto para el orquestador): 91 morfos declaran eventos de sema y 71 tienen pack ⇒ 20 emiten con la base de familia cruda, sin firma propia: button (contact-activate — el evento más frecuente del sistema), card, command, grid-list, pin-input, carousel, field, field-langs, feed, announce, aura, chat-typing, metrics, palabras (12 eventos), virtual-list, virtual-grid, date-picker, date-range-picker, time-picker, time-range-picker.
RAÍCES DE COMPOSICIÓN (pregunta 3): sólo dos registran el conjunto completo — `web/routes/uix/+layout@.svelte` (71 packs, a mano por ruta profunda) y la sección blocks (derivada del barrel, y por eso pierde 3). Las demás llevan listas cortas escritas a mano: demos/cristal 6 packs, temas/grafito 15, alpha 8, temas/sema 2. No es defecto por sí mismo (los packs son opt-in y cada demo puede usar sólo esos componentes), pero es la superficie exacta por la que ya se coló el fallo del media-player en julio.
### morfo-sema
CENSO (reproducido dos veces, idéntico): 166 morfos en `src/uix/morfo/components/` (175 ficheros − 9 `*.test.ts`), 91 con eventos, **248 eventos**. Reparto por familia: commit 124 · emerge 41 · handle 40 · shift 19 · signal 12 · delegate 5 · contact 4 · sustain 3. 71 packs en `src/uix/sema/components/` con 212 reglas de cascada. INCUMPLEN: 0 eventos en los ejes 1 y 2; 1 de 5 eventos polimórficos; 17 de 212 reglas de cascada (5 con háptico muerto + 2 que no pueden casar + 10 con tuning de familia ajena) repartidas en 13 packs.
**Eje 1 — family + verb contra los conjuntos cerrados: LIMPIO, 248/248.** Las 248 `semantic.family` están en `SEMA_FAMILIES` (`src/uix/sema/event.ts:14-31`); los 248 eventos declaran `verb` (ninguno lo omite, aunque el tipo lo permite); y los 248 verbos están en `SEMA_VERBS[family]` (`src/uix/sema/verbs.ts:63-160`). `npm run morfo:vocabulary` sale 0 con `EVENT_NAME_ALLOWLIST` VACÍO (`scripts/morfo-vocabulary-check.ts:75-78`) — sin deuda encubierta. El único WARN de forma, `cropper.commit-crop`, es legítimo: declara `verb: 'apply'` (canónico, `src/uix/morfo/components/cropper.ts:68`) y `crop` es la etiqueta de dominio que el libro autoriza (cap. 8 §1). No lo toques.
**Eje 2 — política de intent: LIMPIO y con DOBLE puerta.** Los 136 eventos de `commit` (124) + `signal` (12) llevan intent, los 136. Y lo detectan dos cosas independientes, ambas verificadas: (a) el tipo — la unión discriminada `MorfoEventSemantic` (`src/uix/morfo/types.ts:416-439`) derivada de `SEMA_FAMILY_POLICY` (`src/uix/sema/types.ts:69-84`), que es la fuente y no una copia; (b) el RUNTIME — probé `validateMorfo` con un `commit` sin intent y con un `signal` sin intent y ambos lanzan, porque `eventSemanticSchema` (`src/uix/morfo/schema.ts:260-281`) es una unión donde la rama laxa exige `semaIntentOptionalFamilySchema` y commit/signal no están en ella. También lanza con familia inventada y con `allowedFamilies` inválido. Esto está mejor cerrado que la media del repo.
**Eje 3 — `expression` + `scope: ['sema']`: coherente con la realidad.** 71 `pack` / 16 `family-default` / 13 `delegated` / 66 sin campo (todos sin eventos). Los 71 que dicen tener pack lo tienen: 71 ficheros, correspondencia 1:1 por kebab, **0 huérfanos en ambas direcciones** y 0 packs sobre morfos sin eventos. 0 morfos con eventos sin `'sema'` en `scope[]`. 0 morfos con `scope:sema` + eventos sin pack ni `expression`. `waveform` (`expression: 'delegated'`, `scope: ['soma','sema','eidos']`, 0 eventos) parece anómalo y NO lo es: el morfo lo documenta explícitamente en su cabecera (`src/uix/morfo/components/waveform.ts:11`) — es delegación declarada, no olvido.
**Eje 4 — polimorfismo: 4 de 5 correctos.** `dialog.close`, `drawer.close`, `popover.close` y `float-panel.close` declaran `allowedFamilies: ['emerge','commit','signal']` y sus cuatro proveedores reenvían `semantic: cause.semantic` desde una tabla `DISMISS_CAUSES` cuyas cinco causas caen TODAS dentro del allowlist (`save`→commit/fulfill, `cancel`/`dismiss`/`dismiss-outside`→emerge, `fail`→signal/threat). Ninguna puede disparar `SomaRuntimePolymorphicError`. El runtime valida bien y trata la familia propia como implícitamente permitida (`src/uix/soma/runtime.svelte.ts:711-719`), con 4 tests que lo pinchan (`runtime.svelte.test.ts:868-975`).
**Renombrado de SOUND_TUNINGS (hoy): LIMPIO, el barrido fue completo.** 14 claves en el catálogo, las 14 consumidas por algún pack, ninguna huérfana y ninguna referencia a clave inexistente. **Cero apariciones vivas** de `form.*`, `tooltip.silent` o `tabs.select.soft` en código o en docs vigentes — las que quedan están en `sounds-grammar.test.ts:9-24` (narración deliberada del drift), `docs/decisions/book-deviations.md:336-346` (registro del renombrado) y ficheros de crónica que `isChronicle()` exime por diseño (`scripts/docs-check.ts:117-138`). `sounds-grammar.test.ts` pasa sus 5 tests con `EXCEPTIONS` vacío, y su test de silenciadores es genuino: lee la base desde `SEMA_MAP` en vez de copiarla (`baseGain()`, :51-53). Los tres `.silent` (`commit`, `emerge`, `contact`) restan exactamente su propia base y ninguno se aplica sobre familia ajena — el modo de fallo aritmético que sí existiría, no está.
**Corte `gain <= 0` en `SoundChannel.handle` (hoy): CORRECTO y probado.** Va después de la puerta `activeChannels` y de la reducción por preferencia, sobre la ganancia RESUELTA (no sobre el tuning), así que las deltas de intent que suben la ganancia por encima de 0 —`threat` +0.1, `fulfill` +0.05— siguen sonando: es exactamente lo que el comentario de :170-176 promete. No toca el canal visual, así que el estampado `data-event-*` y las recetas de eidos siguen corriendo con el evento mudo. Tiene test propio: `src/uix/sema/chans/sound.test.ts:129-144` («never synthesizes a signature resolved to zero gain»). 52 tests verdes en `sounds-grammar` + `chans/sound` + `event` + `verbs`.
**Selector tipado: adoptado al 100%.** Las 212 reglas construyen su `selector` con `semaSelector(morfo, part, matchers)`; no queda ni una cadena a mano apuntando a atributos del morfo. Por eso los 212 selectores nombran parts y nombres de evento que existen de verdad — lo comprobé cruzando cada selector contra el morfo que resuelve su marcador más a la derecha: 0 parts inventados, 0 nombres de evento inexistentes, 0 familias declaradas en un selector que el morfo no pueda emitir. El escape que rompe esta garantía no es el selector sino el `fallbackTarget` del proveedor, que el tipo no ve (hallazgo 2).
### channels
VERIFICADO Y CORRECTO — no tocar:
· ANNOUNCE — prioridad. `priorityForIntent` (announce.ts:55-57, `intent === 'threat' || intent === 'loss' ? 'assertive' : 'polite'`) coincide literal con la doctrina (docs/architecture/sema.md §«Announce channel», SEM-1) y con la implementación gemela del runtime de soma (runtime.svelte.ts, el bloque `runA11y`: mismo ternario, sin la conjunción `family === 'signal'` que causó la divergencia histórica). Ambas pinchadas por tests (announce.test.ts:37-49). El canal es fire-and-forget, absorbe cualquier throw (announce.ts:86-88, test :71-83), no-opea sin `message` y `dispose()` retira sus regiones. Que ignore `activeChannels` es DELIBERADO y está documentado (la mute por dominancia respeta announce a propósito, engine.ts:186 + sema.md §Dominance; y el ejemplo canónico `channels: ['haptic']` de sema.md existe precisamente porque la live region lleva el mensaje). El silencio total sí lo respeta: `signal.channels: []` corta antes del dispatch (engine.ts:401-403).
· VISUAL — el hold es SUELO, no tijera. `awaitExpression` (visual.ts:98-132) implementa bien la doctrina cap. 4 §13 / cap. 12 §6, y el arreglo SEM-3 está en su sitio: `cap.cancel()` en `settle` (visual.ts:117-124) evita el timer fantasma que quedaba hasta 1500 ms. `MAX_EXPRESSION_WAIT_MS = 1500` como CONSTANTE (no derivada del hold) está bien razonada y —contra lo que suele pasar con estos comentarios— el lint que promete EXISTE de verdad: eidos/active-eidos-config.test.ts:1108-1120 importa la constante y falla si una firma transitoria la excede. La cadena de resolución `effective.hold → resolveHoldsByIntent → defaultHold` está cubierta por visual.test.ts:62-286, incluida la granularidad por intent (commit+fulfill → settled 400). Todos los diferidos van por `semaDelay`/`uix.timers`, nunca `setTimeout` crudo (timers.ts:34-50).
· HAPTIC — lo que sí funciona. Gate por `activeChannels` (:114), gate por slice ausente (:116), honra `prefers-reduced-motion` vía el puerto estructural inyectado en vez de un `matchMedia` global (:121, `HapticChannelDom`), `preferences.haptic: 'off'` silencia y `'reduce'` atenúa CORRECTAMENTE incluso los patrones explícitos —`reduceHapticPattern` (:83-90) acorta los pulsos (índices pares) y respeta las pausas (impares), preservando el ritmo—, absorbe el throw de `vibrate` (:135-137) y el `delay` corre por el scheduler gestionado. El suelo `Math.max(0.3, …)` de `kindToPattern` (:155) está justificado (vibrate(0) es un cancel silencioso) y el engine lo sabe: por eso el mute total DROPEA el canal en vez de escalar a cero (engine.ts:186-193).
· STAMP — frontera de propiedad. Sema escribe `data-event-*` y NADA más: ningún atributo de estado (`data-state`, `data-color`…) — pinchado en engine.test.ts:268. La aparente discrepancia entre el docstring («family/intent when present») y el código (siempre presentes en el record) NO es un defecto: `undefined → removeAttribute` (libs/dom/apply.ts:35-38), así que el efecto observable es idéntico y además idempotente (dom.test.ts:49-54). Las escrituras van por `DomApplier` inyectado, no por `setAttribute` directo (dom.test.ts:107+).
· EL CORTE DE GANANCIA DE HOY (sound.ts:176). Correcto y bien situado. `gainScale` sólo vale 1 o 0.4 (:107, :168), luego la condición se reduce a `gain <= 0` y no puede tragarse un `reduce`. La cancelación de los `.silent` es EXACTA en IEEE754 (0.3−0.3, 0.2−0.2, 0.25−0.25 dan 0 exacto), y como los deltas de intent se aplican ANTES que la cascade, threat (+0.1) y fulfill (+0.05) sobreviven al corte tal y como promete el comentario: 0.4−0.3 y 0.35−0.3 son > 0. Pinchado en sound.test.ts:129-144. La reducción sigue siendo por señal (`gainScale` al engine) y no toca el master del documento (sound.test.ts:146-169).
· EL RENAME DE SOUND_TUNINGS (2026-08-05). Sin residuos: no queda ninguna clave `form.*` en `src/` (el único match es la narración del propio guard). `sounds-grammar.test.ts` es un guard honesto: lee las ganancias base DE `SEMA_MAP` en vez de copiarlas (`baseGain`, :51-53), exige que todo `.silent` reste EXACTAMENTE la base de su familia (:79-106), declara explícitamente qué NO comprueba y por qué, y mantiene la lista de excepciones vacía y auto-validada (:108-111). Comprobé por muestreo que la cabeza de familia coincide con la familia del evento al que se aplica (context-menu: `emerge.*` sobre open/close, `commit.subtle` sobre `commit-select`; drag-drop: `commit.subtle` sobre `commit-cancel`; media-player: `commit.silent` sobre `commit-set`) — muestreo, no censo exhaustivo.
· ENGINE (contexto de los canales). Las dos modulaciones perceptivas tocan SÓLO los canales no visuales: ni el hold ni announce se alteran (engine.ts:186-199, 421-427), tal y como manda la doctrina. La memoria de frecuencia y la dominancia cancelan sus timers en `dispose()` (:609-613) y `applyDominance` ya no acuña ids paralelos (:564-570, arreglo SEM-3).
### doctrine
Verificado como CORRECTO y coherente entre doctrina y código — no tocar:
1. **Los conteos del canon.** 8 familias (`SEMA_FAMILIES`, event.ts:28 sobre valenced+transitional de types.ts:15-17), 6 intents (`INTENTS`, src/uix/intent.ts:8) y la policy de dos ejes (`SEMA_FAMILY_POLICY`, types.ts:69-84) coinciden literalmente con CANON.md §2/§3/§4, con sema.md §«per-family intent policy» (:906-915) y con la tabla propuesta en D.3 (:407-416), incluido «`'forbidden'` está reservado, ninguna familia lo usa».
2. **La cascada numerada 1·2·3·4·5a·5b** es la MISMA en los tres sitios y la implementación la respeta: sema.md:171-185, engine.ts:13-23, resolver.ts:5-24. Capa 4 horneada en el mapa en construcción (`applyMapOverrides`, engine.ts:260), packs antes que cascade de app (engine.ts:264-274), orden ascendente de especificidad con desempate por orden de declaración (resolver.ts:191-206). La convención de números (ADD en capa 2 vía `applyChannelDeltas`, REPLACE en 3/4/5 vía `applyOverride`) está implementada tal cual (resolver.ts:250-304).
3. **El eje de holds (D.12) es real de punta a punta.** `SEMA_MAP` ya no tiene `hold` (sema-map.ts:33-45), `SEMA_HOLDS_BY_INTENT` es la única fuente (resolver.ts:137-139, visual.ts:149-156), el peldaño `settled: 400` existe (durations.ts:38), `MAX_EXPRESSION_WAIT_MS = 1500` con espera real de la expresión (visual.ts:58,108-132) y la «triple guarda» incluye el lint de diseño prometido: `active-eidos-config.test.ts:1108-1120` falla si una firma transitoria supera el tope.
4. **El renombrado de hoy quedó completo en el código.** Cero claves `form.*` / `tooltip.silent` / `tabs.select.soft` vivas en `src/**` (sólo en prosa histórica y en la cabecera del propio guard). El guard nuevo (`sounds-grammar.test.ts`) no copia canon: lee las familias de `event.ts` y las ganancias base de `sema-map.ts`, y verifica la invariante del silenciador aritméticamente (`{op:'add', value: -base}`). `docs/canon/vocabularies.md` está regenerado y cuadra: «Sound tunings (14)» con la tabla por familia == las 14 claves de `SOUND_TUNINGS`.
5. **La reversión de los toggles (D.5) es coherente en todo el catálogo.** switch.ts:36, toggle.ts:28 y toggle-group.ts:28 usan `commit.medium` con el háptico ligero intacto (`tap`, 0.3/12/0), y el silencio sobrevive EXACTAMENTE donde D.5 dice: tooltip (`emerge.silent`, tooltip.ts:43) y media-player (`commit.silent`/`contact.silent`, media-player.ts:80-126). Ningún tercer componente heredó el silencio por descuido.
6. **El corte por ganancia está bien planteado y fijado por test.** Mira la ganancia RESUELTA y después del factor de reducción (`sig.gain * gainScale <= 0`, sound.ts:176), así que `threat` (+0.1) y `fulfill` (+0.05) siguen sonando sobre un tuning silenciador — que es la razón por la que la invariante del silenciador existe. Test propio: `never synthesizes a signature resolved to zero gain` (chans/sound.test.ts:129-144).
7. **La excepción de D.7 para `signal` se respeta donde aplica**: chat-log usa `notification.ping` sobre `signal-notify-new` (familia `signal`) y lo justifica citando D.7 en su cabecera; form usa `alert.error` sobre `signal-warn-invalid` (familia `signal`). Ninguno de los dos es el hallazgo — son el uso correcto.
8. **La tabla de activación por familia de D.8 (:444-453) cuadra exactamente con `SEMA_MAP`**: contact/commit/signal → sound+haptic; emerge/shift → sound; handle → haptic; sustain/delegate → ninguno.
9. **El mandato del selector tipado se cumple al 100 %**: cero selectores morfo-dirigidos escritos a mano en los 73 packs (`grep "selector: '"` sin resultados); todos pasan por `semaSelector`, incluidos los descendientes del media-player, donde sólo el combinador es literal (media-player.ts:58,69-70).
10. **El guard de participación S11d que sema.md describe (:360-362) existe y hace lo prometido**: FAIL cuando hay pack y `expression !== 'pack'`, WARN cuando hay pack sin `expression` (scripts/morfo-vocabulary-check.ts:225-253,444-464).
11. **`SEMA_MIGRATION` coincide literalmente** con lo que sema.md §Migration table describe (migration.ts:61-66: motion→state/focus/text · color→shape/icon/text · sound→presence/text/liveRegion · haptic→motion/shape/state, más los refinamientos por familia de :73-91).
12. **Los ejemplos de `validateEventName` de sema.md:1046-1060 reproducen la implementación** (verbs.ts:252-298), incluido el match por prefijo más largo que salva `enter-mode`.
13. **`docs/process/handoffs-claude-md.md:517` sigue nombrando `form.commit.soft` / `form.toggle.silent` / `tabs.select.soft` y «38 sema packs»** — NO es un hallazgo: el fichero declara `status: historical` y «Chronicle — recorded history, not current truth». Correctamente excluido del corpus vivo.
Nota de estado del árbol: `npx vitest run src/uix/sema` da 256 tests en verde; los 5 rojos provienen de dos ficheros NO versionados (`src/uix/sema/__scratch-ancestor.test.ts`, `__audit-dim5.test.ts`) creados por sesiones de auditoría concurrentes, y `src/uix/sema/components/navigation-menu.ts` aparece modificado en el working tree por otra sesión — nada de eso se ha contado como defecto de la capa, y mis citas de navigation-menu se limitan a lo que hay en HEAD.

Powered by TurnKey Linux.