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/fable-eidos-audit.md

54 KiB

Auditoría Fable — Eidos y el sistema de theming

Fecha: 2026-07-01 · Rama: alpha-0.1-sec-dom · Auditor: Claude Fable 5 Alcance: la capa eidos completa — foundation generada, colores, superficies, tipografía, sizes/espaciado/densidad/scaling, elevación (shadow/depth), forma, motion, focus/estado/touch, el theme builder runtime (apply*) y su documentación (THEMING.md, TSC.md, THEMING_GUIDE, components/README, eidos-motion.md). Método: lectura directa de THEMING.md (secciones canónicas + §20–§38), lib/render-css.ts (parcial), lib/recipes/base.ts (4.459 líneas, por barridos), lib/themes/base.ts, lib/primitives/{static,typography}.ts, lib/build-scheme.ts (completo), archetypes.css (completo), events.css (completo), active-eidos.svelte.ts (auditado en fable_audit.md), más barridos cuantitativos por grep sobre los 135 CSS de componentes y el foundation generado. Cada cifra de este informe sale de un grep reproducible, no de la documentación. Complementa (no repite) los hallazgos E1–E6 de fable_audit.md.


1. Resumen ejecutivo — la hipótesis se confirma, con matiz

La sospecha del encargo era: "hay incoherencias, no un esquema coherente; se ha ido desviando con cada modificación y conceptualmente no hay una guía con la que se implementen los componentes". El diagnóstico tras la evidencia:

El núcleo está bien y está protegido. El modelo de color (33 escalas → 9 roles → slots → palette por componente), el TSC, la tokenización de ejes crudos (blur, border, z-index de overlays, tracking) y los guards mecánicos que existen (recipe-css-contract, guard tipográfico anti-literal, guard de z crudo, lint de variants) funcionan: en 135 ficheros CSS de componentes hay 0 usos de --focus-ring legacy, 1 solo fichero con hex crudos, 0 blur literales, 5 duraciones ms sueltas, 1 uso directo de --scale-*. Donde hay guard, no hay deriva.

La deriva es real, pero no está en los tokens: está en los SISTEMAS. Eidos es hoy una federación de ~14 subsistemas canónicos añadidos por sprints fechados (densidad, scaling, depth, shape, structure, focus de dos anillos, state-layer, touch-target, list-surface, gap flotante, motion de dos momentos, tipografía 1:1, opacidad unificada, gradientes) donde cada uno declara ser "el canon" y ninguno alcanzó adopción completa:

Sistema canónico (fecha) Promesa Adopción real medida
State-layer --state-* (§38, 06-28) sustituir ~168 hovers bespoke 21/135 CSS lo usan; 22 tokens *-bg-hover siguen declarados en recipes; el propio archetypes.css mantiene el idiom viejo en close:hover (opacity-dim 0.85)
Depth channel --depth-* (§29, 06-05) "canal unificado, no tres sistemas sueltos" 7 CSS + 6 refs en recipes componen el canal; el resto sigue en la escala --shadow-* directa; adopción por data-depth = "opción futura" → dos sistemas conviven (corrección 2026-07-02: los box-shadow de componentes SÍ están tokenizados — R-4.1 = 0 literales; el hallazgo es la partición escala-vs-canal, no literales)
Bundle --size-{k}-* (§5) "map global coordinado" 0 consumidores (confirmado hoy por grep; el doc lo admite: "huérfano")
Opacidad unificada (§35, 06-15) "~10 valores de disabled → 1" 28 reglas usan --opacity-disabled (0.4)… y archetypes.css hardcodea 0.5 tres veces, contradiciendo su propio token (corrección 2026-07-02: fuera de @keyframes solo quedan 2 componentes con literal — el grueso de los 20 matches iniciales eran fades de keyframes, exentos)
list-surface (06-21) ritmo único de menú/lista 4/135 adoptantes; select/combobox "recipe-level pending"
Motion 2 momentos (F1–F7) keyframes/firmas en config, themeables 49 @keyframes locales en 25 CSS fuera del canal; 14 componentes usan presets (data-animation-style — corrección 2026-07-02; dropdown/context-menu están en AMBAS listas: presets Y keyframe local). El repositorio (verificado): ~40 keyframes + 12 firmas + 13 presets de estado con bySide/reduce + 4 loops — cubre lo que los 15 del backlog re-implementan a mano (slide-fade, collapse, shared-axis-x)
Alpha scales a1..a12 (README) tintas translúcidas para overlays/rings/hovers 0 consumos directos de --primitive-*-aN/--scale-*-aN en recipes+componentes; solo a2/a3 viven indirectamente vía los slots surface/surface-hover → ~500 tokens emitidos sin consumidor
Slots de color emitidos 14 slots × rol text-strong, separator, border-hover = 0 usos; hover = 8, active = 1 (sobre 36 emisiones cada uno)

Y la causa raíz es exactamente la que el encargo intuía: no existe un contrato por componente que diga "una recipe DEBE consumir estos sistemas" — el component-audit mecánico cubre 10 reglas (R-1.x a R-3.x) anteriores a junio y ninguna de las nuevas capas canónicas (state-layer, focus, touch, depth, list-surface). Cada sistema nuevo se desplegó como sprint manual (pilot + N componentes) que se detuvo dejando una sección "Pendiente", y THEMING.md creció como changelog fechado disfrazado de referencia (§20–§38 son sprints; §5 contiene dos doctrinas revocadas inline; §13–§14 están "superseded" pero siguen dentro; §1 dice "5 layers de CSS" y §2 dice "6 capas").

Veredicto: coherencia del foundation 8.5/10 · coherencia de sistemas transversales 5/10 · theme builder 7/10 (bugs concretos abajo) · documentación como guía de implementación 4/10. No hace falta rediseñar: hace falta cerrar adopciones, canonizar el contrato de recipe como regla mecánica, y partir THEMING.md.


2. El patrón de deriva (diagnóstico estructural)

Los cinco mecanismos que producen la incoherencia, todos observables en el repo:

  1. Canon por acreción, no por sustitución. Cada sprint añade un sistema "canónico" sin retirar el anterior: elevación tiene hoy --shadow-{1..6} + aliases semánticos + --depth-{plane}-* conviviendo. Feedback neutro tiene state-layer + surface-swap + opacity-dim + 22 tokens bespoke. Translucidez tiene alpha-scales + color-mix(...%, transparent) + --opacity-*. Varios idiomas por concepto es la norma, no la excepción.
  2. Rollouts que se detienen en el "Pendiente". §38 ("Pendiente: poda de bg-hover + tabs + color-picker"), §29 ("adopción plena vía data-depth queda como opción futura"), §5 ("el refactor a consumir el bundle queda como follow-up"), list-surface ("select/ combobox pending"). El parking-lot es sistemático y nada lo re-visita.
  3. El doc de referencia es un changelog. THEMING.md §20–§38 son informes de sprint con fechas, incidentes (¡el de PowerShell!), commits y doctrinas revocadas conservadas inline. Un implementador nuevo no puede extraer de ahí "qué debe hacer mi recipe hoy" sin arqueología.
  4. Guard-coverage asimétrico. Donde hay guard mecánico (tipografía literal, z crudo, variants, recipe-contract) la deriva es ~0. Donde el canon es prosa (state-layer, depth, opacidad en componentes, padding axes) la deriva es del 30–85%. La lección está en el propio repo y no se aplicó a los sistemas de junio.
  5. Sin plantilla de recipe. No hay un esqueleto canónico de recipe (qué ejes declara, con qué nombres, qué sistemas consume). Resultado medible: los ejes de padding se nombran de dos formas al 50% (padding-x/y ×18 vs padding-inline/block ×18), las alturas de tres (height-* ×50, control-height-* ×15, trigger-height-* ×11), y el vocabulario de slots dimensionales no existe (el §6 solo fija slots de color).

3. Hallazgos por subsistema

Severidades: P0 bug visible/corrección · P1 incoherencia estructural · P2 mejora clara · P3 higiene.

3.1 Color — núcleo sólido, periferia con huérfanos y docs desalineados

Lo bueno: la cadena escala→primitive→slot→palette se respeta en la práctica (609 refs --color-{role}-{slot} en recipes, 1 solo bypass a --scale-*, 0 a --primitive-* en CSS de componentes). El slot vocabulary REAL usado es coherente (border/solid/text/track/ element/solid-hover/contrast dominan).

  • C1 · P1 — Tres listas de slots, ninguna coincide. §3 declara "13 slots" (incluye bg2); el generador emite 14 (sin bg2, con surface/surface-hover); §6 regla 7 fija un "vocabulario fijo" de 9 que no incluye surface, surface-hover, separator, border-hover ni text-strong. La fuente ejecutable (DEFAULT_COLOR_ROLE_SLOT_STEPS, render-css.ts:93) no está citada por ninguna de las tres. Acción: una sola tabla generada desde el código.
  • C2 · P1 — Slots emitidos sin consumidor. text-strong, separator, border-hover: 0 usos; hover: 8; active: 1 (cada uno se emite 36 veces en generated). Son bytes del "foundation floor" que el purge nunca podrá quitar (foundation siempre kept). Decidir: consumir (p. ej. separator es exactamente lo que muchos recipes resuelven con border-subtle) o dejar de emitir.
  • C3 · P1 — La tabla de mapeo de §4 miente. Doc: primary→indigo, risk→amber. Código (THEME_BASE_COLOR_ROLES): primary→purple, tertiary→indigo, risk→orange. Tres celdas estables desde hace semanas — el doc canónico de roles describe un tema que ya no existe.
  • C4 · P1 — Tres idiomas de translucidez. (a) alpha-scales aN (emitidas, huérfanas salvo a2/a3 indirectas); (b) color-mix(in srgb, X N%, transparent) — el idiom real dominante (focus ring, state-layer, backdrops); (c) --opacity-*. No hay doctrina de cuándo usar cada uno. La inversión (≈500 tokens alpha) no la usa nadie mientras el código produce las mismas tintas con color-mix ad-hoc.
  • C5 · P3 — natural-time-picker conserva 6 hex crudos (cielos del reloj natural + --_ntp-ink: #ffffff). Los cielos son candidatos legítimos a "físicamente fijos", pero la excepción exige comentario justificativo (doctrina "no intentional sin anotar") y no lo llevan todos.

3.2 Theme builder (buildScheme / applyColorScheme / applyTheme)

  • TB1 · P0 — El scheme deja las alphas viejas. buildScheme solo emite --primitive-{role}-a2/a3 (build-scheme.ts:155-158). El foundation emite a1..a12 por rol. Tras applyColorScheme(seed), a1 y a4..a12 siguen apuntando al color del tema anterior — cualquier consumidor futuro de esas alphas (o un tema externo que las use, están en el contrato público) mezclará el morado viejo con el brand nuevo. Emitir las 12 (la matemática alphaOverBackground ya existe) o retirar a4..a12 del contrato (ver C4).
  • TB2 · P0 — Donores de intent ignoran el tema activo. Con temper > 0, los intents se derivan desde CANONICAL_INTENT_SCALES (build-scheme.ts:184): risk siempre parte de amber, aunque el tema base mapea risk→orange. Consecuencia: al subir el slider de temper, risk cambia de familia de hue (naranja→ámbar) además de temperarse — un salto perceptivo que no es el contrato del knob ("mantiene su hue"). El donor debe salir del role-map del tema activo (getColorRoleScale('risk')), con el canónico como fallback.
  • TB3 · P1 — Background de compositing hardcodeado. #buildSchemeResult usa mode === 'dark' ? '#111111' : '#ffffff' (active-eidos.svelte.ts:845) mientras el fondo real del tema es --color-surface-default = neutral-1 (#fcfcfc light; el dark de gray-1 no es #111111). Las a2/a3 generadas quedan compositadas contra un fondo que no es el del tema — error pequeño en el base, grande en temas tintados (cristal, brand oscuro azulado). Resolver el token vía resolveToken('--color-surface-default').
  • TB4 · P2 — applyTheme no es atómico en coste (ya en fable_audit E1): cada builder se construye dos veces y apply() regenera TODO el static CSS. Para el caso de uso anunciado (sliders del theme builder en vivo) es cuadrático respecto a lo necesario.
  • TB5 · P2 — Literales duplicados: onSolidContrast: '#1c1917' vive como default en build-scheme.ts:106 Y en los dos bloques de semantics de themes/base.ts; el par #ffffff/#1c1917 es una constante de sistema sin nombre.
  • TB6 · P3 — #renderSchemeCss traga errores (fable_audit E3): un seed que rompe en el re-derive de modo hace desaparecer el scheme sin log.

3.3 Superficies y elevación — el canal "unificado" que no unificó

  • E1 · P1 — Dos sistemas de elevación conviven. (a) escala --shadow-{1..6} + aliases semánticos (13 refs --shadow-subtle en recipes…) consumida directamente por la mayoría; (b) canal depth --depth-{plane}-{shadow|halo|blur|translucency} — compuesto solo por ~13 consumidores (popover, dialog, menús, tooltip, card…). §29 declara "canal unificado" — la frase describe la intención, no el estado: la unificación por atributo (data-depth) quedó explícitamente como "opción futura" y el z-index sigue por libre en cada componente (con su banda --z-index-overlay-*, esa sí guardeada). (Corrección 2026-07-02: la versión inicial de este informe contaba "23 box-shadow literales" — la regla R-4.1 implementada demostró que todos llevan var() (patrones ring / tokens de recipe); literales reales = 0. El hallazgo queda reducido a la partición escala-vs-canal y la adopción parcial del halo.)
  • E2 · P2 — El halo/frost es opt-in y casi nadie lo pide. [data-frost] no aparece en los recipes del catálogo (solo demos/tema cristal). Correcto como diseño gated, pero conviene documentar en el contrato de recipe cuándo un overlay DEBE ofrecerlo.
  • E3 · P3 — Ladder de superficies OK (default<raised<muted<overlay en ambos modos, verificado en themes/base.ts) — sin hallazgos.

3.4 Tipografía — diseño de dos anclas correcto, tres fuentes de verdad de facto

El modelo documentado (escala numérica para recipes + named styles para primitivas de contenido, aliases foundation leyendo del named style) es defendible y está razonado. Pero:

  • T1 · P1 — Los named styles re-declaran line-heights que la escala ya fija. sizes.xxl.lineHeight = '1.05' Y styles.h1.lineHeight = '1.05' (typography.ts:65,89). Retocar la escala NO propaga a los headings — dos fuentes para el mismo número, sin guard que las mantenga iguales. Igual en h2/xl (1.1 vs 1.2 — ya divergen), h3/lg (1.2 vs 1.35 — divergen). ¿Intencional (los styles ajustan óptica) o deriva? No está documentado cuál manda.
  • T2 · P1 — Tercer sistema tipográfico en accordion. El doc del 1:1 (§5) reconoce que accordion usa una progresión propia --text-N-size ("sistema de prosa"). Con words/ palabras/chronos excluidos, hay tres vocabularios activos: --font-size-{t-shirt}, --style-{name}-*, --text-{N}-*. El tercero no aparece en la tabla de naming §6.
  • T3 · P2 — semanticTracking es un eje muerto: 6 claves, todas '0' (typography.ts:221-228). Config + tokens emitidos que no expresan nada. Usarlo (caps de menús → --tracking-caps?) o retirarlo.
  • T4 · P2 — Colisión de namespace --leading-*: leading (none/tight/…) y semanticLeading (ui/prose/text/…) emiten al MISMO prefijo desde dos mapas distintos sin validación de claves disjuntas — un semanticLeading.normal pisaría --leading-normal silenciosamente.
  • T5 · P2 — El pendiente del label lg (divergencia 2px entre label segmentado por calc y label por token, §5) sigue abierto — es exactamente el tipo de asimetría que luego se copia a un componente nuevo.
  • T6 · P3 — 12 font-size: Npx crudos en 8 ficheros; la mayoría en tracks excluidos (words, timeline) pero fab, field, password-field, command, date/time-range-field no son excluidos — el guard tipográfico solo cubre TOKENS de recipe, no la propiedad CSS directa. Ampliar el guard o anotar.
  • T7 · P3 — Dos escalas comparten camiseta con rangos distintos: el Size canónico termina en xxl (+ full de layout) y la tipográfica añade xxxl. Documentado, pero es una fuente recurrente de confusión (¿--size-xxl-font-size vs --font-size-xxl?) que el naming no desambigua.

3.5 Sizes, espaciado, densidad, scaling

  • S1 · P1 — El bundle --size-{k}-* sigue con 0 consumidores (verificado hoy). Está "realineado al 1:1" pero nadie lo lee: cada recipe re-declara su mapeo size→fuente/ altura/padding. Es EL síntoma de la falta de plantilla de recipe: la coordinación existe como datos y se re-implementa a mano 90 veces. Decidir de una vez: consumo real (refactor grande pero mecánico) o degradarlo a documentación y dejar de emitirlo.
  • S2 · P1 — Idioma de ejes partido al 50%: padding-x/-y (18 tokens) vs padding-inline/-block (18). Mismo concepto, dos gramáticas, cero doctrina en §6. Con RTL en el roadmap (memoria: "rtl" en reference-grade remaining), los -x/-y son además la mitad equivocada.
  • S3 · P2 — Alturas con tres nombres (height-*, control-height-*, trigger-height-*) para el mismo concepto de "altura del control por size".
  • S4 · P3 — 17 paddings/gaps/margins en px crudos en CSS de componentes (fuera de tokens); pocos, pero sin guard equivalente al tipográfico.
  • S5 — Densidad y scaling: OK. La composición calc(raw × density × scaling) vive en el foundation (render-css:2397) y los recipes heredan vía var(--space-*) — no encontré doble-escalado en componentes. El diseño post-§20/§23 es limpio.

3.6 Estado e interacción (state-layer)

  • I1 · P1 — Adopción 21/135 + 22 tokens huérfanos. El state-layer es la decisión correcta (MD3, theme-adaptive) pero quedó a ~30% del catálogo interactivo; los tokens *-bg-hover sobreviven en recipes (22 declarados, 5 CSS aún los consumen) y la poda + guard están "diferidos por entanglement". Mientras el guard no exista, cada componente nuevo puede volver al idiom viejo sin que nada falle.
  • I2 · P1 — archetypes.css contradice su propio canon: close:hover usa opacity-dim 0.85 — el idiom que §38 declaró retirado para trigger ("el dim atenuaba también el texto; el tinte no"). El archivo que define --state-* mantiene el anti-patrón tres líneas más abajo.
  • I3 · P2 — El selector del highlight de item/option son 10 selectores × 4 :not() repetidos a mano (archetypes.css:208-217). Funciona, pero es la clase de bloque que :is() reduce a 2 líneas legibles y que hoy nadie se atreve a tocar.

3.7 Focus

  • F1 · P1 — Dos mecanismos + una justificación stale. El ring universal de archetypes es box-shadow "porque layout.css pone outline: none !important global" — ese reset vive en web/routes/layout.css (la shell de docs), no en el framework: la arquitectura de a11y del framework quedó condicionada por su demo. En paralelo, §32 migró superficies a outline (HCM/overflow-safe) y quedan 5 componentes box-shadow pendientes de migrar (inventario reference-grade). Resultado: campos con box-shadow de dos anillos (correcto y deliberado), superficies con outline, archetype-fallback con box-shadow, y forced-colors emitiendo outline por encima. El modelo final es razonable — pero nadie lo describe completo en un solo sitio, y el comentario fundacional es falso para el framework standalone.
  • F2 · P3 — Buena praxis verificada: exclusiones de item/option/content del ring universal bien razonadas; :where() discipline del resto de archetypes correcta.

3.8 Opacidad

  • O1 · P1 — El token dice 0.4; el canon transversal usa 0.5. --opacity-disabled = 0.4 (static.ts:294). archetypes.css hardcodea opacity: 0.5 en trigger/thumb/item disabled (3 sitios). La §35 proclama la unificación; el fichero más transversal del sistema no la consume. Fix trivial + guard (mismo patrón que el tipográfico). (Corrección 2026-07-02: en CSS de componentes, fuera de @keyframes, solo quedan 2 con literal (R-4.2) — el conteo inicial de 20 incluía fades de keyframes, que son animación, no estado, y quedan exentos.)

3.9 Motion

  • M1 · P1 — 49 keyframes locales en 25 CSS, fuera del canal. La doctrina (events.css header + eidos-motion §15) dice que firmas y keyframes viven en EidosConfig.motion.{keyframes,signatures} (themeables, generados). Los 49 locales son invisibles para temas y para el motor — no se pueden retunear ni desactivar coherentemente. Habrá funcionales legítimos (spin de spinner), pero no hay clasificación: ni doctrina "cuándo un keyframe puede ser local" ni inventario.
  • M2 · P2 — El cap de reduced-motion solo frena animation-duration (events.css:24,28) — las transiciones de estado (transition:) bajo data-motion='reduce' no se capan en este archivo (THEMING §13 aún muestra una versión con transition-duration). Verificar dónde vive hoy el cap de transiciones o añadirlo.
  • M3 · P3 — Sano: uso de --duration-*/--ease-* amplio (40 CSS), --ease-default domina (100 usos), 5 duraciones crudas restantes con excepciones razonadas en §35.

3.10 Touch-target y list-surface

  • TT1 — Touch-target: bien diseñado (gated por pointer: coarse, doble función del :not(), exclusiones razonadas). Pendiente reconocido: fila etiquetada de checkbox/switch (tarea de Field).
  • LS1 · P2 — list-surface con 4 adoptantes y select/combobox pendientes — otro canon a medio desplegar; mientras tanto item/option cae al fallback font-driven.

3.11 Documentación del theming

  • D1 · P1 — THEMING.md es referencia + changelog fusionados. 2.583 líneas; §20–§38 son sprints fechados con incidentes y commits; §5 conserva DOS doctrinas revocadas narradas inline ("supersede… quedó revocada"); §13–§14 están marcados "superseded" pero ocupan 100 líneas del doc "current"; §21 es un RFC resuelto conservado. La contradicción interna es inmediata: §1 dice "5 layers de CSS", §2 dice "Las 6 capas" (y enumera 6).
  • D2 · P1 — La "guía de implementación por componente" está fragmentada en 6 documentos (THEMING §5/§6 + TSC.md + THEMING_GUIDE + components/README + COMPONENT_COMPLETION_CHECKLIST
    • eidos-motion §13) y NINGUNO enumera los sistemas transversales que una recipe debe consumir (state-layer, focus, elevación, touch, list-surface, 1:1, motion channel). Esa lista hoy solo existe implícita en los "Pendiente" de cada sprint. Es la carencia exacta que el encargo señalaba.
  • D3 · P2 — Secciones "estado actual" congeladas en docs atemporales: eidos/README "Estado actual (2026-05-17)" (wrappers: lista ~20 vs 137 reales), components/README "Estado de la migración (2026-05-20)". Contradicen docs/authoring.md.
  • D4 · P3 — §4 tabla de mapeo stale (ver C3) y §3 "13 slots" vs generador (ver C1).

4. Lista consolidada de bugs y fixes puntuales (accionables ya)

# Sev Qué Dónde Fix
B1 P0 Scheme deja a1/a4..a12 del tema anterior build-scheme.ts:155 emitir las 12 alphas o retirar a4+ del contrato
B2 P0 temper cambia el hue de risk (amber vs orange del tema) build-scheme.ts:184 donor desde el role-map del tema activo
B3 P1 Alpha compositado contra #fff/#111 en vez del surface real active-eidos:845 resolveToken('--color-surface-default')
B4 P1 opacity: 0.5 ×3 en archetypes vs --opacity-disabled 0.4 archetypes.css:54,113,224 usar el token + guard
B5 P1 close:hover opacity-dim contra doctrina state-layer archetypes.css:131 background-image: linear-gradient(var(--state-hover)…)
B6 P1 "5 layers" vs "6 capas" en el mismo doc THEMING §1 vs §2 corregir §1
B7 P1 Tabla roles §4 desalineada del código (primary/risk/tertiary) THEMING §4 regenerar desde THEME_BASE_COLOR_ROLES
B8 P2 reduce-cap sin transition-duration events.css añadir o documentar dónde vive
B9 P2 #1c1917/on-solid duplicado en 3 sitios build-scheme, themes/base ×2 constante nombrada
B10 P3 hex sin anotación en natural-time-picker ntp.css:15-20 comentario "physically-fixed" o tokenizar ink
B11 P3 12 font-size: px fuera del guard (fab/field/command…) varios css ampliar guard R-2.7 a la propiedad

5. Lo que está bien (y hay que proteger al refactorizar)

  • TSC + pipeline de defensas — intacto y ejemplar (ya valorado en fable_audit).
  • El foundation de color — cadena de capas respetada en la práctica; el modelo §25 (paleta rica + roles alias + intents derivados) es superior al de las referencias.
  • Los guards existentes — cada uno tiene deriva ≈0 en su dominio. La plantilla del éxito ya existe; solo falta replicarla.
  • archetypes.css como pieza — la disciplina :where() con las 3 excepciones intencionales documentadas es de las mejores prácticas del repo (pese a I2/B4).
  • Densidad × scaling — composición limpia post-§20/§23, sin doble escalado.
  • Los builders puros (build-* testeados, sin DOM) — la separación matemática/ aplicación es correcta; los bugs TB1–TB3 son de datos, no de arquitectura.
  • Touch-target y focus-exclusions — decisiones a11y razonadas contra referencias (React Spectrum, WCAG) y verificadas.
  • eidos:purge — con resultados medidos honestos.

6. Valoración del sistema de theming frente a los existentes

Fundada en la comparativa propia del repo (THEMING_NOTES.md, que es honesta y sigue vigente) + los hallazgos verificados de esta auditoría. La pregunta que responde: ¿qué vale este sistema de theming puesto al lado de Radix Themes, Panda/Chakra, Mantine, Tailwind v4, shadcn y Material 3?

6.1 La frase que lo resume

Eidos tiene el motor de theming más capaz del campo y la gobernanza de consumo más débil respecto a su propia ambición. Radix Themes es exactamente lo contrario: un motor modesto con gobernanza perfecta.

Radix Themes ofrece ~un tercio de los ejes de Eidos (accent + gray + radius + scaling), pero el 100% de sus componentes consume el 100% de su sistema — no existe "adopción parcial" porque los componentes y el sistema se diseñaron juntos y no hay vía de escape. Eidos ofrece el triple de ejes y sus componentes los consumen al 30–85% según el subsistema. La calidad percibida de un theming no la da el motor sino la uniformidad del consumo — y ahí es donde Eidos pierde hoy contra referencias objetivamente menos capaces.

6.2 Dónde gana (capacidades que nadie más tiene, verificadas)

Capacidad Eidos Mejor alternativa del campo
Scope de tokens como contrato validado (TSC: álgebra + colisión cross-axis + composition) ✅ único Panda valida parcialmente en build; Radix/Mantine/shadcn: nada — el bug del custom-property congelado es invisible para todos ellos
Builders runtime semilla→sistema en 6 ejes (color · type · depth · shape · space · gradient, applyTheme atómico) ✅ único Material 3 lo hace SOLO para color (dynamic color); Radix Themes ni eso (su panel es token-level)
Color science: OKLCH wide-gamut dual-stack default-on, APCA on-solid, alpha por compositing-inverse, temper de intents ✅ Radix ships hex sRGB; M3 usa HCT (comparable en rigor, sin P3 dual-stack en web)
Roles evaluativos del libro (affirm/fulfill/risk/threat/loss ≠ semáforo Bootstrap) ✅ único Todos los demás: success/warning/danger/info
Densidad × scaling ortogonales en runtime ✅ Radix tiene scaling; NINGUNO tiene ambos ejes componiendo
Integración perceptiva (tokens/motion que reaccionan a data-event-* de sema) ✅ único No existe el concepto fuera de este framework
Contrato CSS introspectable + themes CSS-only + persistencia versionada (getCssContract / renderContractCss / envelope) ✅ Panda introspecta en build; Mantine expone el theme object; nadie publica el contrato como CSS vacío para temas externos
a11y del theming: forced-colors, prefers-contrast, touch-target gated por puntero, reduced-motion cross-modal ✅ React Spectrum es el único comparable en touch/contrast; ninguna lib de theming CSS lo integra
Purge con cierre transitivo (13.6 KB gz para 10 componentes) ✅ competitivo Tailwind 10 / Panda 12 — misma liga

Esto no es marketing propio: cada fila de arriba la contrasté contra el código, no contra los docs. Como motor, el sistema está 2–3 años por delante de lo publicado.

6.3 Dónde pierde (contra referencias concretas)

  1. Contra Radix Themes — uniformidad de consumo. Es el hallazgo central de esta auditoría (§1): en Radix no puede existir un componente con 23 sombras literales o un hover que ignora el state-layer, porque no hay superficie donde escribirlo. En Eidos las recipes son CSS plano + un mapa de tokens sin forma: la vía de escape está abierta en cada fichero.
  2. Contra Panda/Chakra v3 — la recipe tipada. Panda tiene defineRecipe / defineSlotRecipe: la forma de una recipe (ejes, variants, slots, compoundVariants) es un TIPO, y desviarse no compila. Eidos tiene el TSC (que valida scope, mejor que Panda) pero no valida la forma: qué ejes existen, cómo se llaman (padding-x vs padding-inline), qué sistemas consume. Es la única dimensión del theming donde Panda es estructuralmente superior hoy — y es exactamente el hueco que la Fase A propone cerrar.
  3. Contra Tailwind/shadcn — coste de entrada. 7 capas de tokens, 2.583 líneas de doc de referencia con secciones revocadas inline, y "cómo hago un componente" repartido en 6 documentos. La comparativa del propio repo lo admite ("Eidos NO es más simple") — correcto, pero la deriva documental (§3.11) lo empeora más allá de lo necesario.
  4. Contra todos — madurez del catálogo de temas. 2 temas shipped (base + cristal) y un builder con 3 bugs P0/P1 en el camino crítico del retint (TB1–TB3). El sistema permite más que ninguno; lo demostrado en producción es menos que cualquiera de las referencias.
  5. Rendimiento del builder en vivo — apply() regenera el foundation entero por cambio de eje (fable_audit E1); ninguna referencia con theme runtime (Mantine, Chakra) paga ese coste por knob.

6.4 Puntuaciones (escala honesta)

Dimensión Nota Ancla
Arquitectura de foundation (capas, roles, escalas, naming de color) 9 Mejor que Radix (3 capas) y Panda; C1–C4 son periferia
Capacidades del motor (TSC + builders 6 ejes + color science) 9.5 Nadie tiene el conjunto
Implementación del motor 7.5 TB1–TB3 (retint incompleto/incoherente) + E1 (perf) descuentan
Disciplina de consumo del catálogo 5 21/135 state-layer · 3 sistemas de elevación · 0 consumidores de --size-* · 49 keyframes fuera del canal
Ergonomía para el autor de componentes 5 Sin plantilla ni recipe tipada; el contrato vive en 6 docs + "pendientes"
Ergonomía para el autor de temas 7.5 Contract introspection + CSS-only + envelope excelentes; solo 2 temas lo validan
a11y del theming 8 forced-colors/contrast/touch/reduced-motion; F1 y B4 descuentan
Documentación como guía 4 Referencia=changelog; contradicciones internas verificadas
Global 7 Motor de referencia mundial · gobernanza por debajo de su propia doctrina

La nota global no es la media: está dominada por la disciplina de consumo, porque es lo que el usuario final del framework ve. Un tema aplicado sobre componentes que bypassean los canales (sombras literales, hovers bespoke, keyframes locales) produce un retint incompleto — y eso convierte la ventaja del motor en una promesa, no en un resultado.


7. ¿Basta con el contrato por componente? (respuesta directa)

Sí, el contrato de recipe es la pieza clave — pero solo si se entiende como tres cosas a la vez, y hay una cuarta parte del problema que el contrato no toca.

  1. El contrato como documento (la "guía conceptual" que falta) es necesario pero, por sí solo, insuficiente. La evidencia está en este mismo repo: los cánones que viven en prosa derivan al 30–85% (state-layer, depth, opacidad); los que tienen guard mecánico derivan ~0% (tipografía-literal, z crudo, variants, TSC). Un RECIPE_CONTRACT.md sin enforcement sería el decimoquinto sistema canónico parcialmente adoptado.

  2. El contrato como guard (component-audit R-4.x — Fase A2) es lo que congela la deriva futura: cada componente nuevo nace obligado. Es barato (el script y los precedentes de regla existen) y es el paso de mayor palanca.

  3. El contrato como tipo es el destino natural y la versión superior: una defineRecipe de eidos con ejes canónicos tipados (dimensiones con nombres fijos, sistemas transversales como flags declarativos, variants contra EIDOS_VARIANTS), igual que Panda — y coherente con la doctrina ya escrita del propio framework ("types over lint": el precedente de semaSelector frente al eidos-lint). Las recipes CSS planas son hoy la última gran superficie no tipada del sistema. No hace falta empezar aquí (es la versión cara), pero el contrato-documento y el contrato-guard deben redactarse de forma que migren limpio a contrato-tipo.

  4. Lo que el contrato NO arregla — y por eso el plan tiene 5 fases y no 1:

    • La deuda ya emitida: los 135 CSS existentes hay que backfillearlos (Fase B); si no, el guard nace en rojo o con una allowlist eterna que lo desactiva de facto.
    • Los bugs del motor (TB1–TB3: alphas parciales, donors que ignoran el tema, background hardcodeado): son del builder, no de las recipes — ningún contrato por componente los toca (Fase C).
    • Las decisiones de inventario (alphas huérfanas, slots muertos, bundle --size-*, semanticTracking cero): son decisiones de sistema que alguien tiene que tomar una vez (Fase D). El contrato las consume, no las decide.
    • La documentación (Fase E): el contrato debe ser LA página que un implementador lee, y eso exige que THEMING.md deje de ser un changelog donde esa página no cabe.

En una línea: el contrato por componente es la llave, pero la cerradura tiene cuatro piezas — contrato ejecutable + backfill + motor corregido + inventario decidido. El orden correcto es A (contrato+guard en WARN) → B (backfill) → guard a FAIL, con C y D en paralelo, porque redactar el contrato ANTES del backfill es lo que da al backfill su lista de tareas exacta.


8. Plan de acción

Fase A — El contrato de recipe (la "guía" que falta, como regla mecánica)

A1. Redactar RECIPE_CONTRACT.md (1 página, E2): el esqueleto canónico de una recipe — ejes y nombres de tokens dimensionales (canon: padding-inline/-block, control-height-*), qué sistemas transversales consume obligatoriamente (state-layer para hover neutro · focus según su arquetipo (campo=2 anillos / superficie=outline) · elevación SOLO vía --shadow-*/--depth-* · tipografía 1:1 · --opacity-disabled · touch-target implícito por archetype · list-surface si es lista) y las excepciones válidas con anotación.

A2. Convertirlo en reglas R-4.x de component-audit: (a) ningún box-shadow literal; (b) ningún opacity disabled literal; (c) ningún :hover con background que no sea state-layer o palette-swap declarado; (d) ejes físicos -x/-y prohibidos en tokens nuevos; (e) @keyframes local exige comentario /* functional: … */. WARN primero, FAIL tras la Fase B.

Fase B — Cerrar las adopciones a medias (mecánico, por barridos)

Acción Cierra
B-1 Completar state-layer (tabs, color-picker + resto neutro), podar los 22 *-bg-hover huérfanos de recipes, fix close:hover y opacities de archetypes I1, I2, B4, B5
B-2 Decidir la unificación escala-vs-canal depth (adopción data-depth o doctrina de convivencia explícita) — los box-shadow ya están tokenizados (R-4.1 = 0) E1
B-3 Terminar la migración focus box-shadow→outline en los 5 pendientes + un doc-mapa único del modelo de focus (campos/superficies/fallback/HCM) + corregir el comentario stale de archetypes F1
B-4 list-surface a select/combobox (pendiente declarado) LS1
B-5 Codemod padding-x/y → padding-inline/block + unificar naming de alturas S2, S3
B-6 Inventariar los 49 keyframes: mover firmas al config, anotar los funcionales M1

Fase C — Theme builder correcto

C1: B1+B2+B3 (alphas completas, donors del tema, background real) + tests de "retint completo" (aplicar scheme → grep de tokens del color viejo en el CSS resultante = 0). C2: memoización de renderStaticCss + apply(scope) (fable_audit E1). C3: log en #renderSchemeCss.

Fase D — Decisiones de inventario (una sesión de decisiones, no de código)

  • Alpha scales: ¿consumir (focus rings, scrims, hovers → sustituyendo color-mix ad-hoc) o recortar a a1–a3? (C4/TB1 se resuelven juntos).
  • Slots text-strong/separator/border-hover/hover/active: consumir o dejar de emitir (C2).
  • Bundle --size-*: refactor de consumo o degradar a doc y retirar (S1) — lleva un año en el limbo; cualquiera de las dos opciones es mejor que el limbo.
  • semanticTracking todo-cero: usar o retirar (T3).
  • tertiary: sigue "RESERVED" sin consumidor — fijar fecha de revisión.

Fase E — Documentación

E1: Partir THEMING.md — THEMING.md (referencia atemporal ≤600 líneas: mental model, capas, roles+slots generados desde código, sizes, naming con los ejes dimensionales, punteros a TSC/GUIDE) + THEMING_CHANGELOG.md (§20–§38 tal cual). E2: retirar §13/§14/§21 al changelog. E3: borrar las secciones "estado actual" congeladas o regenerarlas por script. E4: tabla única de slots/roles/mapeos autogenerada (generate:contracts-docs ya existe como precedente).


9. Nota final

La frase del encargo — "se ha ido desviando con cada modificación" — es correcta como descripción del proceso, pero la desviación no es entropía aleatoria: es una serie de mejoras genuinas (state-layer, depth, 1:1, opacidad, touch) que se quedaron sin las dos piezas que el propio repo demostró que funcionan: el guard mecánico y el cierre del rollout. La materia prima para el "esquema coherente" ya existe; lo que falta es promover el contrato de recipe a regla ejecutable (Fase A) y pagar la deuda de adopción (Fase B). Con eso, eidos pasa de "federación de sprints" a el sistema que sus propios docs describen.

Cifras verificadas por grep a 2026-07-01 sobre alpha-0.1-sec-dom (y recalibradas el 2026-07-02 con las reglas R-4.x, que parsean declaraciones completas en vez de líneas): 135 CSS de componentes; state-layer 21; *-bg-hover 22 declarados / 5 consumidores; box-shadow literales 0 (los 23 matches iniciales llevaban var()); keyframes locales 49 en 25 ficheros — 23 componentes sin anotación funcional (R-4.5); --size-* 0 consumidores; alphas directas 0; slots huérfanos text-strong/separator/border-hover 0 usos; padding-x/y 18 vs inline/block 18 (2 componentes, R-4.4); opacity literales fuera de @keyframes 2 componentes (R-4.2) + 3 en archetypes.css; hex crudos 6 (1 fichero); var(--scale-*)/--primitive-* directos 2 componentes (R-4.6); duraciones crudas 5.


Addendum 2026-07-02 — Fase A ejecutada + desfase del tooling

  1. El Recipe Contract existe: src/uix/eidos/RECIPE_CONTRACT.md (canon E2 — esqueleto de recipe, sistemas transversales obligatorios, excepciones anotadas) + sección C4 / reglas R-4.1–R-4.6 en COMPONENT_COMPLETION_CHECKLIST.md y scripts/component-audit.ts (severidad WARN; graduarán a error tras el backfill). Referenciado desde THEMING §8, eidos/README y docs/README (mapa E2). Tracks words/palabras/chronos excluidos.

  2. Línea base R-4.x (primer run): R-4.1 = 0 · R-4.2 = 2 · R-4.3 = 0 · R-4.4 = 2 · R-4.5 = 23 · R-4.6 = 2. El grueso del backfill de Fase B es el canal de motion (keyframes sin clasificar), no la elevación ni los hovers — mejor de lo estimado.

  3. Backfill Fase B (mecánico) ejecutado — 2026-07-02. R-4.1/4.2/4.3/4.4/4.6 = 0:

    • R-4.4: padding-x/y-* → padding-inline/block-* en popover + tooltip (recipes/base.ts + popover.css + tooltip.css + natural-time-picker.css + regen; diff del generated = exactamente los 14 tokens). Verificado en navegador: popover viewport padding: 12px, tooltip 6px 10px, token viejo vacío, 0 errores de consola.
    • R-4.2: scroll-frames 0.5 → var(--opacity-50), spinner ring-track 0.6 → var(--opacity-60) (value-preserving, la escala numérica dual de §35 existía para esto).
    • R-4.6: menu-dial scrim → var(--color-neutral-text-strong) (primer consumidor del slot huérfano C2, value-preserving); media-player amber anotado /* literal: theme-stable accent over arbitrary video */.
    • R-4.5: los 13 keyframes de maquinaria continua (spinner ×3, progress ×3, shimmer ×2, spin ×4, toast-pulse) anotados /* functional: … */. Quedan 15 componentes como backlog real de migración al canal de motion — entradas/salidas de overlays (link-preview, nav-menu, menús, select, combobox, tabs, tooltip, collapsible, card, onion/menu-dial, clipboard-pop, timeline-reveal) y la reacción a data-event-* de metrics-flash. Migrarlos exige verificación visual por componente (presets data-animation-style / firmas en config) — deliberadamente NO se hizo en esta pasada mecánica.
    • Tests eidos: los 15 fallos existentes son WIP paralelo pre-existente (palabras, natural-time-picker, calendar view-switch, paleta 31→33) — ninguno menciona los tokens renombrados; recipe-css-contract pasa limpio sobre popover/tooltip.

3.ter Migración R-4.5 — segunda tanda (2026-07-02): 7/15. Tres patrones más, todos verificados en navegador:

  • tooltip (máquina tri-estado delayed-open/instant-open): tras la objeción del usuario ("¿sale fuera del canon?") la solución intermedia (keyframes del canal sin preset) se sustituyó por la plenamente canónica: el generador gana PRESET_STATE_ALIASES (render-css.ts) — delayed-open es alias de apertura del trigger de presets (selector propio, bySide y reglas reduce incluidos), e instant-open deliberadamente NO lo es (su semántica ES "aparecer sin entrada"). Tooltip estampa motion='scale-fade' como cualquier overlay (preset conmutables + política reduce del preset) conservando sus duraciones vía hooks. Verificado: delayed-open → scale-in, fade-in 0.18s; instant-open → none; cero keyframes locales; generated emite los alias. El alias sirve a cualquier futura máquina soma con vocabulario de apertura más rico.
  • link-preview: el caso de libro — sus 8 keyframes por-lado eran la re-implementación manual del bySide; ahora preset slide-fade. Verificado con side='top' → slide-from-bottom (la side-awareness que costaba 8 keyframes).
  • collapsible (momento-EVENTO, no estado): sus keyframes reaccionaban a data-event='expand/collapse' de sema → migrados a BUILTIN_SIGNATURES (firma genérica por verbo emerge.expand/collapse, como present/dismiss) + keyframes expand-reveal/collapse-conceal en el canal. Beneficia a CUALQUIER emisor futuro de esos verbos. Verificado en pleno hold: expand-reveal 0.24s / collapse-conceal forwards.
  • Fix de la regla R-4.5: el scanner ahora blanquea comentarios antes de buscar @keyframes (un comentario con la palabra daba falso positivo); el lookback de la anotación sigue sobre el texto original. Backlog restante: 8 — navigation-menu (≈shared-axis-x), tabs (content fade/slide ×3), card (card-emerge≈scale-fade/present), clipboard-pop, metrics-flash (reacción a data-event → candidata a signature per-intent), timeline-reveal, onion-menu y menu-dial (radial/backdrop — preset propio o excepción razonada).

3.bis Migración R-4.5 en curso (2026-07-02) — 4/15 hechos, patrón validado. dropdown-menu · context-menu · select · combobox migrados al preset canónico slide-fade: prop motion (default 'slide-fade', 'none' opt-out) en el Content/SubContent eidos → data-animation-style; keyframes locales retirados del recipe. Los hooks --motion-duration-enter/-exit del preset permiten migración value-preserving: select conserva sus --select-open/close-duration alimentándolos al hook (verificado en navegador: 0.18s exactos); combobox ídem con --duration-normal. Los 4 verificados en navegador (attr + animationName: slide-from-top, scale-in, fade-in side-aware + 0 errores de consola) y PASS en el audit. Ganancia colateral: los paneles ganan exit animation + política reduced-motion que sus keyframes locales no tenían. Backlog restante: 11 (tooltip, link-preview, navigation-menu, tabs, collapsible→preset collapse, card, clipboard, metrics, timeline, onion-menu, menu-dial). Hallazgo pre-existente (no regresión, detectado al verificar): el stagger de items de menú ([data-stagger][data-state='open'] > [data-animation-style]) exige hijo DIRECTO — los items dentro de [data-*-group] nunca matchean; el cascade solo anima items sin agrupar. Candidato a fix en el generador de renderMotionBlocks (selector descendant o variante group-aware).

3.quater Migración R-4.5 — tercera tanda (2026-07-02): 10/15. Tres casos de MONTAJE, cada uno con su mecanismo correcto:

  • clipboard (bubble "Copied!"): entrada de contenido pura → motionAttrs('scale-fade') en el wrapper del Indicator (el mecanismo canónico de mount). Hooks preservan el pop original (--motion-scale-enter: 0.85 + fast). Verificado con sonda MutationObserver (el bubble vive timeout ms y el click sintético no da user-activation a la Clipboard API — hizo falta click CDP real): scale-in, fade-in a 0.12s, scale 0.85.
  • card (emerge de montaje): NO puede usar motionAttrs — la card tiene data-state SEMÁNTICO (máquina selected) que el data-state='open' constante pisaría. Patrón materiales: la regla del recipe consume los keyframes registrados (slide-from-bottom + fade-in) con sus tokens; el token de distancia alimenta --motion-distance-md scoped. Verificado por inyección: 0.32s (slow preservado), 8px.
  • timeline (reveal de live-feed): el trigger es CONDICIONAL al flag data-live del ROOT — un stamp incondicional animaría timelines no-live. Patrón materiales con la regla container-gated. Verificado por inyección en su ruta (recipes code-split: el CSS solo carga donde el componente monta): 0.24s, distancia 12px del token vía override scoped. Doctrina emergente de la tanda: preset stamp cuando el elemento no tiene data-state semántico ni trigger condicional; patrón materiales (keyframes registrados + razón documentada) cuando lo tiene. Hallazgo colateral preexistente (chip creado): el demo de card anida <button> en <button> (Card interactiva + Buttons hijos) — node_invalid_placement_ssr en consola. Backlog restante: 5 — navigation-menu, tabs, metrics, onion-menu, menu-dial.

3.quinquies Migración R-4.5 COMPLETADA (2026-07-02): 15/15 — R-4.5 graduada a error. Cuarta tanda (5 componentes), verificada en navegador por inyección:

  • onion-menu y menu-dial: sus keyframes eran fades puros → fade-in registrado (trivial).
  • tabs: sus 3 sabores (data-motion fade/slide × orientación — eje per-componente que el trigger genérico no modela) → materials: fade-in / slide-from-bottom+fade-in / slide-from-right+fade-in con el nudge de 4px preservado vía --motion-distance-sm. Verificado los tres.
  • navigation-menu: enter → slide-from-top+scale-in+fade-in (panel CSS-posicionado sin data-side); los 4 swaps direccionales → el par shared-axis M3 completado en el canal (slide-axis-x-in/out-reverse, adición genérica: la dirección backward faltaba) con travel de 12px vía --space-3. Verificados los 5 triggers.
  • metrics: keyframe genérico value-flash registrado (tinte vía hook --motion-flash-tint); el recipe conserva su graduación por intent y el trigger (materialización por-componente de la señal, más rica que el pulse genérico de announce). Doctrina final consolidada en RECIPE_CONTRACT §4: preset stamp / firma de evento / patrón materiales — los tres modos legítimos de consumir el canal, con el criterio de cuándo aplica cada uno. Verificación de cierre: audit global estable (75/50/5, la graduación costó cero), generated-css + recipe-css-contract con solo los 3 fallos pre-existentes ajenos, npm run check con la línea base intacta (61 pre-existentes, 0 propios).

3.sexies Fase C — theme builder corregido (2026-07-02): TB1 + TB2 + TB3 cerrados. buildScheme reescrito en dos fases (planificar escalas → emitir):

  • TB1: emite la rampa alpha COMPLETA a1..a12 por rol (antes solo a2/a3 — el resto quedaba apuntando al tema anterior tras applyColorScheme). Verificado en vivo en /temas/color: el scheme aplicado pasa de 18 a 108 alphas (9 roles × 12).
  • TB2: nueva opción intentSeeds — la base del temper es el step-9 del tema ACTIVO por intent (ActiveEidos.#resolveIntentSeeds), con la convención canónica solo como fallback. El knob ya no cambia la familia de hue de risk (naranja del tema vs ámbar de la convención). Test dedicado.
  • TB3: sin background hardcodeado — las alphas compositan sobre el step 1 del neutral DERIVADO (la superficie real de la página bajo el scheme, mode-correcta gratis porque los donors son por-modo). options.background queda como pin explícito. Test de no-op contra el neutral derivado.
  • Bonus: options.mode ahora fuerza también el DONOR (vía el theme resolver) — antes solo tocaba el background (gap doc↔impl preexistente); y el demo /temas/color dejó de pasar el #fff/#111 heredado (dogfooding del fix). Verificación: build-scheme 10/10 + resolve-token 16/16; suite eidos con los mismos 15 fallos pre-existentes (cero nuevos); npm run check en línea base (61, 0 propios).

3.septies Fase D — decisiones de inventario tomadas y ejecutadas (2026-07-02). Las 4 decisiones (usuario eligió la recomendada en todas) + tertiary resuelta por los hechos (el builder la deriva vía M3 → se queda, documentada como slot de jerarquía para apps):

  • semanticTracking BORRADO (eje todo-cero): typography.ts + config-types + validación + emisión; su único consumidor (tabs.trigger-letter-spacing) pasó a --tracking-normal (value-preserving). 0 tokens --tracking-{role} en generated.
  • Slot border-hover RETIRADO del generador y del contrato (0 consumidores; los 19 --{c}-*-border-hover restantes son tokens bespoke de recipe, legítimos). Comentario tombstone en COLOR_ROLE_SLOTS por si vuelve un caso.
  • separator ADOPTADO: sweep de los divisores de línea (5 tokens de recipe
    • combobox/command css + el componente Separator) de border-subtle (step 4, lavado) al slot canónico (step 6 — tono Radix de divider; Separator standalone queda idéntico porque 6==border-default). Verificado en navegador (dropdown: oklch(0.3485) = slot) + screenshot.
  • Alphas: contrato completo + workstream de adopción abierto — sustituir color-mix ad-hoc por steps alpha donde el semántico coincida (candidatos: tintas soft, rings, scrims). Sin código en esta pasada más allá del TB1 que ya las emite bien.
  • Bundle --size-*: refactor de consumo APROBADO + patrón establecido con el pilot toggle — consumir la coordenada canónica donde el valor coincide (height/font-size, alias idénticos, verificado 36px/16px en navegador) y conservar las desviaciones deliberadas (px/gap) visibles. THEMING §5 actualizado; el barrido de ~90 recipes es el workstream abierto (por tandas verificadas). Verificación: regen limpio, recipe-css-contract con solo los 3 fallos pre-existentes, guard R-4.x en verde.
  1. Tracks WIP excluidos consistentemente (2026-07-02). words / palabras / chronos son componentes SIN TERMINAR (vías de desarrollo paralelas) — pero solo el guard tipográfico los excluía; el resto de checks de recipe-css-contract.test.ts y el component-audit completo los contaban como catálogo roto. Unificado en un set WIP_TRACKS compartido: el audit los saca del catálogo y los veredictos (130 componentes; BROKEN 7→5; auditables a demanda vía --only words), y las 3 suites del contrato que fallaban por ellos ahora señalan solo deuda REAL de otros frentes en curso: navigation-menu-indicator-h sin declarar, los hex de natural-time-picker (C5, pendiente de decisión physically-fixed) y los 14 tokens huérfanos de drawer/color-field/calendar (WIP del view-switch del calendario).

  2. El tooling también había derivado (hipótesis del encargo extendida a los scripts, confirmada): component-audit auditaba contra la arquitectura anterior — E-2.2 exigía el import del CSS desde eidos/index.css (el patrón actual es el import del wrapper) y fallaba en 120/132; D-1.2 exigía el template de 6 tabs (el canónico v2 es de 9) y fallaba precisamente en las demos ya migradas; D-3.1 exigía somaSnippet (v2 usa snippet único); TabsVariant en el vocabulario espejo no tenía segmented. Corregido en la misma pasada. Veredictos antes/después del fix del tooling: 0 PASS / 117 NEEDS-WORK / 15 BROKEN → 75 / 50 / 7. Moraleja para la Fase E: los espejos manuales dentro de scripts (SHARED_VARIANT_VOCAB, templates de demo) necesitan el mismo tratamiento anti-drift que los docs — generarlos o testearlos contra el canon.

Powered by TurnKey Linux.