- Para quién es: Leads de design system y plataforma donde todos vibecodean (marketing, producto, founders) y la UI fuera de marca sigue cayendo en ingeniería para rescate.
- Qué problema resuelve: Los agentes ven metadata del componente pero no el source, así que inventan hex, espaciados y variantes que pasan review visual y entran a producción como mentiras sutiles sobre el sistema.
- Qué cambia si aplicas esto: Tokens legibles por máquina (~500 → ~350, mismo rango visual); modelo shadcn copy-not-npm; split MCP (listar vs implementar) → menos PRs con CSS fuera del sistema de tokens; una sola fuente de verdad en lugar de tres catálogos compitiendo.
- Dónde está hoy: una fuente que compila a cinco canales de distribución: registry autenticado, CDN inmutable, embed scopeado, tools MCP y paste nativo de Webflow con motion; 100% de los componentes elegibles llevan metadata de grado agente y docs editoriales, redactadas por agentes y gateadas por un humano; y el CI pone en rojo cualquier merge que haga al sistema menos verdadero (gates de reproducibilidad, contrato y cobertura de contenido).
Un agente de IA genera un botón. Compila. Renderiza. Se ve bien. El violeta del fondo es #534AB7, un color que no existe en ninguna parte del design system.
Nadie lo decidió. El agente tenía la metadata del componente (sabía que existía una variante, sabía que aceptaba un tamaño), pero no tenía el código real. Así que inventó el resto. Un hex plausible. Un padding de 36px donde el sistema usa 40px. Un font-size que se aproxima pero no coincide. El resultado pasa el code review humano porque se ve correcto. Y entra a producción siendo, sutilmente, una mentira sobre el sistema.
El camino honesto hacia un design system que los agentes no pueden alucinar son cinco problemas en secuencia, cada uno visible solo tras resolver el anterior: reinterpretar un sistema existente para lectores máquina; conectar agentes y ver que alucinan igual; descubrir que la arquitectura tenía la verdad duplicada en tres sitios; enseñarle al sistema a entregar donde el código ni siquiera corre; y llenarlo de conocimiento más rápido de lo que una sola persona puede escribir. Abajo va esa secuencia y lo que implica para design systems e IA.
El modelo de distribución sigue el registry de shadcn/ui (opens in new tab) (copiar source, no instalar caja negra por npm). El acceso para agentes sigue el Model Context Protocol (opens in new tab). Los tokens siguen el formato W3C Design Tokens (opens in new tab). El auth corporativo para MCP sigue las guías MCP de Clerk (opens in new tab). Los snippets de abajo muestran nuestro cableado; la tabla de referencias es lo que uso cuando el argumento debe valer fuera del repo.
Dónde empezó esto: una empresa que vibecodea
> En pocas palabras: Todos generaban UI con IA; ingeniería seguía corrigiendo el mismo desvío visual en páginas de marketing. Llegué al cliente en un momento preciso. El equipo de producto, un grupo de gente muy talentosa, estaba terminando la primera etapa de su design system: un sistema con la estética de shadcn, construido para la plataforma del cliente. Buen trabajo, base sólida. Pero vivía dentro del producto.
El cliente es una empresa de agentes de IA multimodales para WhatsApp. AI-first no es un eslogan ahí; es la forma en que todo el mundo trabaja, y eso incluye una práctica que define la cultura: todos vibecodean. Marketing, producto, founders. Generar código con IA es la norma, no la excepción.
Yo estaba a cargo de todo el pipeline web, y desde ahí veía el otro lado de esa cultura. Me llegaban páginas vibecodeadas que necesitaban cambios de estilo, homogeneidad entre touchpoints, o que simplemente tenían el código rotísimo. El design system de producto resolvía la consistencia dentro de la plataforma, pero entre los touchpoints de marketing (landings, campañas, microsites) no había nada que sostuviera la marca. Cada página generada era una interpretación ligeramente distinta de lo mismo.
Tenía dos caminos. Convertirme en el cuello de botella que revisa y arregla cada página a mano. O construir algo que le diera a los no técnicos el poder de generar correcto desde el inicio, y de paso quitarme ese trabajo de encima.
Tomé el design system de producto como base y lo reinterpreté. No para reemplazarlo, sino para extenderlo a un terreno donde quien genera el código no siempre es un ingeniero. Empezó como un side project. Creció demasiado de volumen. Y al final no solo me funcionó a mí.
Primero: un design system diseñado para lectores que no son humanos
> En pocas palabras: Simplificar el reglamento para que humanos e IA elijan los mismos colores y espaciados a la primera. El Client UIKit no es una copia del sistema de producto. Es una reinterpretación, mismo lenguaje visual pero arquitectura distinta, optimizada para dos lectores al mismo tiempo: el developer y el LLM.
El sistema original tenía ~500 tokens. Variantes por plataforma, tokens de CRM, estados interactivos mezclados en la capa semántica, escalas lineales con pasos que nadie podía distinguir a simple vista. Lo reduje a ~350 tokens en tres capas estrictas, sin perder una sola capacidad visual.
La razón no es minimalismo por estética. Es que un sistema con menos opciones es más fácil de generar correctamente, para un humano y, sobre todo, para un modelo.
- Menos tokens = menos decisiones = menos errores. Un LLM no tiene que elegir entre 27 espaciados cuando 13 cubren todos los casos.
- Pares
bg/foregroundpara cada superficie = el modelo siempre sabe qué color de texto va sobre cada fondo. - Naming consistente (BEM, kebab-case) = patrones que el modelo aprende rápido.
- CSS puro, sin CSS-in-JS = el modelo no necesita entender abstracciones de runtime.
El resultado es visualmente idéntico al sistema original. La diferencia está en la facilidad con la que un constructor (humano o IA) produce código correcto a la primera.
Esa frase, "diseño para que la IA genere correcto", suena a marketing hasta que la conviertes en decisiones concretas de arquitectura. La primera es cómo se distribuye el código.
Distribución shadcn, no npm
> En pocas palabras: Por qué los componentes viven como archivos visibles en lugar de un paquete oculto que la IA no puede inspeccionar. Los componentes del UIKit no están en npm. Esto está escrito, textual, en la primera línea del CLAUDE.md del monorepo:
> [!note] "Distributes via private registry (shadcn model): source copied to consumer projects, not installed as npm dependencies."
La decisión tiene una filosofía detrás, la misma que documenta shadcn para su CLI (opens in new tab): los componentes se añaden a tu proyecto, no se esconden en node_modules. Una dependencia npm es una caja negra: la instalas, la importas, y el código vive donde nadie lo lee. Para un agente de IA es el peor escenario: ve la firma del paquete pero no el interior, y rellena huecos con suposiciones.
El modelo registry (opens in new tab) invierte eso: ítems JSON describen archivos a copiar; el código es tuyo y modificable. Para el agente, el source real llega por una sola herramienta de implementación, no como import opaco.
El monorepo se organiza en ocho packages independientes:
tokens/- primitives/
- semantic/
- components/
css/- componentes en CSS puro + foundation
animations/- módulos GSAP, init(): CleanupFn
components-react/- ~60 componentes React 19
components-astro/- componentes Astro
whatsapp/- widget WCI como IIFE autocontenido
cli/- CLI de auth + consumo del MCP
layouts/- plantillas de página, patrón pure-DS
Ocho packages, pero una sola fuente de valores. Los tokens no están atados a ningún framework; son valores en un formato estándar, agnósticos de la tecnología que los consume. Por eso el mismo sistema produce componentes en CSS puro, en React, en Astro, y hasta un widget de WhatsApp como IIFE autocontenido. Esa capa agnóstica es lo que lo vuelve especial: no es un set de componentes de React, es una fuente de la que muchos stacks derivan el suyo. Y se consume de dos formas: por MCP para agentes en el editor, y por HTTP para quien vibecodea.
Mapa de documentación multi-repo
> En pocas palabras: Qué repositorio guarda tokens, componentes y el puente del agente. Sáltalo si no vas a implementar. El design system no es un solo repositorio; son cinco repos coordinados con contratos escritos en cada CLAUDE.md. Esta tabla es el índice que uso al onboardar ingeniería o agentes.
| Repositorio | Docs canónicos | Qué gobiernan |
|---|---|---|
client-uikit-ds | CLAUDE.md raíz, scripts/registry-schema.ts | Tokens (3 capas), packages, pnpm build:registry, client.discovery vs client.implementation |
client-uikit-cms | CLAUDE.md → Component article standard | Colecciones Payload, flag restricted en docs, secciones legibles por MCP (doc Button ID 67 = referencia) |
client-uikit-docs | src/app/api/auth/mcp-token/route.ts, /auth/mcp-oauth | Login Clerk, gate de dominio corporativo, intercambio de token CLI |
client-uikit-db | CLAUDE.md, supabase/functions/get-docs | Postgres Supabase + edge functions (get-docs, get-navigation); header x-restricted-access para blocks restricted. Retirado como dependencia de runtime en la última consolidación: el MCP hoy es fuente única sobre el registry (ver *La operación*) |
uikit-client-mcp | CLAUDE.md, src/server.ts | El MCP hosteado (repo propio): tools discovery/implementation, OAuth 2.1, flujo de página con gates, audit + export DESIGN.md |
Pipeline del registry (textual del CLAUDE.md del DS):
| Archivo | Propósito |
|---|---|
registry.json | Schema interno ClientRegistryItem |
scripts/extract-component-metadata.ts | Saca variants, props, cssClasses del source |
scripts/build-registry.mjs | Escribe public/r/*.json (compatible shadcn) |
scripts/test-extract-metadata.ts | 27 tests unitarios del extractor |
public/r/index.json | Catálogo discovery para warm start del MCP |
public/r/{name}.json | Archivos por componente con campo client completo |
El core de tools anti-alucinación (uikit-client-mcp/src/server.ts). Estos nueve aplican el split discovery/implementation; las capas de página, audit y export llegaron después (ver de un componente correcto a una página correcta):
| Tool | Clase | Rol |
|---|---|---|
client_uikit_context | Discovery | Bootstrap: componentes, tokens, estado auth. Llamar primero |
client_uikit_search | Discovery | Búsqueda en docs (fuzzy + sinónimos) |
client_uikit_component | Discovery | Solo props/variants; emite implementationAccess: requires_client_uikit_source |
client_uikit_get | Discovery | Cuerpo del doc por slug; filtro de sección (install, usage, props, …) |
client_uikit_list | Discovery | Slugs de todos los docs publicados |
client_uikit_navigation | Discovery | Árbol de navegación completo |
client_uikit_install | Discovery | Comandos de install + imports (paquetes deduplicados) |
client_uikit_source | Implementation | Único tool con CSS/React real (o tokens / foundation) |
client_uikit_validate | Implementation | Variants inválidos, reimplementaciones, clases desconocidas |
La separación anti-alucinación no es idea de blog; está en el output estructurado de component.ts y en la plantilla de sección Instalación del skill de artículos en el CMS.
Los tokens como contrato, no como variables bonitas
> En pocas palabras: Reglas de marca escritas para que los errores se noten, no se escondan. Si la distribución shadcn es la forma, los tokens en tres capas son el contrato. Y un contrato solo sirve si nadie puede romperlo por accidente.
Las tres capas referencian hacia atrás, nunca hacia los lados:
Primitivos son literales: un hex, un número de píxeles, una curva de easing. 271 colores en 26 familias, spacing base-4 de 13 pasos, escala tipográfica Major Third. No significan nada por sí solos; solo tienen un valor.
Semánticos le dan intención al primitivo. No dicen "usa zinc-900", dicen "esto es el color primario". Aquí vive la convención central: cada superficie tiene un compañero -foreground.
De componente acotan un semántico a un componente específico, solo cuando hace falta un estado que el semántico no cubre (hover, pressed, disabled).
La regla que sostiene todo el edificio es una sola: un token de componente nunca referencia un primitivo directamente. Siempre pasa por el semántico. Y no es pedantería; es lo que hace que dark mode funcione.
El CSS del botón dice var(--button-bg-primary), que resuelve a var(--primary), que resuelve a #18181b. Cuando cambias el tema, solo cambia el semántico, y el botón se actualiza sin tocar una sola línea de su propio CSS.
Si un token de componente referencia un primitivo directamente, saltándose el semántico, dark mode se rompe para ese componente. El primitivo no cambia con el tema. Solo los semánticos lo hacen.
Todo esto sigue el formato del W3C Design Tokens Community Group (opens in new tab) ({ "$value": "...", "$type": "..." }), con primera versión estable en octubre de 2025 (opens in new tab). No es cosmético: herramientas externas (Style Dictionary (opens in new tab), exportadores Figma, validadores MCP) leen el mismo contrato. El token es legible por máquina, no una convención en la cabeza de alguien.
Segundo problema: los agentes alucinaban de todos modos
> En pocas palabras: Aun con buena documentación, la IA seguía inventando componentes hasta cambiar qué podía tocar. Había construido un sistema deliberadamente predecible. Menos tokens, naming consistente, source siempre disponible. Y aun así, la primera vez que dejé a un agente generar interfaces, pasó lo del principio.
No mal como "roto". Mal como "fuera de contexto." El agente usó un violeta inventado porque parecía razonable. Usó 36px porque es un valor común. Eligió un font-size que casi coincidía. Cada decisión, aislada, era defendible. En conjunto, eran un sistema distinto al mío que se hacía pasar por el mío.
La causa era estructural, no del modelo. Yo le estaba dando al agente la metadata del componente (nombre, variantes, sizes, props) y esperando que produjera la implementación. Pero la metadata no contiene los valores CSS. Así que el agente hacía lo único que podía: los inventaba.
El problema no era que el agente supiera poco. Era que yo le estaba pidiendo que hiciera algo para lo que no le había dado la fuente. Y peor: nada en el sistema le impedía intentarlo.
La idea central: separar lo que un agente puede saber de lo que puede hacer
> En pocas palabras: Como catálogo de biblioteca versus llaves del archivo. Mirar libre, cambiar solo con herramientas aprobadas. La solución es un MCP server con separación deliberada entre dos clases de herramientas, la misma idea que Anthropic describió al lanzar MCP (opens in new tab): dar al cliente una superficie de tools pequeña y tipada en lugar de volcar contexto opaco. El capítulo Tools (opens in new tab) del spec es el contrato; nuestro split discovery/implementation es cómo lo aplicamos al CSS.
> [!note] DS CLAUDE.md: "This split enforces the anti-hallucination pattern: LLMs see enough to discover components but must call client_uikit_source for actual implementation details."
Cada item del registry tiene dos secciones. Una es visible para las herramientas de descubrimiento. La otra solo para las de implementación.
Las herramientas de discovery (client_uikit_context, client_uikit_component, client_uikit_search) devuelven solo metadata. Un agente puede listar componentes, leer sus props, entender qué existe, pero nunca ve una línea de CSS real.
Las herramientas de implementation en producción son client_uikit_source y client_uikit_validate (uikit-client-mcp/src/server.ts). client_uikit_source es el único que devuelve CSS/React real; client_uikit_validate contrasta snippets con MERGED_MANIFEST (variants inválidos, reimplementaciones prohibidas, ARIA faltante).
Lo que cierra el patrón es que el discovery no se queda callado sobre lo que oculta. Emite una señal explícita:
El sistema completo es una defensa en cuatro capas:
El cambio fue medible. Antes: el agente generaba `#534AB7`, 36px, font-sizes erróneos. Después: usa la escala zinc real, 40px, 13px, porque está forzado a llamar `client_uikit_source` antes de escribir. No porque el modelo sea más listo. Porque el sistema ya no le permite adivinar.
La metáfora correcta no es "el agente sabe más". Es "el agente ya no tiene dónde inventar". El patrón anti-alucinación no mejora al modelo; elimina la superficie donde el error era posible.
Quinta capa: quién puede conectarse (Clerk, OAuth, solo la empresa)
> En pocas palabras: Quién puede conectar una herramienta de IA al design system. Decisión de acceso, no detalle de diseño. El anti-alucinación controla qué sale del agente. El auth controla quién puede preguntar. El MCP no es un CDN público del design system. Es infraestructura para personas dentro de la empresa (y sus clientes de IA aprobados), con un flujo de punta a punta: login en el browser → bearer token → cuerpo completo de docs restringidos.
El artículo ya tenía mermaid para capas de tokens (primitivos → semánticos → componente), anti-alucinación en cuatro pasos, y ahora la secuencia de auth. Abajo va el mapa de plataforma, cómo se conectan repos y servicios, y cómo la capa 5 envuelve todo lo anterior.
Tokens: jerarquía de capas, cadena de resolución · Anti-alucinación: cuatro capas de tools · Auth y plataforma: mapa de arquitectura, envoltura de cinco capas, secuencia OAuth/CLI más abajo.
Por qué Clerk y no un login hecho en casa
Emitir tokens MCP no es lo que vendemos. Tampoco SAML, endurecimiento de sesión, políticas MFA ni la cola larga de compliance de auth. Montar login desde cero serían meses de seguridad antes de que el primer diseñador llame client_uikit_source. Usamos Clerk por velocidad y compliance que no queremos ser dueños. Clerk documenta construir un MCP server en Next.js (opens in new tab), conectar clientes MCP (opens in new tab) y OAuth para clientes terceros (opens in new tab); seguimos ese playbook en lugar de inventar OAuth propio.
Clerk protege la app de docs (ClerkProvider en Next.js). Nuestras rutas llaman auth() y currentUser() antes de mintear cualquier token.
Dos entradas, un mismo gate en el servidor
Todos pasan por el mismo check en el MCP hosteado (https://uikit-mcp.vercel.app/mcp): sin Bearer, no hay sesión MCP. El 401 con WWW-Authenticate: Bearer es intencional; Claude Web lo usa para disparar OAuth.
verifyBearerToken acepta un JWT de sesión de Clerk (verificado con @clerk/backend, sin red si CLERK_JWT_KEY está configurado) o un personal access token, JWT de 30 días firmado con MCP_SIGNING_SECRET, issuer client-uikit-mcp, audience client-uikit-mcp. Tokens inválidos o vencidos reciben invalid_token; no hay lectura anónima.
Camino A: Claude Web / clientes MCP remotos (OAuth 2.1 + PKCE)
1. El cliente descubre metadata OAuth en /.well-known/oauth-authorization-server y el recurso protegido en /.well-known/oauth-protected-resource/mcp. 2. /api/authorize redirige al frontend /auth/mcp-oauth con code_challenge (S256). 3. El usuario entra con Clerk. Si no hay sesión, va a /sign-in con redirect_url de vuelta a la página OAuth. 4. Tras el login, la app valida email corporativo: en nuestro deploy, primaryEmailAddress debe terminar en @client.io. El resto ve Access Denied; no se emite código. 5. El frontend mintea un authorization code de vida corta (JWT, issuer client-uikit-mcp-oauth, embebe code_challenge) y redirige al redirect_uri del cliente. 6. El cliente llama POST /api/token con el code y el verifier PKCE; el servidor valida el challenge y devuelve access + refresh tokens.
Camino B: Claude Code / Cursor / CLI local (`npx @client.io/mcp-uikit auth`)
1. La CLI levanta un servidor localhost de callback y abre GET /api/auth/mcp-token?port=PORT&state=STATE. 2. Mismo sign-in con Clerk y mismo gate de dominio en la ruta de la app de docs. 3. El servidor no pone el token largo en la URL. Guarda un código de intercambio de un solo uso (TTL 60s) y redirige a http://127.0.0.1:PORT/callback?code=...&state=.... 4. La CLI hace POST del code a /api/auth/mcp-token, recibe el JWT y lo guarda en ~/.config/client-uikit/credentials.json. 5. El MCP por stdio lee ese archivo, verifica el JWT con MCP_SIGNING_SECRET, y solo entonces activa restrictedAccess en los fetch a Supabase.
La página OAuth (/auth/mcp-oauth) repite el mismo check de dominio antes de firmar el authorization code. Usuarios aleatorios de internet no completan ningún camino, aunque conozcan la URL del MCP.
Docs restringidos: auth en el borde y en la base
Algunos docs del CMS van con restricted: true. Sin sesión MCP válida, get-docs devuelve metadata con blocks vacíos: el agente sabe que la página existe, no el contenido de implementación.
Cuando el handler del MCP valida el bearer, pasa RESTRICTED_CONTENT_SECRET al cliente Supabase como header x-restricted-access. Solo entonces la edge function devuelve los blocks completos. En stdio pasa lo mismo tras verificar el JWT de credentials.json; sin MCP_SIGNING_SECRET en dev, stderr avisa que el contenido restringido no está disponible.
La cadena es: Clerk (identidad humana) → dominio corporativo (pertenencia) → JWT MCP (sesión máquina) → header restricted (puerta de contenido). No es "el MCP es público, portense bien."
Mismo patrón, dos permisos
La separación discovery vs implementation responde "¿el modelo puede inventar CSS?" El auth corporativo responde "¿este caller puede cargar nuestro source?" Juntos explican por qué el sistema es infraestructura interna: marketing y agentes ganan velocidad, ingeniería mantiene la marca, y el compliance sigue en el roadmap de Clerk, no en el mío.
Tercer problema: mi propia arquitectura tenía la verdad duplicada
> En pocas palabras: Tres sitios decían ser “la lista de componentes”, receta para desvío silencioso.
Aquí es donde la historia deja de ser sobre el agente y pasa a ser sobre mí.
La primera versión del MCP funcionaba, pero por dentro era frágil de una forma que tardé en ver. La metadata del componente vivía en el DS. Pero el MCP la re-embebía en build-time con un script (embed-source.ts), generaba un manifest, y encima le aplicaba un archivo de component-overrides.ts para parchar campos que el extractor todavía no sacaba. La fuente de verdad estaba en tres lugares a la vez.
Eso es exactamente el tipo de deriva que el sistema de tokens fue diseñado para prevenir, y yo lo había reintroducido en la capa de distribución. Si el DS decía una cosa, el manifest embebido decía otra, y el override una tercera, ¿cuál era la verdad? La respuesta honesta era: depende de cuál leyeras primero.
La consolidación fue un proceso de cuatro waves a lo largo de tres días. No fue un rediseño; fue ir migrando, con tests de regresión en cada paso, hacia una sola fuente.
Wave 1: Enriquecer el registry (DS). Hacer que el registry del DS sea la fuente de verdad de la metadata. Un extractor (extract-component-metadata.ts) que saca discovery + implementation directo del source. 61 items enriquecidos, 27 tests unitarios, 0 errores.
Wave 2: Migrar los tools del MCP. Mover cada tool del manifest embebido al registry vía HTTP. Un adapter de tres funciones (getAllDiscovery, getComponentInfo, getImplementationData). 41 assertions de regresión validando el patrón anti-alucinación: 10 de 10 componentes verificados, cero fugas de campos de implementación.
Wave 3A: Borrar el camino viejo. Eliminar el feature flag, los handlers legacy, el código muerto. Siete archivos borrados, ~3,800 líneas, incluido embed-source.ts. El build pasó de un paso de embed a tsc solo.
Wave 3C: Sincronizar el sitio de docs. Reemplazar 62 JSONs del registry commiteados en el repo por un sync en build-time desde el DS. /public/r/ agregado al .gitignore.
Wave 4: Migrar los overrides. Mover los últimos cuatro campos de component-overrides.ts al registry + extractor. El archivo de overrides se borró por completo. Cero deuda de overrides.
| Métrica | Antes (Wave 1) | Después (Wave 4) |
|---|---|---|
| Entradas de override | 10 | 0 |
| Archivos legacy (MCP) | 7 (~3,800 líneas) | 0 |
| Fuentes de datos del MCP | 4 (manifest, embed, supabase, layouts) | 2 (registry, supabase) |
| Tests del DS | 0 | 38 |
| Build del MCP | embed-source && tsc | `tsc` |
| Build de docs | JSONs commiteados | sync en build-time |
El detalle que comunica madurez: build-time sync
> En pocas palabras: Chequeos automáticos para que catálogo y código real no se contradigan mucho tiempo. De todas las decisiones, la que más me gusta es la más pequeña. El sitio de documentación ya no commitea los JSONs del registry. Los sincroniza desde el DS cada vez que buildeas.
"build": "tsx scripts/sync-registry.ts && next build"El script de sync tiene dos fuentes con fallback: primero el filesystem (el DS como repo hermano, ~28ms), y si no está disponible, HTTP contra una URL de registry. Valida que el índice tenga al menos 50 items, que cada item tenga su campo name, que la metadata de discovery exista. Escribe atómicamente con archivos .tmp y rename para no corromper nada a medias.
Un artefacto derivado no se commitea. Se deriva. Si los JSONs viven en git, alguien eventualmente edita uno a mano, y la fuente de verdad vuelve a fracturarse. Al sacarlos del repo y generarlos en cada build, el sistema garantiza que lo que el sitio publica es, por construcción, lo que el DS dice, no una copia que alguien olvidó actualizar.
Cualquier cosa que puedas derivar de la fuente de verdad y elijas commitear de todos modos es una segunda fuente de verdad esperando divergir. Los JSONs commiteados se ven inofensivos hasta el día en que el del repo y el del DS no coinciden, y nadie sabe cuál ganó.
El sistema siguió creciendo: de un componente correcto a una página correcta
> En pocas palabras: Generar un botón correcto no es lo mismo que entregar una página correcta. La misma regla de "sin dónde inventar" ahora cubre la página entera, no solo sus partes. El split anti-alucinación resolvió el componente. Pero un botón correcto no garantiza una página correcta. En cuanto dejé a un agente armar una landing completa, apareció una clase nueva de error: esta vez no CSS inventado, sino estructura inventada. Secciones en el orden equivocado. Un hero sin llamado a la acción. El botón de WhatsApp apuntando a la nada. Un layout que ninguna plantilla del sistema tiene. Cada pieza era on-brand; la composición no.
La solución fue la misma regla, un nivel arriba: no le pidas al modelo que siga los pasos, quítale la posibilidad de saltárselos. El MCP ahora corre un flujo de página con gates: reúne el brief, propone secciones, elige layouts, llena los slots, y solo entonces entrega. Cada gate devuelve un token firmado que el siguiente exige. Los tokens van firmados con HMAC y son stateless (sin memoria en el servidor), así que el modelo literalmente no puede adelantarse: si llama a finalize sin un plan aprobado, la llamada falla indicando el paso que se saltó.
client_uikit_finalize es el único lugar que puede decir "approved". Re-corre las reglas de marca del lado del servidor (sin forms, solo el número wa.me confirmado, sin gradientes en botones) antes de que algo salga. Es de nuevo la idea de requires_client_uikit_source: el resultado correcto es el único camino disponible.
Del mismo trabajo salieron dos capas más, y ambas apuntan de vuelta a donde empezó esta historia:
| Capa | Tool | Qué hace |
|---|---|---|
| Flujo de página | client_uikit_plan_page → client_uikit_finalize | Gates forzados por el servidor; finalize es la única fuente del veredicto "approved" |
| Rescate | client_uikit_audit / client_uikit_patch_plan | Apúntalo a código vibecodeado existente; marca color/espaciado/font fuera del sistema y devuelve parches mínimos from→to |
| Export | client_uikit_design_md | Un comando emite un DESIGN.md portable (o tokens W3C, o un theme de Tailwind) para que otras herramientas de IA hereden el mismo contrato |
La capa de rescate es la escena inicial al revés. Este artículo abre con una página vibecodeada cayendo en mi escritorio para arreglarla a mano; audit + patch_plan es ese arreglo, automatizado: el sistema que evita que el error ocurra también limpia los que ya ocurrieron.
El MCP empezó como un puñado de tools de lectura que le daban al agente source de componente correcto. Hoy es un constructor: puede planear una página, generar sus imágenes, gatear su entrega, auditar la de alguien más, y exportar el sistema entero como un archivo que otra herramienta puede leer. La misma regla todo el camino hacia arriba: una fuente de verdad, una vía de entrada, un gate de salida.
Cuarto problema: el canal más importante no ejecuta código
> En corto: Las páginas con más tráfico de la marca viven en un builder no-code. Un design system que solo entrega código era invisible exactamente donde más importaba. Todo lo anterior asume que el consumidor ejecuta código. La realidad de marketing del cliente no: el sitio principal vive en Webflow, editado por gente que jamás va a abrir un editor de código. El canal con más tráfico y más exposición de marca era precisamente el que el sistema no alcanzaba. Y las dos opciones obvias estaban mal. Reconstruir componentes a mano dentro del Designer crea ports gemelos que driftean, la enfermedad exacta que este sistema existe para curar. Embeber código compilado preserva fidelidad pero mata la edición nativa, que es todo el punto de una herramienta no-code.
La entrada fue el clipboard. El formato de paste de Webflow (@webflow/XscpData) no está documentado, así que se hizo reverse-engineering contra dumps reales del Designer, y el build ahora emite un artefacto de paste por componente: estructura y estilos como elementos nativos y editables; lo que el Designer no puede expresar viaja en un snippet chico de head; y en el modo "connected" por default, el componente pegado consume la hoja de tokens viva: cambias un token en git y los componentes ya pegados se repintan en el siguiente publish.
El motion también viaja. Los behaviors son módulos agnósticos de framework que exportan su propio contrato DOM (qué hooks data-* requieren), y el emisor valida cada componente renderizado contra ese contrato antes de emitir. Pegas un marquee y anima al publicar, con el easing de la marca.
Dos detalles cargan casi toda la honestidad de ingeniería:
- Allowlists empíricas. Hay CSS que crashea el panel de estilos del Designer al seleccionar el elemento, encontrado por bisección de repro mínima, una propiedad a la vez. Esas propiedades se enrutan al snippet de head; los pseudo-estados que ningún dump real contiene jamás se emiten. El generador codifica lo que el Designer realmente sobrevive, no lo que sus docs insinúan.
- Exclusiones declaradas. No todo componente pertenece a un paste. App chrome, portales runtime-only, paneles interactivos se excluyen en la fuente, con una razón que viaja en el índice del canal. Una exclusión es una decisión que puedes leer, no un error que descubres. El canal hoy: 37 componentes emitidos, 22 excluidos, cada uno con su razón declarada, cero huecos silenciosos.
> [!note] El paste es una fotografía; el comportamiento se adjunta por contrato. La misma regla de todo el sistema: nada se genera dos veces, nada driftea en silencio. El artefacto se emite desde la fuente canónica en cada build, y un test de igualdad estructural contra fixtures reales del Designer pone el build en rojo si el formato se mueve debajo de nosotros.
Quinto problema: una biblioteca perfecta con los estantes vacíos
> En corto: Infraestructura sin conocimiento es un catálogo vacío. El fix: los agentes redactan, la máquina verifica, un humano firma los juicios, y el CI trata el contenido como cobertura de código. Con los canales construidos llegó una auditoría incómoda: infraestructura de grado plataforma, y 14 de 120 items del registry llevaban conocimiento de grado agente: rangos seguros de tuning, gotchas reales, cuándo-no-usarlo. Los payloads eran correctos y estaban vacíos. Y la densidad es lo único que no se puede forzar a lo bruto, porque un rango como "0.2–0.4 para FAQs densas" no es documentación. Es una decisión de diseño.
La división del trabajo que funcionó: el agente redacta, la máquina verifica, el humano decide. Un agente lee el source real y redacta la metadata; la suite de conformance hace imposible inventar (cada prop y default se verifica contra el código, la idea anti-alucinación, apuntada a la documentación); y los rangos llegan a mi escritorio como una tabla para aprobar o ajustar antes de cualquier commit. Las reglas de review son estrictas: guía prescriptiva o nada ("75 para logo bars; 40–60 para texto denso"), gotchas solo si son reales, cero relleno.
No puedes cerrar una ola que no puedes medir, así que el build ahora emite un tablero de cobertura de contenido con, por componente, estado de metadata, de editorial, de canal, y un score agregado. Los lotes de trabajo se derivan del tablero, no de una lista a mano. Y el CI ganó un gate de no-regresión: cualquier merge que baje el score agregado sale rojo. Borrar conocimiento ahora hace tanto ruido como romper un test.
La cobertura pasó de 14 al 100% de los componentes elegibles, con los excluidos listados y razonados, y el tramo final (12 manifests más 46 docs editoriales) lo redactaron agentes en una sola mañana, con el rol humano reducido a lo que debe ser: firmar los juicios.
Mide la documentación como mides la cobertura de código. Lo que hizo terminable la ola de contenido no fue escribir más rápido; fue un tablero que hizo visible lo que faltaba y un gate que hizo imposible retroceder.
La operación: una cadena de suministro, no una carpeta de copias
> En corto: Una fuente compila a cinco superficies de distribución, con un contrato ejecutable en cada costura, y CI que falla cuando cualquier copia deja de ser derivable de la fuente. A estas alturas el sistema es menos un repositorio que un pipeline: un monorepo compila decisiones de diseño en artefactos que se promueven por cinco superficies.
| Canal | Superficie | Contrato en la costura |
|---|---|---|
| Registry API | JSON autenticado por componente | conformance: manifest ↔ source, tests de schema |
| CDN público | /v1/*, inmutable y versionado | smoke contra producción con cache-busters |
| Embed scopeado | embed.css para páginas host hostiles | leak-tests bidireccionales en navegador real |
| MCP | tools tipadas para agentes | governance tests del adapter; split discovery/implementation |
| Paste Webflow | XscpData nativo + motion | contratos DOM + igualdad estructural vs fixtures reales |
Tres reglas operativas lo mantienen honesto:
- Reproducible por construcción. El CI falla si el registry publicado no puede reconstruirse desde el source commiteado, y cada artefacto lleva el commit del que se derivó. El mundo de supply chain llama a esto reproducible builds y provenance; aquí existen por una razón más humilde: una copia que nadie puede regenerar es una verdad que nadie puede verificar.
- La lista de distribución tiene un solo dueño. Todo lo que el canal público necesita (archivos, pasos de build, triggers de deploy) vive en un manifest único que el build consume y un check verifica contra sus cuatro consumidores. Esa regla se pagó con sangre: una sola tarde produjo cinco fallos de deploy, todos rastreados a la misma lista copiada a mano en cuatro sitios sin nadie sincronizándolos.
- La última consolidación. La prosa de docs por fin se separó en lo derivable (nueve secciones emitidas desde el source, para los 120 items) y lo editorial (juicio humano, en git). La dependencia de runtime de CMS-y-base-de-datos se eliminó por completo; el MCP hoy arranca desde exactamente una fuente, y una caída de un tercero ya no se lleva al design system con ella.
El plano de auth tuvo su propio pase de hardening en el camino: auditoría completa de la superficie OAuth, códigos de autorización de un solo uso con estado compartido (para que la garantía aguante entre instancias serverless), y una superficie de revocación de verdad. Un token que puedes mintear pero nunca revocar es una promesa, no un control.
La última capa: un pulso que sobrevive a la distribución
> En corto: La personalidad (el motion) codificada como tokens y contratos, para que sobreviva a todos los canales en vez de vivir en el archivo de animaciones de un solo dev. Lo último que ganó el sistema es lo primero que la gente nota: personalidad. El lenguaje de motion se especificó como todo lo demás: la curva de easing firma y la escala de duraciones viven como tokens, el ritmo de stagger se volvió una escala de tokens, y los behaviors leen esas custom properties en runtime. Hay un test que setea un valor falso en la hoja de estilos y verifica que el tween lo usó: el motion sale demostrablemente del sistema, no de literales enterrados en JS.
La decisión interesante no fue técnica sino de producto: los defaults de animación son por canal. Las superficies no-code traen el motion encendido por default con opt-out explícito: quien pega componentes no debería necesitar conocer un API para obtener el feel de la marca. Las superficies de código lo exponen como prop booleana; los ingenieros optan deliberadamente. Y el cursor decorativo existe como componente opcional que está siempre apagado por default, porque la accesibilidad está por encima de la personalidad.
La personalidad suele ser la parte menos sistematizada de un design system: vive en la cabeza de un animador y muere en la distribución. Codificarla como tokens más contratos DOM es lo que permite que un marquee pegado en una página no-code se mueva exactamente igual que el sitio insignia.
Qué cambió en cómo pienso
> En pocas palabras: Cambio de mentalidad para quienes financian design systems en equipos con mucha IA. Empecé creyendo que un design system para IA era un design system normal con una API encima. Terminé entendiendo que es otra cosa.
Un design system para humanos puede tolerar ambigüedad. Un humano ve dos naranjas casi iguales y elige el correcto por contexto, por gusto, por haber visto el Figma. Un agente no tiene ese contexto: tiene exactamente lo que el sistema le expone, ni un bit más. Eso convierte cada ambigüedad de tu arquitectura en un error garantizado, no en un error probable.
Las tres lecciones se encadenan. Reducir los tokens no fue sobre estética; fue reducir la superficie donde un generador puede equivocarse. Separar discovery de implementation no fue sobre seguridad; fue reconocer que "saber que algo existe" y "saber cómo construirlo" son permisos distintos que deben concederse por separado. Y las cuatro waves no fueron limpieza; fueron la consecuencia inevitable de tomarme en serio mi propia regla: una sola fuente de verdad, o ninguna.
El patrón anti-alucinación no es realmente sobre alucinaciones. Es sobre autoridad: una sola fuente de verdad, accedida de una sola forma, validada de una sola forma. La alucinación es solo el síntoma que aparece primero cuando esa autoridad no existe.
Y hay un efecto que no anticipé. El mismo sistema que construí para que un agente no alucinara resultó ser el que le permite a alguien de marketing vibecodear una landing y que salga consistente con la marca a la primera. La restricción que protege al generador automático es la misma que le da poder al humano no técnico. Por eso dejó de ser mi side project y se volvió infraestructura del equipo: cuando el sistema garantiza el resultado correcto, deja de importar quién o qué escribe el código.
Cómo replicarlo en tu próximo design system
> En pocas palabras: Pasos iniciales si quieres las mismas barandillas sin copiar cada decisión técnica.
- El registry es la única fuente de verdad. Toda la metadata (variantes, sizes, props, clases, peer deps) se extrae del source, no se mantiene a mano en paralelo. Si tienes un manifest, un override y el source diciendo cosas sobre el mismo componente, ya tienes tres versiones de la verdad y la pregunta no es si van a divergir, sino cuándo.
- Separa discovery de implementation. Da a los agentes una capa de metadata para descubrir qué existe, y una capa de source, accesible de una sola forma, para construirlo. Que la capa de discovery declare explícitamente que oculta el código y cómo pedirlo. Un agente que sabe que no debe inventar es la mitad de la solución; un sistema que no le deja inventar es la otra mitad.
- Tokens en capas, con una regla inviolable. Primitivos, semánticos, componente. El componente nunca toca el primitivo. Esa única regla es la diferencia entre un dark mode que funciona y uno que se rompe en los lugares más difíciles de detectar.
- No commitees lo que puedes derivar. Si un artefacto se puede generar desde la fuente en build-time, genéralo. Cada JSON derivado que vive en git es una invitación a editarlo a mano y fracturar la verdad.
- Fail-closed por default. El sistema no debería depender de que el agente "se porte bien". Debería hacer que el error correcto sea el único camino disponible. La señal
requires_client_uikit_sourceno le pide al modelo que sea responsable; le quita la opción de no serlo.
- Protege el MCP como una API de producto. Auth antes de ejecutar tools; dominio corporativo antes de mintear tokens; nunca secretos de larga vida en URLs. Usa Clerk (o equivalente) cuando el login no es tu diferenciador: entrega el design system, no un programa de compliance.
- Pon un contrato en cada costura. Todo artefacto que cruza una frontera de repo o de canal necesita un check ejecutable del lado que recibe: validación de schema, igualdad estructural, smoke contra producción. Una costura sin contrato es donde la verdad se va a fracturar primero.
- Mide el contenido como mides el código. Un tablero de cobertura para metadata y docs, más un gate de no-regresión en CI, convierte "la documentación" de un deseo en una ola terminable con definición de done. Los agentes pueden escribir el volumen; reserva al humano para los juicios, y haz ese gate explícito.
El codebase de este sistema cabe en la cabeza. Ocho packages, un registry, un MCP donde cada tool sigue resolviendo a una sola fuente de verdad. La complejidad no vive en el código; vive en las restricciones y en quién es dueño de la verdad.
Referencias (externas, para guardar)
> En pocas palabras: Referencias de industria detrás de las decisiones de MCP y tokens.
El aprendizaje real
> En pocas palabras: Un design system para equipos con IA es historia de permisos tanto como de paleta. Los design systems no fallan en el componente. Fallan en la pregunta "¿cuál es la versión correcta de esto?" cuando hay más de una respuesta posible. Un humano navega esa ambigüedad sin darse cuenta. Un agente de IA la convierte en #534AB7 en producción.
Construir para que una máquina lea tu sistema no es una restricción molesta; es el ejercicio que te obliga a hacer explícito todo lo que antes resolvías con criterio. Cuando el único lector posible es uno que no tiene tu contexto, no te queda más remedio que poner el contexto en el sistema. Y un sistema donde el contexto es explícito es, resulta, mejor también para los humanos.
Hay una frase con la que evalúo si un design system está terminado: si dos partes del sistema pueden decir cosas distintas sobre el mismo componente, todavía no tienes un design system, tienes varias opiniones compartiendo un repositorio.