32 KiB
Auditoría arquitectónica del sistema — activeUIX
Auditor: Claude Opus 4.6 (rol: software architect, frameworks web) Fecha: 2026-06-04 Rama:
active-uixAlcance:src/arts,src/libs,src/svrs,src/uix/{morfo,sema,soma,eidos,active-uix},src/arts/active-app, más el contrato ejecutablesrc/uix/contracts.ts. Excluido por instrucción:web/routes/*(demos),src/lib/_demo. Base doctrinal: libro «Diseñando lo que ocurre — Una gramática de eventos para interfaces» (src/docs/Disenando_lo_que_ocurre_…docx, leído íntegro), la guíaGUIA_IMPLEMENTACION_SEMAUIX.md,LIBRO_VARIACIONES_Y_EXTENSIONES.md,active_architecture.mdycontracts.ts.
0. Resumen ejecutivo
Veredicto general: el sistema está arquitectónicamente sano. Las cuatro capas de
UIX (Morfo · Soma · Sema · Eidos) mantienen sus fronteras con una disciplina poco
común. La cadena de transcripción declarativa («Morfo declara · Soma transcribe ·
Sema emite · Dom aplica · Eidos lee») se respeta de forma verificable: Sema no toca
DOM directamente (salvo un leak puntual), Soma no decide color/CSS (salvo un caso),
Eidos no importa internals de Soma ni pregunta a Sema, Morfo es TypeScript declarativo
puro sin runtime, y la regla de composition roots (sólo ActiveApp y ActiveUix
standalone crean servicios) se sostiene en todo el árbol.
Fidelidad al canon del libro: muy alta. Las 8 familias, los 6 intents (sin info,
correctamente —el libro lo reclasifica como función de signal, no intent), los verbos
delegate exactos, la política de evaluabilidad por familia, el intent declarado
per-evento (nunca heredado implícitamente, como exige el Apéndice A: «el runtime no
debe adivinar»), la persistence/holds-by-intent y la migración semántica a11y están
codificados con fidelidad. La única desviación doctrinal profunda es consciente y
documentada (partición de los canales de expresión — §1.2 de este informe).
No se encontró ningún problema causado por refactors recientes (polymorphic-close, persistence, a11ySemantic, TSC v2.2, toggle-group structural identity, motion-as-art). Esos sprints aterrizaron limpios.
Los hallazgos se concentran en cinco temas transversales, ninguno de los cuales bloquea el framework:
| # | Tema transversal | Severidad máx. | Nº sitios |
|---|---|---|---|
| T1 | Erosión de la superficie $adom (imports directos de $libs/dom) |
MEDIA | ~8 |
| T2 | Observers crudos que saltan ActiveDom (new ResizeObserver) |
ALTA | 2 |
| T3 | Drift de documentación vs. código (docs van por detrás) | MEDIA | ~6 |
| T4 | Completitud del contrato ejecutable (contracts.ts) |
MEDIA | 2 |
| T5 | Editor Words (dev-track en curso) — fuera del contrato morfo | MEDIA | cluster |
Más dos bugs funcionales puntuales (uno superviviente de 2 auditorías) y un
wiring gap real en el slice de servicios de motion.
Estado de verificación al cierre
| Comando | Resultado |
|---|---|
npx vitest run src/uix/sema |
152/152 ✓ |
npx vitest run src/uix/morfo |
62/62 ✓ |
npx vitest run src/uix/soma |
822/822 ✓ |
npx vitest run src/uix/eidos |
142/145 (3 fallos, todos en words) |
npx vitest run src/uix/active-uix |
25/25 ✓ |
npx vitest run src/uix/contracts.test.ts |
31/32 (1 fallo: data-words-* no respaldados) |
npm run morfo:check |
1 fallo estable (words.toolbar) |
npm run morfo:vocabulary |
exit 0 (sólo warnings, en words/tree-grid) |
| Eidos lint (selectores) | 1 inválido (navigation-menu), pre-existente |
1. Fidelidad al canon del libro (la lente arquitectónica profunda)
El framework es, en esencia, la materialización ejecutable del libro. Esta sección evalúa esa correspondencia porque es donde una incoherencia tendría el coste más alto: una desviación silenciosa del canon erosionaría la tesis entera.
1.1 Lo que el framework codifica con fidelidad ✓
| Canon del libro | Implementación | Veredicto |
|---|---|---|
| 8 familias (contact, commit, signal, handle, emerge, shift, sustain, delegate) — cap. 8/29 | SEMA_MAP, SEMA_FAMILY_POLICY, SEMA_VERBS, types.ts (4 valenced + 4 transitional) |
Exacto |
| 6 intents (neutral, affirm, fulfill, risk, threat, loss) — cap. 10 | SEMA_MAP.intents (sema-map.ts:235-288) |
Exacto |
info no es intent, es función de signal — cap. 10 §8, cap. 34 §3 |
El framework no tiene info intent; usa signal.announce/notify + neutral |
Correcto |
Verbos delegate: offer, plan, authorize, act, review, escalate, return — cap. 29 §2 |
verbs.ts:149-157 |
Exacto, comentado al libro |
threat ≠ loss, affirm ≠ fulfill (regiones valencia/activación distintas) — cap. 9/10 |
sema-map.ts deltas separados; loss desciende (posterior, baja activación) vs threat (anterior, alta) |
Correcto |
| Intent declarado per-evento, nunca heredado del contenedor — Apéndice A, cap. 9 §10 Regla 1-2 | Doctrina per_event_intent_intrinsic; morfo declara semantic.intent por evento; emerge.open sin intent |
Correcto |
| Evaluabilidad por familia (commit/signal pleno; contact leve; handle en drop; emerge/shift/sustain none) — cap. 9 §9 | SEMA_FAMILY_POLICY.intentRequirement + intentGuidance (dos ejes) |
Correcto |
| Persistence: risk until-correction, threat until-action, loss con huella, affirm breve — cap. 32 §5-6, cap. 34 §13 | SignalPersistence (transient/untilAction/untilFix/stateBound) + SEMA_HOLDS_BY_INTENT |
Correcto |
| Migración semántica a11y: conservar significado no estímulo; reduced-motion, live region, foco — cap. 33 | MorfoA11ySemantic (requiresLiveRegion, requiresFocusMove, reducedMotionFallback) + ActiveDom.prefersReducedMotion + ActiveUix.announce |
Correcto |
| 5 niveles de gramática (familia / verbo / fase / intent / realización) — cap. 5 §8, cap. 37 | morfo.events.semantic (family+verb+intent) → data-event-phase (fase) → canales sema + eidos CSS (realización) |
Correcto |
| Composición temporal de eventos (secuencias, dominancia atencional) — cap. 30 | Morfos per-evento + cascade sema + expression field |
Correcto |
Esta fidelidad no es accidental: morfo:vocabulary hard-failea sobre drift de verbos
declarados, y SEMA_FAMILY_POLICY deriva la unión discriminada de tipos. El canon
está anclado a nivel de tipos, no sólo de documentación.
1.2 La desviación profunda — partición de los canales de expresión (consciente, documentada, pero con costuras)
El libro (Glosario; Apéndice A; cap. 11; cap. 30 §"Hemos separado canales") define 8 canales de expresión como un eje unificado del evento:
tiempo · motion · presencia · profundidad · forma · color · sonido · háptica
y los trata como una firma de expresión única (Apéndice B): una sola configuración
que materializa la lectura del evento. El ejemplo declarativo del Apéndice A lo deja
explícito: "channels": ["presence", "depth", "shape", "color"].
El framework parte ese eje en dos dueños:
- Sema resuelve sólo
sound+haptic(canales runtime reales) + un meta-canalvisualque proyectadata-event-*durante el hold. - Eidos materializa motion / presencia / profundidad / forma / color reaccionando
por CSS a esos
data-event-*y a losdata-state/data-intentdel morfo.
Esto está documentado deliberadamente (LIBRO_VARIACIONES_Y_EXTENSIONES.md §D.8;
active_architecture.md «Sema sin slices visuales»; el resolver.ts confirma que
EffectiveSignature no transporta motion/color/presence). Es defendible y el
propio libro lo autoriza (cap. 36 §1: «La gramática no obliga a una implementación
concreta. Obliga a no confundir funciones perceptivas distintas»). La justificación
arquitectónica —que Sema no debe conocer DOM/CSS y que Eidos es el único dueño visual—
es coherente con las reglas duras de active_architecture.md §7.
El problema no es la decisión; es la costura que deja sin sellar: la partición crea
tres versiones distintas de «cuántos canales hay» que conviven en el repo y se
contradicen (ver T3 / hallazgo X1). Un nuevo contribuidor que lea CLAUDE.md
(«5 canonical channels: motion, sound, color, presence, haptic») construirá un modelo
mental directamente falso respecto al código (SemaChannelSignatures sólo declara
sound + haptic). Recomendación: fijar una sola narrativa canónica —«el libro
define 8 canales de expresión; el framework los reparte: Sema ejecuta 2 (sound+haptic)
- proyecta el meta-canal visual; Eidos materializa los 5 visuales desde CSS»— y
propagarla a
CLAUDE.md,sema/README.mdy los headers deengine.ts.
2. Hallazgos por capa (con evidencia file:line)
Severidad: CRÍTICA (rompe el sistema) · ALTA (viola contrato, efecto real) · MEDIA (deuda arquitectónica con consecuencia) · BAJA (cosmético / dead code).
2.1 Morfo — contrato declarativo
Invariantes verificadas limpias: TS declarativo puro (cero $state/$effect/svelte/
imports de $adom/$soma/engine sema — sólo imports de tipos de sema, permitido);
no crea servicios; picker-shell correctamente en morfo/internal/; expression
cubierto; selectores construidos vía builders (cero strings [data- en runtime).
[M1 · ALTA] Eidos estiliza 3 atributos no declarados en el morfo de Words (drift cross-layer).
morfo:check falla de forma estable: words.toolbar: undeclared attr "data-words-menubar".
- Emisión:
src/uix/eidos/components/words/words-menubar.svelte:173(data-words-menubar),:175/:201/:219(data-words-menubar-group),:283(data-words-menubar-spacer). - Consumo CSS:
src/uix/eidos/components/words/words.css:91,107,114,117,122. - El part
Toolbar(src/uix/morfo/components/words.ts:430-453) sólo declaradata-orientation+data-disabled; no existe el partmenubarni su data. - Por qué viola: Eidos sólo puede seleccionar contra atributos respaldados por el morfo (el «conjunto cerrado de selectores»). Tres selectores quedan sin contrato → drift silencioso si el morfo renombra.
- Fix: declarar
menubar/menubar-group/menubar-spacercomo partskind:'private'en el morfo, o renombrar adata-_*privados (que el morfo excluye legítimamente).
[M2 · MEDIA] button.ts declara states: ['idle','loading'] huérfano que contradice su propio comentario doctrinal.
src/uix/morfo/components/button.ts:99states: ['idle', 'loading'], justo bajo un bloque de comentario (:84-98) que afirma quedata-state«se eliminó por completo» y «'idle' no es un valor». NingúnstateRef/state-equals/valuesreferencia esos estados. El campostates[]existe para respaldarstateRef— aquí no respalda nada.- Fix: borrar la línea 99.
[M3 · BAJA] words.ts texts usa claves camelCase.
src/uix/morfo/components/words.ts:21-24:bubbleMenu,slashMenu,linkEditor,findReplace. Son claves del maptexts(no el namespace de traducción); los idlangref resueltos sí son kebab (components.words.bubble-menu), así que el namespace está limpio. La preocupación del audit previo (camelCase en el namespace) está resuelta; queda sólo inconsistencia estilística de naming de claves.
Resuelto desde el audit 2026-05-27: el drift de
avatar/color-picker/date-picker/dropdown-menu/table/tree-grid(data-state="idle",data-color,data-kind="date") ya pasamorfo:check. Verificado.
2.2 Sema — vocabulario + canales perceptivos
Invariantes verificadas limpias: el resolver emite sólo hold/sound/haptic
(cero motion/color/presence — confirma «sema sin slices visuales»); 8 familias
consistentes entre map/policy/verbs/types; cascade selectors 100% vía semaSelector
(cero strings hand-written); las reglas de cascade no pisan pitch/gain/contour
(preservan la firma per-intent); delegate con canales activos vacíos; engine es
DOM-free (proyección inyectada vía SignalProjector); SemaChannelSignatures es
interface mergeable (registro abierto).
[S1 · MEDIA] HapticChannel alcanza matchMedia/navigator globales en vez del puerto ActiveDom inyectado.
src/uix/sema/chans/haptic.ts:62-63y:75:this.matchMediaFn ?? (typeof matchMedia === 'function' ? matchMedia.bind(globalThis) …).- A diferencia de
VisualChannel(recibedom/projector) ySoundChannel(recibeSoundChannelDom),HapticChannelse construye (active-uix.svelte.ts:117-121,engine.ts:161-165) sin puerto DOM, y cae almatchMediaglobal. Es incorrecto en iframe/popup/happy-dom (justo lo queCLAUDE.mdprohíbe con la reglauix.dom.getWindow(node)), y duplica la lógica reduced-motion que ya existe canónicamente comoActiveDom.prefersReducedMotion(arts/adom/reduced-motion.svelte.ts). - Fix: inyectar la query reduced-motion del active-dom en
HapticChannel, igual queSoundChannelDom. (El acceso anavigator.vibratesí es legítimo: es el backend del canal, no una dependencia dewindow.)
[S2 · BAJA] Dead export SemaRuntimeChannelId. src/uix/sema/channels.ts:73
(re-exportado en exports.ts:82), cero consumidores en src/. Cablear o borrar.
[S3 · BAJA] Dead import/const SEMA_VALENCED_FAMILY_LIST. sema-map.ts:307,
importado en resolver.ts:36 pero nunca usado en el cuerpo; la lista funcional vive en
event.ts como SEMA_VALENCED_FAMILIES. Es un unused-import esperando aflorar en
npm run check. Borrar import + const huérfana.
2.3 Soma — comportamiento headless
Invariantes verificadas limpias en todo el árbol (~60 componentes): cero traversal
crudo del morfo (todos vía runtime/compileMorfo); cero setAttribute/dataset/
style.setProperty de attrs de estado en providers (todo fluye por dom.apply); cero
$libs/dom directo; cero data-soma-*; providers refieren al padre como provider
(nunca root); runtime no expone la superficie prohibida; sólo core/soma.svelte.ts
importa $active-uix (la frontera sancionada).
[SO1 · ALTA] Carousel construye new ResizeObserver crudo, saltando ActiveDom.
src/uix/soma/components/carousel/carousel-provider.svelte.ts:132:const ro = new ResizeObserver(() => {…}); ro.observe(vp).- Es el único observer crudo en soma no-test. Salta
this.soma.dom.observeResize(quefeed-provider.svelte.ts:444usa correctamente) y el wrapperResizeObserver$(que el propio fichero ya tiene a mano).this.soma.domestá en scope (usado en:111). No es iframe/popup/happy-dom-safe; lifecycle no trackeado por ActiveDom. - Fix:
const cleanup = this.soma.dom.observeResize(vp, () => {…}); return cleanup.
[SO2 · MEDIA] Carousel decide visualidad: curva + duración de animación hardcodeadas.
src/uix/soma/components/carousel/carousel-provider.svelte.ts:550:transition: …isDragging ? 'none' : 'transform 300ms ease-out'.- Soma horneando
300ms+ease-outviola «no transitions, no motion CSS decisions» (active_architecture.md §7;SOMA_ARCHITECTURE.md §2). El'none'durante drag es comportamiento legítimo (feedback inmediato); la rama no-drag es decisión visual que pertenece a la recipe eidos reaccionando a[data-carousel-item-group]:not([data-dragging]). Comparar con drawer (:1064/:1067) que sólo emite'none'en drag y deja la transición estilada a eidos — esa es la frontera correcta. - Fix: emitir sólo
transition: 'none'mientras drag; eliminar la rama else.
[SO3 · BAJA · zona gris, requiere sign-off] Color-picker emite gradientes/hex literales.
color-picker-provider.svelte.ts:971-996(channelGradient),:1047(checkerboard),:750/:842(hsl(...)). En apariencia es soma decidiendo color, pero estos gradientes son la representación funcional del dato seleccionable (el rail de hue debe mostrar el espectro real; cambian por píxel con el valor vivo) — no son decisiones de tema/marca y no pueden expresarse como tokens estáticos. Clasificable como acceptable / data-viz, pero merece firma de un revisor por ser el único sitio donde soma emite color literal. El patrón--cp-current-colorCSS-var (:536/:588) sí es el correcto.
2.4 Eidos — capa visual
Invariantes verificadas limpias: cero imports de internals soma o engine sema (todos
los $soma/* son namespaces públicos); componentes no importan getActiveUix/
$active-uix (sólo active-eidos.svelte.ts, que ES el runtime root); token naming
limpio (cero --eidos-/--air-/--terra-/--soma-); EIDOS_VARIANTS como única
fuente de verdad con uniones derivadas; conditional children para fallback soma
correcto; toggle-group structural identity aterrizado (cero duplicación de derivaciones);
tokens portaled usan globales.
[E1 · ALTA · bug funcional, superviviente de 2 auditorías] navigation-menu indicador: selector muerto [data-state='visible'].
src/uix/eidos/components/navigation-menu/navigation-menu.css:300.- El morfo declara el part
indicatorcondata-state ∈ {open, closed}(morfo/components/navigation-menu.ts:150-151); soma escribe exactamente eso (navigation-menu-provider.svelte.ts:769:'data-state': this.isOpen ? 'open' : 'closed'). El CSS reacciona a'visible', valor nunca emitido. La regla base poneopacity:0(:293) y sólo[data-state='visible']lo sube a1→ el subrayado del trigger activo es permanentemente invisible. No es drift cosmético: es un bug de render. - Único selector
invaliddel lint. Flaggeado ya enAUDIT_REPORT_2026-05-27.md. Sigue sin arreglar. - Fix (1 línea):
[data-navigation-menu-indicator][data-state='open']. Sin tocar morfo/soma.
[E2 · BAJA] use-canvas.svelte.ts instancia new ResizeObserver crudo.
src/uix/eidos/lib/canvas-text/use-canvas.svelte.ts:46,55,59. Consumido pors-text+s-text-virtual-list, ambos con handleeidosdisponible. Contrasta contabs-indicator.svelte:75,80que enruta todo poreidos.dom.observeResize. No iframe-safe, lifecycle no trackeado, no mockable.- Fix: pasar
ActiveDomauseContainerWidth(getEl, dom)y usardom.observeResize.
[E3 · BAJA · dev-track Words] 3 fallos de test eidos, todos en words:
words.svelte:15,50usaonMount(lifecycle legacy) → fallacomponent-api-contract.test.ts. Justificado in-code (guard SSR-hidratación porcrypto.randomUUID()), pero rompe el guard.words-inspector.svelte:783tiene bloque<style>scoped vacío (workaround del import graph) → mismo test.recipe-css-contract.test.ts:192: tokens de recipe words huérfanos/no consumidos (--words-rail-border,--words-selection-color,--words-heading-font-size,--words-status-*).
[E4 · INFO] codex_audit.md es el único .md RFC/audit sin banner de estado resuelto.
- Los demás (
COLOR_MODEL_RFC.md,SCALING_RFC.md,THEMING_AUDIT_2026-05-27/06-01.md) tienen banner explícito («RESUELTO»/«IMPLEMENTADO»/«HISTÓRICO»).codex_audit.md(388 líneas, 2026-05-23) aún dice «el resultado no puede considerarse cerrado». Decidir archivo o banner.
3. Composition root + servicios (active-uix / active-app)
Las 8 invariantes del composition root se sostienen: sólo los roots crean servicios;
frontend totalmente retirado (cero refs vivas — sólo la lista forbidden de
contracts.ts y el assert del test); la superficie prohibida (semantic/soma/eidos/
lang/settings/presentation/somaPortalTo) no aparece; ActiveUix no auto-proyecta
prefs al DOM (testeado activamente); split de ownership de proyección correcto; attach
exige langs+dom y falla temprano; langs←prefs.language sólo en standalone; dispose
respeta ownsApp.
[A1 · MEDIA] defineUixServices nunca registra motion, pero attachActiveUix consume app.motion → el fallback es SIEMPRE la regla, nunca la excepción.
src/uix/active-uix/services.ts:50-81(elPickomitemotion; no hayservices.motion = defineEngineMotion()) vsactive-uix.svelte.ts:163-165(leeapp.motion, crea fallback conownsMotion=true).- Como el slice canónico nunca ofrece
motion, toda app compuesta vía UIX carece deapp.motion, así que active-uix siempre crea el fallback — el fallback documentado como excepción (:162«use the app's engine if it declared one») nunca es excepción.defineEngineMotion()existe y es exactamente para esto. Motion es servicio first-class de UIX (CLAUDE.md: «exposed asuix.motion, consumed by BOTH soma and eidos»). - Fix: añadir
if (options.dom !== false) services.motion = defineEngineMotion()adefineUixServices. Entoncesapp.motiones real en attach y el fallback pasa a ser el edge case que dice ser.
[A2 · MEDIA] El contrato ejecutable infra-describe la superficie pública: faltan motion y announce.
src/uix/contracts.ts:17-31(ActiveUixServiceContract) y:118-131(publicSurface).ActiveUixexpone públicamentemotion: EngineMotion(types.ts:112, impl:379) yannounce(...)(types.ts:149, impl:411). Ninguno aparece en el contrato, enpublicSurface, ni enforbiddenPublicSurface. Consecuencia: el extends-checkUIX_TYPE_CONTRACTS.activeUix(:195) no pineamotion/announce(pueden renombrarse/eliminarse sin fallo de tipos) y el loop runtime del test (contracts.test.ts:216-218) no los verifica. El fichero se anuncia como «el contrato ejecutable / fuente de verdad», pero dos miembros públicos reales se le escapan.- Fix: añadir
readonly motion: EngineMotion+announce(...)aActiveUixServiceContracty'motion'/'announce'apublicSurface.
[A3 · BAJA] Composition root alcanza $libs/dom saltando $adom. (Ver T1.)
active-uix.svelte.ts:32-37 importa BREAKPOINTS_DEFAULT, resolveResponsiveProp,
Breakpoint, ResponsiveProp de $libs/dom, mientras la línea 29 importa
createActiveDom/ActiveDom de $adom. $adom re-exporta todo $libs/dom. Inconsistencia
gratuita en el mismo fichero. Fix: importar los cuatro de $adom.
[A4 · BAJA] dispose() — comentario de orden obsoleto. active-uix.svelte.ts:481-482
documenta «format → events → dom → langs → prefs → timers → bus → logger» pero el
teardown real (:484-496) también dispone motion, disabledDom, clipboard.
Comportamiento correcto; comentario incompleto.
4. Capas de soporte (arts / libs / svrs)
Sanas. Verificado limpio: cero deps de valor libs→arts/uix/svrs (un lib no importa
hacia arriba); cero UI en svrs; todos los alias resuelven contra vite.config.ts
(en sync con svelte.config.js); cero $lib singular; $frontend no resucita; sin
fachadas hollow (cada par art/lib añade runtime Engine*/Active* real); motion
correctamente en arts/motion consumido por soma Y eidos (disuelve el acoplamiento
soma→eidos). libs/forms + libs/datagrid usan runas pero con cero deps de arts —
extensión consistente de la excepción documentada libs/reactive.
[SU1 · MEDIA] $libs/dom importado directo en varios sitios fuera del allow-list (T1).
Sólo arts/adom/* y los tests de $libs/dom pueden importarlo directo; el resto consume
$adom. Fuera de la lista:
src/arts/prefs/dom-projection.ts:3(DomAttrValue) — y el mismo fichero ya importaActiveDomde$adomen:1(inconsistencia interna).src/uix/active-uix/active-uix.svelte.ts:32-37(valor:BREAKPOINTS_DEFAULT,resolveResponsiveProp) — el más concreto (import de valor, no type-only).src/uix/sema/{engine.ts:25, stamp.ts:1, define-engine-semantic.ts:25, projection/dom.ts:26, chans/visual.ts:4}(type-onlyDomApplier/DomAttrValue).src/uix/sema/{engine.test.ts:9, emit.test.ts:6, projection/dom.test.ts:13}(tests de sema, no de$libs/dom).- Fix: repuntar todos a
$adom. Mayoría type-only (inocuo en runtime) pero drift de contrato de superficie; el deactive-uix(valor) es el prioritario.
[SU2 · BAJA] Inversión de capas sólo-en-tests. libs/prefs/test/{resolve-prefs,validate-intent}.test.ts
importan booleanDimension/enumDimension/localeDimension de $prefs (el arte) —
forman el ciclo libs/prefs/test → arts/prefs → libs/prefs. libs/logger/test/diagnostics.test.ts:8
importa SILENT_LOGGER de $logger. No es violación de build (test-scope), pero un lib
puro no debería ejercitar su conducta alcanzando hacia su arte consumidor. Fix: mover
los fixtures mínimos a libs/, o reubicar los tests al arte que posee los símbolos.
5. Temas transversales (la imagen de conjunto)
T1 — Erosión de la superficie $adom · MEDIA
El patrón más repetido del repo: ~8 sitios importan $libs/dom directo en lugar de
$adom (CLAUDE.md: «Components and soma NEVER import from $libs/dom directly»).
La mayoría son type-only (DomApplier/DomAttrValue) e inocuos en runtime, pero erosionan
la promesa de que $adom es la única superficie DOM. Concentrados en sema/* (5),
active-uix (1, de valor), arts/prefs (1, con inconsistencia interna), tests sema (3).
Es deuda barata de saldar (un buscar-reemplazar de la fuente del import) y de alto valor
porque mantiene nítida la frontera que sostiene la testabilidad iframe/popup.
T2 — Observers crudos saltando ActiveDom · ALTA (soma) / BAJA (eidos)
La regla «todo DOM por ActiveDom» tiene exactamente 2 fugas en el árbol no-test:
carousel-provider.svelte.ts:132 (SO1, ALTA — única en soma) y use-canvas.svelte.ts:46
(E2, BAJA — eidos lib). Ambas instancian new ResizeObserver con el dom ya en scope.
Pequeñas, locales, de fix mecánico, pero rompen el lifecycle/iframe-safety que la regla
existe para garantizar.
T3 — Drift de documentación vs. código · MEDIA
El código va por delante de la prosa en varios sitios. La doc describe un mundo pre-refactor:
[X1] CLAUDE.md afirma «The framework ships 5 canonical channels (motion, sound,
color, presence, haptic) declared in SemaChannelSignatures» — factualmente falso:
channels.ts:57-60 declara sólo sound + haptic. motion/color/presence se
eliminaron explícitamente (sema-map.ts:6-8). Es la afirmación más engañosa del repo
porque construye un modelo mental erróneo del corazón del sistema (ver §1.2).
[X2] sema/README.md:8 lista 7 familias (falta delegate); :9-12,562-606
documenta el intentPolicy mono-eje deprecado en vez del split intentRequirement+
intentGuidance; types.ts:213-226 JSDoc referencia un SEMA_INTENT_POLICY inexistente.
[X3] engine.ts:9 dice «6 capas de cascade» mientras resolver.ts implementa 5 y
el README dice «5 capas». Numeración inconsistente entre 3 fuentes.
[X4] CLAUDE.md:42 (alias table) lista el retirado $frontend, escribe $lang
(debería ser $langs) y omite $clipboard. Los alias vite↔svelte sí están en sync; la
tabla de doc está stale.
[X5] ~14 README/.md describen aún el shape pre-polymorphic de Dialog/Drawer/Popover
(catalogados en AUDIT_REPORT_2026-05-27.md). Prosa, no runtime.
[X6] CLAUDE.md (nota de excepción de runas) debería reconocer libs/forms +
libs/datagrid como libs reactive-primitive sancionados, junto a libs/reactive.
T4 — Completitud del contrato ejecutable · MEDIA
contracts.ts se anuncia como fuente de verdad pero no pinea toda la superficie
pública de ActiveUix (faltan motion, announce — A2). Un contrato ejecutable que
no cubre miembros públicos reales da falsa seguridad: el extends-check pasa pero el drift
de esos miembros no se detecta.
T5 — Editor Words (dev-track) · MEDIA, en curso
Words es el mayor cluster individual de fallos: el único fallo estable de
morfo:check (M1), el único de contracts.test.ts (data-words-* no respaldados), y los
3 fallos de vitest src/uix/eidos (E3). Es WIP conocido y declarado del usuario, y opera
deliberadamente fuera del contrato morfo (contenteditable con contrato DOM propio
data-words-*). No es regresión — pero es el techo de calidad pendiente del repo, y
conviene una decisión doctrinal explícita: o se declaran los data-words-* como parts
privados del morfo (sellando M1 + el fallo de contracts.test), o se documenta formalmente
que Words es un sub-sistema con contrato propio exento.
6. Backlog de remediación priorizado
Orden por (severidad × coste-de-fix inverso). Ninguno bloquea el framework.
| # | Acción | Sev. | Coste | Tipo |
|---|---|---|---|---|
| 1 | navigation-menu.css:300 'visible'→'open' (bug de indicador invisible + limpia el único lint inválido) |
ALTA | 1 línea | bug |
| 2 | Carousel: new ResizeObserver→dom.observeResize (SO1) |
ALTA | ~3 líneas | contrato |
| 3 | defineUixServices: registrar motion (A1 — wiring gap real) |
MEDIA | ~3 líneas | wiring |
| 4 | contracts.ts: pinear motion + announce (A2) |
MEDIA | ~6 líneas | contrato |
| 5 | Carousel: quitar 'transform 300ms ease-out' hardcodeado (SO2) |
MEDIA | ~2 líneas + recipe eidos | contrato |
| 6 | HapticChannel: inyectar puerto reduced-motion de ActiveDom (S1) |
MEDIA | ~10 líneas | contrato |
| 7 | Words: declarar data-words-menubar* como parts privados o documentar exención (M1 + T5) |
MEDIA | decisión doctrinal | drift |
| 8 | T1: repuntar imports $libs/dom→$adom (~8 sitios; empezar por active-uix:32 valor + arts/prefs:3) |
MEDIA | buscar-reemplazar | superficie |
| 9 | T3/X1: corregir CLAUDE.md «5 canonical channels» + fijar narrativa única de canales (§1.2) |
MEDIA | doc | drift |
| 10 | use-canvas: enrutar ResizeObserver por eidos.dom (E2) |
BAJA | ~5 líneas | contrato |
| 11 | button.ts:99: borrar states:['idle','loading'] huérfano (M2) |
BAJA | 1 línea | dead code |
| 12 | Sema: borrar dead exports SemaRuntimeChannelId + SEMA_VALENCED_FAMILY_LIST (S2/S3) |
BAJA | borrado | dead code |
| 13 | T3/X2-X4: actualizar sema/README.md (8 familias, split intent), numeración capas, alias table |
BAJA | doc | drift |
| 14 | SU2: resolver inversión test-only en libs/{prefs,logger}/test |
BAJA | mover fixtures | layering |
| 15 | E4/E3: banner de estado a codex_audit.md; cerrar deuda de tests Words |
BAJA | doc/WIP | housekeeping |
7. Conclusión arquitectónica
UIX cumple su tesis. Las cuatro capas aceptan sus límites —el riesgo §11.3 del
propio active_architecture.md («invasión de responsabilidades») está, en la práctica,
contenido: Sema no sabe de DOM, Soma no decide CSS, Eidos no mira internals, Morfo no
ejecuta. Las dos micro-invasiones encontradas (carousel decidiendo motion; haptic
alcanzando matchMedia global) son excepciones puntuales y locales, no erosión
sistémica de la doctrina.
La fidelidad al canon del libro es el activo más fuerte y el más frágil a la vez: está
anclada a tipos (la unión discriminada deriva de SEMA_FAMILY_POLICY; los verbos
hard-failean en lint), lo que la hace robusta; pero la prosa que la explica ha
divergido del código en puntos clave (familias, canales, política de intent). El mayor
riesgo de coherencia futura no es que el código se desvíe —los tipos lo impiden— sino que
un contribuidor confíe en una documentación que ya no es verdad. La inversión de mayor
retorno de esta auditoría es saldar el drift de documentación (T3), no el de código.
El sistema está listo para seguir creciendo. Los 15 ítems del backlog son quality-of-life; el framework, en su forma actual, es coherente con su arquitectura declarada y con el libro que lo funda.
Auditoría realizada con análisis estático (grep/lint/tsc), ejecución de la suite de
tests por capa, y lectura íntegra del manuscrito canónico. Evidencia file:line
verificada. Las afirmaciones de «limpio» derivan de greps exhaustivos del árbol
(excluyendo web/routes), no de muestreo.