You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/audit-opus-4-6-26.md

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-uix Alcance: src/arts, src/libs, src/svrs, src/uix/{morfo,sema,soma,eidos,active-uix}, src/arts/active-app, más el contrato ejecutable src/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ía GUIA_IMPLEMENTACION_SEMAUIX.md, LIBRO_VARIACIONES_Y_EXTENSIONES.md, active_architecture.md y contracts.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-canal visual que proyecta data-event-* durante el hold.
  • Eidos materializa motion / presencia / profundidad / forma / color reaccionando por CSS a esos data-event-* y a los data-state/data-intent del 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.md y los headers de engine.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 declara data-orientation + data-disabled; no existe el part menubar ni 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-spacer como parts kind:'private' en el morfo, o renombrar a data-_* 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:99 states: ['idle', 'loading'], justo bajo un bloque de comentario (:84-98) que afirma que data-state «se eliminó por completo» y «'idle' no es un valor». Ningún stateRef/state-equals/values referencia esos estados. El campo states[] existe para respaldar stateRef — 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 map texts (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 pasa morfo: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-63 y :75: this.matchMediaFn ?? (typeof matchMedia === 'function' ? matchMedia.bind(globalThis) …).
  • A diferencia de VisualChannel (recibe dom/projector) y SoundChannel (recibe SoundChannelDom), HapticChannel se construye (active-uix.svelte.ts:117-121, engine.ts:161-165) sin puerto DOM, y cae al matchMedia global. Es incorrecto en iframe/popup/happy-dom (justo lo que CLAUDE.md prohíbe con la regla uix.dom.getWindow(node)), y duplica la lógica reduced-motion que ya existe canónicamente como ActiveDom.prefersReducedMotion (arts/adom/reduced-motion.svelte.ts).
  • Fix: inyectar la query reduced-motion del active-dom en HapticChannel, igual que SoundChannelDom. (El acceso a navigator.vibrate sí es legítimo: es el backend del canal, no una dependencia de window.)

[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 (que feed-provider.svelte.ts:444 usa correctamente) y el wrapper ResizeObserver$ (que el propio fichero ya tiene a mano). this.soma.dom está 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-out viola «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-color CSS-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 indicator con data-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 pone opacity:0 (:293) y sólo [data-state='visible'] lo sube a 1 → el subrayado del trigger activo es permanentemente invisible. No es drift cosmético: es un bug de render.
  • Único selector invalid del lint. Flaggeado ya en AUDIT_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 por s-text + s-text-virtual-list, ambos con handle eidos disponible. Contrasta con tabs-indicator.svelte:75,80 que enruta todo por eidos.dom.observeResize. No iframe-safe, lifecycle no trackeado, no mockable.
  • Fix: pasar ActiveDom a useContainerWidth(getEl, dom) y usar dom.observeResize.

[E3 · BAJA · dev-track Words] 3 fallos de test eidos, todos en words:

  • words.svelte:15,50 usa onMount (lifecycle legacy) → falla component-api-contract.test.ts. Justificado in-code (guard SSR-hidratación por crypto.randomUUID()), pero rompe el guard.
  • words-inspector.svelte:783 tiene 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 (el Pick omite motion; no hay services.motion = defineEngineMotion()) vs active-uix.svelte.ts:163-165 (lee app.motion, crea fallback con ownsMotion=true).
  • Como el slice canónico nunca ofrece motion, toda app compuesta vía UIX carece de app.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 as uix.motion, consumed by BOTH soma and eidos»).
  • Fix: añadir if (options.dom !== false) services.motion = defineEngineMotion() a defineUixServices. Entonces app.motion es 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).
  • ActiveUix expone públicamente motion: EngineMotion (types.ts:112, impl :379) y announce(...) (types.ts:149, impl :411). Ninguno aparece en el contrato, en publicSurface, ni en forbiddenPublicSurface. Consecuencia: el extends-check UIX_TYPE_CONTRACTS.activeUix (:195) no pinea motion/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(...) a ActiveUixServiceContract y 'motion'/'announce' a publicSurface.

[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 importa ActiveDom de $adom en :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-only DomApplier/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 de active-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.

Powered by TurnKey Linux.