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:
- Un peligro de emparejamiento (
closest()) que convierte toda regla de pack en regla de ancestro — con una fuga audible reproducible. - La capa 5 pisando la capa 2: los tunings fijan
gaincomo número desnudo yapplyOverridelos aplica en modoreplace, borrando el perfil evaluativo del intent. Es justo lo que la doctrina prohíbe. - 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:
safeMatchesacepta el match por ANCESTRO, y como losdata-event-*viven toda la ventana de hold (o indefinidamente conuntilFix/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 matcherancestordesemaSelector(morfo/selectors.ts:116-121,184) emiten un combinador de descendencia quematches()resuelve nativamente. La única regla que alguna vez intentó apoyarse enclosest()(navigation-menu, analizada endocs/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 conancestor: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 acommit-confirmycommit-fail(familiacommit, nosignal), y como el resolver aplica los overrides de cascada en modoreplacesobre 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:
riskythreatsuenan 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. Unfulfillpierde 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 paracommit-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ólosampleUrl. 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
preloadSamplesen un motor falso; ni el fetch, ni el decode, ni la caché por URL, ni el fallback a síntesis, nipreloadtienen 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
playSampleno rompe nada hoy. - Arreglo propuesto (NO aplicado): Tres casos con
audioContextFactory+fetcherfalsos, que es como ya se testea el resto del motor: (1) fetch OK → se creacreateBufferSourcey NO oscillators; (2){ok:false}→ se crean oscillators (fallback) y se emite el diagnóstico nuevo; (3)preloadcon contexto suspendido → el fetch se emite igualmente. Los tres corren en el proyectoserversin navegador (verificado: mi harness los reproduce en tsx). - Verificación adversarial: Confirmado en el código real, sin contradicción encontrada. (1)
playSampleexiste en src/arts/sound/engine-sound.ts:643-683 (fetch → response.ok → decodeAudioData → sampleCache,return falsepara que play() caiga a synthesize en :238-242) ypreloaden :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 depeaks(). Ampliando el patrón a decodeAudioData sólo aparecen :294/:295 y :352, que pertenecen a los tests dedecode()— 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.tsda 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 congetOrCreateContext(), que haceawait 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 justificarensureContextendecode(). Y comoEngineSemanticlanza 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 flotantevoid ...que nunca liquida. Rompe además la invariante publicada del art: «no context, no listeners until prime()/play()». - Arreglo propuesto (NO aplicado):
preload()debe usarensureContext()(decodificar funciona en contexto suspendido — es literalmente lo que dice el comentario dedecode). Opcionalmente diferir el preload del constructor deEngineSemantical primerprime(); y dar un diagnóstico alcatchvací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 conawait this.getOrCreateContext(), que en :417-423 haceawait ctx.resume()con uncatchque sólo cubre rechazo, no promesa pendiente — exactamente el peligro que el propio motor documenta en :428-437 para justificar quedecode()useensureContext(). Cadena de disparo verificada:createActiveUixconstruyeEngineSemanticeager consoundEngine: uix.sound(active-uix.svelte.ts:142-155) → ramaopts.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 registrachatLogSemaconsound: 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:
SoundChannelavisa cuando le llegan opciones de construcción junto a unengineinyectado. PeroEngineSemanticfiltra esas claves ANTES de construir el canal: en la rama de motor compartido sólo reenvía{ engine, preferences, logger }. Comoactive-uixsiempre inyectasoundEngine,masterGain/dom/timers/audioContextFactory/fetcherpasados comoevents.soundse 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
soundOptionsy (a) avisar con el mismo mensajeIGNORED_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 usasound: 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 ennow + durationSeccon 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.gainpor 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_TUNINGSfijangaincomo número desnudo, yapplyOverrideaplica 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/fulfillsuena 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 hacencommit.silent/emerge.silent/contact.silent. Y extendersounds-grammar.test.tscon un test que prohíbagainnumérico desnudo enSOUND_TUNINGS(el guard ya sabe leer el gain base de la familia desdeSEMA_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):
- 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). - Medicion Dialog EXACTA:
openfamily 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.fallbackTargetGANA 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ó consemaSelector(morfo, partDeclarado, …), y la regla deja de casar en silencio. - Impacto: Las dos reglas
emergede NavigationMenu están muertas: abrir y cerrar un panel del nav suena con la base de familiaemerge(gain 0.2) en vez deemerge.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 paracommit-select: se arregló una regla de tres y quedaron dos. Hay 185 llamadas confallbackTargeten soma, así que el riesgo de más instancias es alto. AVISO:sema/components/navigation-menu.tsaparece 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, llamarlotargetOverridey que el morfo lo declare. (b) Guard de censo que, para cadaSema, compruebe que el part del selector coincide con eltargetdeclarado 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
soma/runtime.svelte.ts:757y:829dicen literalmenteopts.fallbackTarget ?? targetReg?.ref?.current ?? null— el fallback gana. Verificado.morfo/components/navigation-menu.tsdeclaraemerge-openyemerge-closecontarget: v.partRef('content'); el pack (sema/components/navigation-menu.ts:45,49) seleccionacontent; el proveedor (navigation-menu-provider.svelte.ts:150-154,162-169) pasafallbackTarget: triggerEl. Las tres piezas están donde dice.- El matcher real es
safeMatches(sema/resolver.ts:211):target.matches(sel) || target.closest(sel) !== null. El razonamiento del auditor sobreclosest()es correcto en mecanismo: verifiqué en el demo real (web/routes/uix/components/navigation-menu/+page.svelte:138-160) queContentes 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:
applyMapOverridesno valida las rutas: un segmento inexistente se CREA en vez de fallar, así que una errata en una clave deoverrides.runtimees 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 deSemaMap. - 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,projectorytimerscon los del engine. Si el llamante configuró el proyector en la bolsa anidada y no a nivel raíz, se le asignaundefinedyVisualChannellanzaSemaConfigError. Es la asimetría exacta contra sound/haptic/announce, que sí usancanal ?? engine. - Impacto: VERIFICADO EN RUNTIME (vitest, spec temporal ya borrada):
new EngineSemantic({ visual: { projector } })lanzaSemaConfigError;new EngineSemantic({ visual: { dom } })también; con la misma opción a nivel raíz ambos construyen sin error. El tipoVisualChannelOptions(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-838afirma el contratorequiresOneOfForVisualProjection: ['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) esfalse | 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 todosanidado ?? 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>. ComoDeltaValueincluyenumber | string | boolean | null, la segunda rama traga cualquier clave con cualquier primitivo: ni el nombre del campo ni su tipo se comprueban.SOUND_TUNINGS, tipadoas const satisfies Record<string, SoundOverride>, no valida nada de sus valores. - Impacto: Un
gainstring llega intacto hasta el audio:applyLeaf(resolver.ts:344-346) devuelve el string tal cual, el corte nuevoif (typeof sig.gain === 'number' && sig.gain * gainScale <= 0) return;(sound.ts:176) lo deja pasar por eltypeof, yengine.play(sig, …)(sound.ts:181) recibegain: 'loud'. Por el lado morfo tampoco hay red: el esquema sium deeventSemanticSchema(morfo/schema.ts:260-281) NO declarachannelsnioverrides, yobject()usaunknownKeys: 'strip'por defecto (arts/sium/core/object-combinator.ts:30) mientrasvalidateMorfodevuelve el objeto CRUDO (morfo/schema.ts:757-768). El guardsounds-grammar.test.tsvigila la FORMA de las claves del catálogo, nunca sus valores. - Arreglo propuesto (NO aplicado): Estrechar la rama abierta a los
DeltaOpsobre campos conocidos:{ [P in keyof S]?: S[P] | DeltaOp }en vez deRecord<string, DeltaValue>, dejandoRecord<string, DeltaValue>sólo para ids de canal SIN slice tipado. Alternativa mínima: tiparSOUND_TUNINGScontraPartial<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 yas const satisfies Record<string, SoundOverride>. Ejecuténpx tsc --noEmit -p tsconfig.jsonsobre 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 contarget: v.partRef('input')y el proveedor los dispara SINfallbackTarget, así quedata-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-submitsuena con la base de familia commit (gain 0.30, sema-map.ts:114-133) en vez delcommit.soft0.05 elegido → ~6× más fuerte. El aviso de desbordamiento suena con la base signal (gain 0.40) + deltas derisken lugar de 0.03 → ~13× más fuerte, y el háptico pasa detapapulse0.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): pasarfallbackTarget: this.opts.ref.currenten el proveedor, pero eso contradice eltargetdel 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 llamathis.select(value)sin pasarlo, teniendoactiveElen la mano. Enter/Espacio sobre un item no emitecommit-select: no hay sonido, ni háptico, ni estampadodata-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
activeElen la llamada del teclado (this.select(value, activeEl)) o, mejor, resolver el elemento dentro deselect()pordata-valuecomo hace TreeGrid, y eliminar la guardaif (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 pasaractiveEl, 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-resety el pack le dedica una regla completa (canal + sample + háptico), pero el proveedor sólo emitecommit-saveycommit-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-resetdel Picker genérico compuesto (picker-provider.svelte.ts:201), que estampa sobre[data-picker…]y activa OTRA firma (la del packpicker). Regla muerta + documentación que miente. - Arreglo propuesto (NO aplicado): Emitir
commit-reseten la acción Clear del proveedor (confallbackTargetsi 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:
src/uix/morfo/components/gradient-picker.ts:36declara el eventocommit-reset(familycommit, verbreset, targetv.partRef('provider'), intentneutral).src/uix/sema/components/gradient-picker.ts:40-45le dedica una regla completa (onProvider({ eventName: 'commit-reset' }), canales sound+haptic), con la doctrina en el comentario de cabecera (línea 16).src/uix/soma/components/gradient-picker/gradient-picker-provider.svelte.tssólo disparacommit-save(:135, ensavePreset) ycommit-remove(:143, endeletePreset). Grep exhaustivo sobre TODO el árbol soma+eidos de gradient-picker (12 ficheros):commit-resetaparece únicamente en los dos README (soma/.../README.md:65yeidos/.../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 confallbackTarget: triggerEl, yfallbackTargetGANA 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 deemerge.softal abrir y el 0.05 descendente deemerge.exit.softal 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 matchingitemwhile the runtime stamped on the trigger, so the selector NEVER matched»): se arregló la regla decommit-selecty 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 quemenubar.ts:23-33hace bien), o el proveedor deja de pasarfallbackTargetparaemerge-open/emerge-closey deja que el runtime resuelva elcontentque el morfo declara (target: v.partRef('content'), morfo navigation-menu.ts:41 y :50) — pero ojo: enemerge-openel 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 (morfotarget+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.
-
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-154y:162-169) emite ambos confallbackTarget: triggerEl, yruntime.svelte.ts:757es literal:const target = opts.fallbackTarget ?? targetReg?.ref?.current ?? null— el fallback GANA sobre el ref del part registrado. -
La lectura del anidamiento es correcta.
navigation-menu-content.svelterenderiza 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 deemerge-closeno pasasemantic, así que la familia siempre se queda enemergey 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ú comocommit(selección que navega) osignal(fallo), y no hay camino. Nada lo detecta: nimorfo:vocabularyni el runtime avisan de capacidad declarada sin uso — sólo hay error en el sentido contrario (SomaRuntimePolymorphicErrorcuando se pasa una familia fuera del allowlist). - Arreglo propuesto (NO aplicado): O el proveedor concreta (un
DISMISS_CAUSEScomo el de popover para distinguir cierre-por-navegación de cierre-por-blur), o se quitaallowedFamiliesdel morfo y se deja el evento concreto. Y añadir al censo de morfos una comprobación de que cadaallowedFamiliestiene al menos un call site que pasasemantic: es barato (grep tipado sobreruntime.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-selectsobreitem) y sinallowedFamiliesen 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 (Men 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:
src/uix/morfo/components/navigation-menu.tslí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 superficiedata-event-*del target sin mirar de quién es;unstampEventAttrsrecibe el signal y lo ignora (_signal). Como el engine llama al cleanup de cada emit en su propiofinally, 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 unhandle-draganterior ~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 queVisualChannel.awaitExpressionexiste para evitar. Durante el arrastre losdata-event-*parpadean (presentes ~2/3 del tiempo, hueco de ~48 ms cada 72 ms). (b) SEÑAL PERSISTENTE FANTASMA: unsignal.warn + risk(persistence: 'untilFix', holds.ts:94) queda estampado indefinidamente; cualquier señal transitoria posterior sobre ese mismo target (uncontact-activatedel propio campo) le borra los attrs al terminar su hold, mientrasengine.activesigue reteniendo la entrada —hasActive(id)sigue devolviendotrue(engine.ts:467-469, 519-521) yclear(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:
unstampEventAttrsya recibe el signal — comparartarget.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
_signaly lo descarta. El handle de projection/dom.ts:62-71 sólo cierra sobretarget— 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 leeintensityen dos de sus tres caminos: cuando haypatternlo reenvía tal cual, y los kindssuccess/warning/errordevuelven patrones CONSTANTES que ignorandurationeintensity. 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: unnew EngineSemantic({ haptic: { masterIntensity: 0.1 } })sigue disparando [40,60,40,60,40] completo en un threat. El deltafulfill.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 yKIND_MIN_MS). Alternativa mínima: queattenuateNonVisualescale tambiénhaptic.patterncuando exista, y quekindToPatternmodule los tres kinds constantes porscaleen 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.patternreenvía[...sig.pattern]sin multiplicar por intensity ni masterIntensity; sólo kindToPattern recibesig.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 caminofactor<=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:
AnnounceFnpide la prioridad en un objeto de opciones;uix.announcela 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ónannounceal 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=alertal documento junto a las deuix.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,priorityllega como objeto ythis.liveRegionIds[priority]esundefined: 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
AnnounceFnsea(message: string, priority: AnnouncePriority) => void, o que active-uix paseannounce: (m, { priority }) => this.announce(m, priority)al construir elEngineSemantic. 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, mientrasuix.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 puenteannouncedel 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 familiacommit, 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
intentno 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_LIBRARYestá 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 sinbase.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
activeChannelsdel family. Añadirhaptica un pack que opera sobre familyemergees incoherente; añadirsoundahandletambié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ó:
channelses 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ñadenchannels: ['sound','haptic']o se borran. Guard natural: un test que recorra los packs y falle cuando una regla declarasound/hapticque elactiveChannelsresultante no incluye. - Verificación adversarial: Ambas patas se sostienen contra el código real. (a) dialog.ts:93-96 y 105-108 declaran
hapticsobreeventFamily: 'emerge'sinchannels:; 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 traechannels— declararhapticpueblanext.haptic(rama current===undefined, líneas 265-269) pero NO activa el canal; el morfo del dialog no declarachannelsen ningún evento y sus eventos open/close son family 'emerge' (morfo/components/dialog.ts:30,87); haptic.ts:114 corta conif (!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
emitcontract» —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 aclear/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 declaraasync emit(signal: SemanticSignal): Promise<string>conreturn id(:402 y :471) yclear(id): boolean(:485). Busqué activamente el neutralizador y no existe:emitse declara UNA sola vez en todo src/ (no hay port/interfaz más estrecha que justifique elvoid;EventEngineEmitteren soma/runtime.svelte.ts:90 esPick<EngineSemantic,'emit'>y hereda elstring), 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 —emitexiste, no dispara—, I10 tablas de props). La deriva es incluso más ancha: sema.md no mencionapersistence,clear,clearTargetnihasActiveen 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
AnnounceChannelexiste, 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.tsen 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) conhandle(),dispose(), regiones live propias de fallback.src/uix/sema/engine.ts:88→announce?: true | false | AnnounceChannelOptions | Channel;enEngineSemanticOptions, junto avisual/sound/haptic.src/uix/sema/engine.ts:338-350→ registro real en el constructor (this.register(announceChannel)), simétrico al dehaptic(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) trataannouncecomo canal de primera clase: «...accessibleannouncechannel 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:
playSampledevuelvefalseen cuatro caminos (sin fetcher,!response.ok, throw del fetch, throw del decode) yplay()cae asynthesize()sin emitir nada. El catálogoSOUND_DIAGNOSTIC_EVENTSni 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 quewarnedVoicesen :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:
preloadresuelve el contexto congetOrCreateContext(), que CREA el AudioContext y luego haceawait ctx.resume(). Como sema dispara el preload dentro de su constructor y active-uix construyeEngineSemanticde forma eager, en la carga de página el contexto estásuspendedsin gesto de usuario: elresume()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
liveContextssube, contradiciendo el comentario de active-uix.svelte.ts:128-131 («no AudioContext, no listeners until the firstprime()»); (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()enpreload(decodificar funciona con el contexto suspendido, igual que endecode) y, si se quiere cero-coste en boot, no crear contexto:if (!this.audioCtx) return;y reintentar el preload desdeprime(). Alternativa complementaria: que sema no llame al preload en el constructor sino desde el primerprepare(). - 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 congetOrCreateContext()(:402-426), que CREA el AudioContext (master + 2 buses = los 3gaindel harness), instala el unlock listener y luego haceawait ctx.resume(); el propio fichero documenta que ese resume fuera de gesto queda pendiente (:428-437) y por esodecode()usaensureContext(). El disparo eager existe: sema/engine.ts:329-335 lanzavoid soundChannel.preloadSamples(...)en el constructor, y active-uix.svelte.ts:134-156 construyeEngineSound+EngineSemanticeager consoundEngine: sound, así que el contexto abierto es el COMPARTIDO — justo el que el comentario :128-133 promete que no existe hastaprime(). 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.popynotification.dingno 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_LIBRARYcree que overlay/surface tienen voz de sample y no la tienen. Ademásoverlay.*ysurface.*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
preparesólo mirasignal.channels; nunca consultapreferences.sound. Consound: '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: «
offsilencia») 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 ejercitahandle, nuncaprepare, así que el guard cree estar puesto y no lo está. - Arreglo propuesto (NO aplicado): En
shouldPrime(o antes dethis.engine.prime()) devolver false cuandothis.preferences?.sound === 'off', y extender el test de 'off' para llamar también aprepare. 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 mirasignal.channels) →engine.prime();this.preferencesse lee ÚNICAMENTE enhandle()(:166).engine-sound.ts:203-220prime()crea el AudioContext y llamasetupUnlockListener()(:479-508 → 5 tipos de evento en captura + visibilitychange si autoSuspend).engine.ts:406-409invocapreparepara todo canal registrado; el único corte previo essignal.channels.length === 0(:401). No hay guard aguas arriba: el registro del canal (:289) depende deopts.sound, nunca de preferencias. Ysound.test.ts:113efectivamente sólo ejercitahandlepese 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
trydeprime()abarcacreateContext()(que ya incrementóliveContextsy dejó el contexto vivo),setupUnlockListener()yresume(). Si algo posterior a la creación lanza, elcatchponeaudioCtx = nullsin cerrar el contexto ni decrementar el contador: el contexto queda huérfano (nadie puede cerrarlo ya, ni siquieradispose()) yliveContextsse 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
catches 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()yresume()fuera del try, que es lo que ya hacegetOrCreateContext: allí sólocreateContext()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: eltryabarcacreateContext()+setupUnlockListener()+ctx.resume(); elcatchsolo hacethis.audioCtx = null; this.masterGainNode = null;. Niclose(), niliveContexts--, ni diagnóstico. Literal como se afirma.createContext()455-477: asignathis.audioCtx, monta el árbol de buses, yliveContexts++(470) es de las ÚLTIMAS sentencias, seguida delemitSoundDiagnostic(MULTIPLE_CONTEXTS). Así que un throw posterior deja contador incrementado + contexto vivo.dispose()316-337: todo el cierre está bajoif (this.audioCtx). ConaudioCtxnulificado, el contexto es INALCANZABLE (el gettercontexttambié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/conectaprefs.soundconbus('ui')ni conSemaPreferences; el único consumidor deprefs.soundes 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 cambiadata-sounden<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 desrc/arts/README.md) con el texto ya corregido del README: el busuies la PALANCA que una app puede cablear; lo garantizado por construcción es el negativo (ninguna política de UI escribe el buscontentni el volumen de una fuente). - Verificación adversarial: CONFIRMADO en todos sus extremos, y con alcance MAYOR del declarado.
-
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.soundmaps to theuibus ONLY and must never touch the content's volume».git blamelo fecha en2f872ef864(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:120repite la fórmula en la celda del art: «prefs.soundgovernsuionly». -
La contradicción intra-art es literal.
src/arts/sound/README.md:44-53dice lo contrario: «Honest about the mechanism (audit AU-4): todayprefs.soundacts through sema's channel … and nothing wires prefs to a bus directly. Theuibus 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
activeChannelsy 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 queslider.ts:24-31ya usa para reactivarsounden la familiahandle— o borrarlas. Y un guard de censo que cruce cada regla conSEMA_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.
-
El código está donde dice.
src/uix/sema/chans/haptic.ts:114→if (!effective.activeChannels.includes('haptic')) return;.sema-map.ts:169emerge →activeChannels: ['sound'];:190shift →['sound'].dialog.ts:94-96(alertdialog, acotadaeventFamily:'emerge'),dialog.ts:106-108(data-sheet, acotada a emerge),editable.ts:18(eventoshift-enter-mode, familia shift fija enmorfo/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). -
La lectura del resolver es correcta.
resolver.ts:143siembraactiveChannelsdesde 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,
applyOverrideclona 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 llevanpattern, que enhaptic.ts:127gana sobrekind+duration. Pero en cuanto se arregle el hallazgo 4 activando el canal,kindToPattern('tick', undefined, undefined)(haptic.ts:147-156) calculaMath.max(0.3, Math.min(1, NaN)) = NaNy termina ennavigator.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>comoTen 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.soundEngineexiste, el engine promueve las opciones del canal de la forma B (motor propio) a la forma A (motor compartido) construyendo{ engine, preferences, logger }y descartandomasterGain,audioContextFactory,fetcher,domytimers. El aviso anti-contrabando deSoundChannelno 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 } })dejachannel.engine === shared, pierdemasterGainywarnNO se llama ni una vez; sinsoundEnginelas 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
soundOptionsy avisar por el logger (reutilizarOWN_ENGINE_ONLY_KEYS+SOUND_CHANNEL_LOGS.IGNORED_ENGINE_OPTIONS), o mejor: no aceptarlas en el tipo cuandoEngineSemanticOptions.soundEngineestá 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 —soundysoundEngineson campos independientes en EngineSemanticOptions (engine.ts:68 y :78) y ActiveUixOptions.events es la superficie entera (active-uix/types.ts:77), así quecreateActiveUix({ 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, dondeintentestá 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á aceptandocontact + threatsin decir nada.validateSemaEventtampoco cubre 'forbidden': validation.ts:39-46 sólo comprueba=== 'required'. - Arreglo propuesto (NO aplicado): Derivar también
IntentForbiddenFamilyy añadir la tercera rama aSemaEvent({ family: IntentForbiddenFamily; intent?: never }), o eliminar 'forbidden' deIntentRequirementhasta que exista. Añadir la rama simétrica envalidateSemaEvent/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' producenevery 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 IntentOptionalFamilyes 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-110isSemaEventtiene el mismo agujero — si el intent está PRESENTE devuelveisIntent(...)||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_POLICYse 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 conevent.ts:14-31, que sí lo ata conas 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.
- 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 eneventSemanticSchema(260-281): rama 1 conintent: optional(...), rama 2 conintentobligatorio.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.
- La lectura es correcta y el desacople es total.
schema.tsNO 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 | SemaEventLabelySemaEventLabel = 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:
SemaEventLabelcomoIntentOptionalFamily | \{SemaFamily}-{Intent}`, y envalidateSemaEventparsear 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-errorsobre{ family: 'commit' }comoSemaEventSE CONSUME (la rama estructurada sí cierra), mientrasconst viaLabel: SemaActionEvent = 'commit'compila limpio; yvalidation.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)SemaActionEventNO 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 paraIntentExpectedFamily, yeventSemanticSchema(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,gradientBuilderSemaygradientPickerSema. 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-dragsobreevent-chipyhandle-resizesobreevent-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 familiaemergeoshift, cuyas entradas en SEMA_MAP declaranactiveChannels: ['sound']. El resolver INSTALA la firma háptica (resolver.ts:260-270, ramacurrent === undefined) pero no tocaactiveChannels, yHapticChannel.handlesale 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
tapde 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:vocabularyno mira las reglas de cascada, ysounds-grammar.test.tssó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 levantarsoundsobre la familiahandle); o (b) borrar los bloqueshapticsi 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únactiveChannelsalcanzable activa: es el mismo tipo de cruce mecánico que ya hacesounds-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 familiashift,signalocontact. El guard nacido hoy sólo valida la FORMA de la clave contraSEMA_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 unsignal-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:signales 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 decommit. Y el riesgo latente es el que ya se materializó una vez: si mañanacommit.subtlepasa 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 extendersounds-grammar.test.tscon 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_KEYSpor!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
verbdentro deopts.semantic. El runtime lo asigna aresolvedVerby acto seguido lo tira; elemitsale conname: 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 pordata-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
namecon é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 quitarverbde los cuatroDISMISS_CAUSESpara 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 campoverben 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
handlepero NO enprepare. Con el sonido enoff, cada emit sigue llamando aengine.prime(), que crea elAudioContext, 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 registrandopointerdown/mousedown/click/touchstart/keydownen el documento, en el primer gesto del usuario. Colateral del mismo hueco: una familia sin sonido enactiveChannels(p. ej.handle, sema-map.ts:192-202activeChannels: ['haptic']) también ceba el audio. - Arreglo propuesto (NO aplicado): Mover la política al gate:
shouldPrimedebe devolverfalsecuandothis.preferences?.sound === 'off'(y, si se quiere apurar, cuando la familia del signal no declaresounden suactiveChannels), y fijarlo con un test sobreprepare, no sobrehandle. - Verificación adversarial: El código es exactamente el descrito. sound.ts:150-156
prepare()sólo consultashouldPrime(signal)y llamaengine.prime(); sound.ts:197-200shouldPrimemira únicamentesignal.channels, nuncapreferences; la políticaoffvive sólo enhandle(sound.ts:166-167). engine-sound.ts:203-220 confirma queprime()crea el contexto y llamasetupUnlockListener(), que engancha los 5 eventos endocumenten captura (:479-489). engine.ts:405-409 ejecutapreparede todos los canales en cadaemit, con el único filtro designal.channels: [](:401-403). Ningún test cubreprepare+ prefs off (sound.test.ts:113-127 y sound-port.test.ts:126-133 sólo ejercitanhandle; sound.test.ts:171-189 sólo el casochannels:['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:
writeLiveRegionescribe 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 endispose(). - 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; hoyAnnounceChannelOptionsni siquiera lo acepta), y construir/retirar las regiones a través del aplicador DOM inyectado en vez decreateElement+appendChild. - Verificación adversarial: El núcleo del hallazgo se sostiene literalmente.
announce.ts:100-107es exactamente como se cita y no programa vaciado alguno;AnnounceChannelOptions(:41-52) ni siquiera admite un scheduler yengine.ts:344-347sólo le reenvíaannounce+dom, mientrasvisual.ts:38yhaptic.ts:58sí recibenSemaTimerSchedulery usansemaDelay— 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='') yannounce-provider.svelte.ts:86,106-124(clearAfter por prioridad). No hay guard, test ni fallback que lo neutralice:announce.test.ts:51-69só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/holdcopiando 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 :521loss: { hold: 'noticed' }. El fichero real tiene holds.ts:86fulfill: { hold: 'settled' }y holds.ts:103loss: { 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 declaranpersistence?yhold?: SemaDurationSpeccomo campos del evento, resolver.ts:137-139 hacesignal.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) yzoom(extensión firmada 2026-06-10). - Impacto: Un autor que valide su morfo contra la tabla de sema.md concluirá que
unselect/zoomno son canónicos y renombrará eventos correctos — o al revés, dudará demorfo:vocabularycuando 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-closeapuntan acontenty el estampado va altriggerREFUTADO POR ESTADO ACTUAL, NO POR ERROR DE LECTURA. Verifiqué toda la cadena mecánica y el auditor la leyó bien: (1) runtime.svelte.ts:757opts.fallbackTarget ?? targetReg?.ref?.current— fallbackTarget GANA sobre el target declarado en el morfo, pese al nombre; (2) resolver.ts:208-215safeMatches= matches||closest sobre un selector compuesto[data-navigation-menu-{part}][data-event="{name}"], y comodata-eventsó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):
-
Los 5 .wav son audio válido. Cabecera RIFF/WAVE con
fmtde 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. -
La ruta existe de punta a punta y SÍ se alcanza.
sample()(sounds.ts:57-66) →SOUND_LIBRARY→sound(name)→resolveSoundDefinition(:230-239) inyectasampleUrlsobre el fallback → regla de cascada →resolveSignatureconservasampleUrl→SoundChannel.handle(chans/sound.ts:181) →EngineSound.play(:222-246) →playSamplecon fetch +decodeAudioData+ caché por URL +BufferSource. Verificado ejecutando el motor con contexto y fetcher falsos: con respuesta decodificable se creanbuffersource,gainy CERO oscillators; con 404 se creanosc,osc,filter(fallback). Hay fetch/decode real, no es fachada. -
Los packs con sample están cableados en la app real y sus eventos se emiten.
web/routes/uix/+layout@.svelte:123-125arranca consound: truey la lista incluyechatLogSema(:129),formSema(:142) yproofOfHumanSema(: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. -
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-invalidesfamily: 'signal'(morfo/components/form.ts:34-40) ysignal-notify-newtambién (morfo/components/chat-log.ts:44-50). El comentario doctrinal de chat-log.ts:8-15 es exacto. -
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:668envelope.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 retirasounddeactiveChannels(engine.ts:189-193) y la ataja el gate anterior (:159);reducedagainScale0.4, nunca 0 (:107,:168). Eltypeof … === 'number'es defensivo pero inocuo (gaines obligatorio enSoundSignature, channels.ts:17). Cubierto por test: chans/sound.test.ts:129-144. -
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 supervivienteform.*nitooltip.silentnitabs.select.soft. El guard nuevo (sounds-grammar.test.ts) verifica cabecera de familia, forma del tail y —lo mejor— que cada.silentreste EXACTAMENTE elbase.sound.gainde su familia leyéndolo del mapa en vez de copiarlo (:79-106). Esa es la pieza que impide que un.silentdeje de ser silencio si alguien retoca el mapa. -
Las URLs resuelven.
svelte.config.jsno definepaths.base, así que/sounds/*.wavcae en la raíz del origen, que es donde adapter-static publicastatic/. -
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 essrc/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 quesrc/uix/sema/components/navigation-menu.tsaparece 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:
-
Sin fuga de nivel entre reproducciones.
gainScaleviaja 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 conreduce). El bug histórico A-5 (master × 0.4por reproducción) está realmente cerrado. -
Propiedad y disposal del motor compartido.
SoundChannel.dispose()sólo cierra el motor si lo creó (ownsEngine, sound.ts:184-187); las tres ramas deEngineSemantic(engine.ts:299-319) son ramas y no un spread, así que la unión discriminada de opciones no se rompe.active-uixstandalone dispone lo que creó y en el orden correcto (eventsantes quesound, línea 630-635), y en attach sólo siownsSound(línea 649).ensureContextestá guardado pordisposed(engine-sound.ts:438-448), así que nidecode()ni unattach()tardío pueden abrir un contexto en un motor muerto. -
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.silentno 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 ramaoffsigue delante del corte, así que no hay cambio de orden de política. -
El renombrado de SOUND_TUNINGS está limpio. Cero residuos de
form.*ensrc/uix/sema/; todos lossoundTuning(...)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. -
Sin dobles emisiones.
emitdespacha cada canal exactamente una vez (engine.ts:435-442) y nadie fuera del canal llama aengine.play— verificado ensoma,blocksypacks: los únicos consumidores del art sonuix.sound.media(el)(media-player) ydecode/peaks(waveform). El canal es fire-and-forget, así que el earcon nunca retrasa el commit estructural del caller. -
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 unmedia()post-dispose no queda colgado. Elattach()es explícitamente irreversible y documentado como tal. -
La política sobrevive a la ausencia de contexto.
setGain/setMutedantes del primer sonido se guardan y se aplican al materializar el grafo (createContextllama aapplyBuspor bus, líneas 463-468; el master tomamasterGainValueen 460-461). Los ducks refcontados usan el factor MÁS FUERTE en vez de multiplicar (381-387), que es la semántica correcta. -
Fallback de sample → síntesis. Un fetch/decode fallido devuelve
falsey 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:
-
El orden de capas es exactamente el documentado.
engine.ts:274this.cascade = [...packCascade, ...(opts.overrides?.cascade ?? [])]+resolver.ts:200-203ordenación ascendente ESTABLE con desempatea.index - b.indexy aplicación en ese orden ⇒ la regla de app gana al pack en empate de especificidad. La cabecera deresolver.ts:5-28y la deengine.ts:14-22describen el mecanismo real. -
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 usanapplyOverride→applyLeaf(current, delta, 'replace')(resolver.ts:271-275); capa 4 usa'replace'enapplyPathOverride(resolver.ts:407).{op:'add'|'multiply'|'replace'}se resuelven enapplyLeaf(resolver.ts:350-357) y ganan siempre al modo por defecto. -
Las tuningsg
.silentson doctrinalmente correctas Y están guardadas.commit.silent(sounds.ts:202),emerge.silent(:211) ycontact.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-106lo 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). -
El renombrado de SOUND_TUNINGS de hoy está COMPLETO en código. Cero referencias a
form.*,tooltip.silentotabs.select.softensrc/; las 14 claves usadas por los packs existen todas en el catálogo. La gramática{familia}.{cola}la cierrasounds-grammar.test.ts:60-77contraSEMA_FAMILIES, y la lista de excepciones está vacía y auditada (:108-111). -
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 deactiveChannels(:159) y desig(:160-161) y ANTES deengine.play(:181); compara la ganancia RESUELTA (no la tuning) y multiplica porgainScaleantes de comparar, así quepreferences.sound === 'reduce'(factor 0.4) no puede cambiar el signo. No tocaprepare()/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 sueloFREQ_FLOOR0.25, así que nunca convierte un gain positivo en 0 ni un 0 en positivo; el caso mute usafactor <= 0, que elimina el canal deactiveChannelsen vez de escalar. -
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][...]), quetarget.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. -
El slider reactiva bien el canal de sonido para la familia
handle.handledeclaraactiveChannels: ['haptic'](sema-map.ts:201);slider.ts:24-31añadechannels: ['sound','haptic']explícitamente, y las primitivas delhandle-dragllegan por capa 3 desdeslider-provider.svelte.ts:250(resolveSliderDragSound). La regla sinsound:es correcta por diseño, no un olvido. -
safeMatchesno puede lanzar. Eltry/catch(resolver.ts:210-214) más el escapado propio e isomorfo de valores enselectors.ts:50-52y la validación de nombres de atributo (:55-63) cierran la clase de bug MOR-1. -
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:
-
El declaration merging de
SemaChannelSignaturesFUNCIONA por la ruta documentada. Probado con tsc: condeclare module '$uix/sema' { interface SemaChannelSignatures { voice: VoiceSignature } }, la aserción'voice' extends keyof SemaChannelSignatures ? true : falsedatrue(y confalseel compilador protesta, o sea el test discrimina), ySemaSignatureOverridegana el slicevoicetipado. Es notable porque la interfaz vive enchannels.tsy 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:61ybook-deviations.md:474dicen la verdad. -
La rama estructurada de
SemaEventimpone el intent de verdad.const x: SemaEvent = { family: 'commit' }es error de compilación (el@ts-expect-errorse consume). La derivaciónIntentRequiredFamilydesdeSEMA_FAMILY_POLICYes genuina para 'required'/'optional' — el único agujero es el tercer valor y la forma etiqueta (hallazgos aparte). -
SoundChannelOptionsSÍ es una unión discriminada real. Ambas formas declaranengine(engine: EngineSoundvsengine?: undefined, sound.ts:57-59 / 66-71), así que el narrowing deengine.ts:299funciona ymasterGainjunto a unengineinyectado 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. -
El corte de ganancia nuevo hace lo que dice. Verificado en runtime:
gain: 0no llama aengine.play;gain: 0.05sí. Y la aritmética que el docblock decommit.silentpromete es exacta: con la familia commit (base 0.3) el resolver deja neutral en 0,fulfillen 0.05 ythreaten 0.1 — los deltas de intent siguen aflorando, que era el objetivo. -
La ley de nombres de
SOUND_TUNINGStiene un guard que no copia el canon.sounds-grammar.test.tslee las familias deSEMA_FAMILIESy la base de ganancia deSEMA_MAP.families[f].base.sound.gainen vez de duplicarlas, y comprueba que cada.silentreste 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. -
El renombrado está completo en código. Ni una llamada a
soundTuning(...)con clave muerta en los ~50 packs desrc/uix/sema/components/*.ts(barrido completo de los aciertos). Las menciones aform.*que quedan enbook-deviations.md:336-345son narración histórica correcta. -
Cero
anyen el código de producción de sema. Los únicos casts sonas 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. -
Todas las opciones de
EngineSemanticOptionsse 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. -
applyMapOverridesno muta el mapa canónico (clona por camino, resolver.ts:376-409) yresolveSignaturees puro. El fallback de id muerto enapplyDominanceya se retiró con su justificación explícita (engine.ts:564-570) y la cancelación del cap de expresión envisual.ts:120-124cierra el timer fantasma. -
La separación de propiedad del motor de audio es coherente:
ownsEnginedecide eldispose(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:
-
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. -
Falsos positivos del cruce estático que resultaron estar VIVOS por
fallbackTarget(verificados uno a uno leyendo el proveedor): · calendar — 6 reglas deshift-navigatesobre provider/prev-button/next-button/month-select/year-select/day:CalendarPaginationButtonProvider.onclickpasae.currentTarget(calendar-provider.svelte.ts:670-672), los<select>pasane.currentTarget(:778-781) y el teclado pasa la celda (:518). Escopetazo deliberado, todo alcanzable. · pagination — las 6 reglas: los 4 botones pasane.currentTarget(:240,:295,:350,:405), el item también (:472) y el provider queda para las llamadas programáticas (:166). · rating-group (item víahandleItemClick:240-242) · toolbar group-item (:354) · tree-view branch/item (:180 confromEl) · tree-grid (resuelve la fila porquerySelector:168-174,:218-226) · toggle-group (itemEl ?? this.resolveItemEl(value):140) · menubar (:145) · menu-dial y onion-menu (desde eidos conasTarget(...)) · path-trace · number-field/css-field scrubber · drag-drop · file-upload · month-grid · context-menu · dropdown-menu · listbox/select/combobox/tag-group (víacomputeListSelection, soma/layers/list-selection.ts:86-126). -
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í queregistrations.set(part, reg)(runtime.svelte.ts:498) no se pisa y el sello cae en el elemento correcto aunque no hayafallbackTarget. -
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 wrapperdata-media-player-volume-slideres 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 aprovidery 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). -
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. -
Atributos no declarados en el morfo que SÍ existen en el DOM (falsos positivos míos, confirmados):
data-sizeydata-sheetlos pone eidos en el mismo nodo Content (dialog-content.svelte:89-91, documentados como eidos-only) yrolelo pone el proveedor por variante (role: this.provider.opts.variant.current, dialog-provider:449). Ydata-last-action="dismissed-outside"existe literalmente en los 4 proveedores de overlay (dialog:52, popover:68, drawer:66, float-panel:55). -
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_TUNINGSno compilaría). Los silenciadores restan exactamente su base (commit.silent-0.3,emerge.silent-0.2,contact.silent-0.25) ysounds-grammar.test.tslo guarda leyendo el gain del mapa, no copiándolo. -
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í quethreat(+0.1) yfulfill(+0.05) siguen sonando sobre un.silenty sólo se ahorra la síntesis cuando el resultado es 0;prepare()sigue llamando athis.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. -
Familias imposibles: ninguna. Las reglas por familia de dialog/popover (
commit,signal) son alcanzables porqueclosees polimórfico conallowedFamilies: ['emerge','commit','signal']y losDISMISS_CAUSESconcretan save→commit/fulfill y fail→signal/threat. Las 5 reglas por intent de toast son alcanzables porqueannounceligaintent: { 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:
-
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». -
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íaapplyChannelDeltas, REPLACE en 3/4/5 víaapplyOverride) está implementada tal cual (resolver.ts:250-304). -
El eje de holds (D.12) es real de punta a punta.
SEMA_MAPya no tienehold(sema-map.ts:33-45),SEMA_HOLDS_BY_INTENTes la única fuente (resolver.ts:137-139, visual.ts:149-156), el peldañosettled: 400existe (durations.ts:38),MAX_EXPRESSION_WAIT_MS = 1500con 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-1120falla si una firma transitoria supera el tope. -
El renombrado de hoy quedó completo en el código. Cero claves
form.*/tooltip.silent/tabs.select.softvivas ensrc/**(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 deevent.tsy las ganancias base desema-map.ts, y verifica la invariante del silenciador aritméticamente ({op:'add', value: -base}).docs/canon/vocabularies.mdestá regenerado y cuadra: «Sound tunings (14)» con la tabla por familia == las 14 claves deSOUND_TUNINGS. -
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.mediumcon 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. -
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í quethreat(+0.1) yfulfill(+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). -
La excepción de D.7 para
signalse respeta donde aplica: chat-log usanotification.pingsobresignal-notify-new(familiasignal) y lo justifica citando D.7 en su cabecera; form usaalert.errorsobresignal-warn-invalid(familiasignal). Ninguno de los dos es el hallazgo — son el uso correcto. -
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. -
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 porsemaSelector, incluidos los descendientes del media-player, donde sólo el combinador es literal (media-player.ts:58,69-70). -
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 sinexpression(scripts/morfo-vocabulary-check.ts:225-253,444-464). -
SEMA_MIGRATIONcoincide 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). -
Los ejemplos de
validateEventNamede sema.md:1046-1060 reproducen la implementación (verbs.ts:252-298), incluido el match por prefijo más largo que salvaenter-mode. -
docs/process/handoffs-claude-md.md:517sigue nombrandoform.commit.soft/form.toggle.silent/tabs.select.softy «38 sema packs» — NO es un hallazgo: el fichero declarastatus: historicaly «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.