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

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 hasta clearTarget (form-provider.svelte.ts:129, siguiente submit). Medido con jsdom + packs reales + resolver real: un contact-activate de un 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
    <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
    , no , 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.
  1. 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 unSemaActionEventválido aunquecommittengaintentRequirement: '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 validateSemaEventparsear la etiqueta y aplicarle la misma comprobación deintentRequirement` 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 (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.