Brand Validator Guide

publiccofoundyv1·by Cofoundy·May 14, 2026referencebrandvalidatordesign-system

Esta página es el manual de referencia para el brand validator de DocsAI. Si te llegó aquí
desde un error 422 con code: brand_violation en stderr, lee la sección How to read a
violation
primero y luego salta a Common fixes para el patrón exacto que necesitas
arreglar.

El validator existe para que cada doc publicado en docs.cofoundy.dev respete los tokens de
@cofoundy/ui sin que ningún autor — humano o agente — tenga que memorizar la paleta. Es la
compuerta entre "MDX que se compila" y "MDX que se publica".

What it is

El brand validator es una garantía en tiempo de compilación: cada doc que se publica respeta
el vocabulario visual de @cofoundy/ui. Corre como un plugin de dos capas dentro del pipeline
de rehype, intercalado entre rehype-mermaid y rehype-pretty-code, sobre el HAST después
de que el MDX ya se parseó. Si el doc no pasa, no se publica.

La primera capa es de seguridad y siempre está activa. Bloquea <script>, <iframe>,
<object>, <embed>, <link>, atributos on* (event handlers), href="javascript:...",
y srcdoc en cualquier elemento. Es un perímetro no negociable: ni siquiera brand: off
lo desactiva.

La segunda capa es de marca y valida classes, estilos inline, fuentes, y protocolos
contra el allowlist derivado de @cofoundy/ui. Una clase de Tailwind ad-hoc como
bg-red-500, un style={{ color: '#FF0000' }}, o un font-family: Arial reciben una
violación con sugerencia cuando es posible. Esta capa se puede atenuar (modo warn) o
apagar (modo off) por doc.

Three modes

El campo brand en el frontmatter elige el modo. Default es strict.

ModoComportamientoCuándo usar
strictCualquier violación lanza 422 en publish; el doc se rechaza.Default. Todo doc nuevo.
warnLas violaciones se acumulan en tree.data.brandViolations y el doc se publica igual.Migración de docs legacy, o contenido importado con un ticket de cleanup vivo.
offLa capa de marca se salta entera. La capa de seguridad sigue corriendo.HTML importado de un vendor que no se puede limpiar — solo con brand_off_reason explícito.

strict es la única opción que mantiene la garantía. warn es un compromiso temporal: el
doc sale, pero las violaciones quedan registradas y el equipo debe revisarlas. Si un doc
queda en warn por más de un sprint sin un ticket de migración, eso es deuda — no estado
estable.

off requiere brand_off_reason en el frontmatter explicando por qué. Sin ese campo, el
publisher rechaza el doc aunque brand: off esté seteado. La razón debe ser específica
(p. ej. "legacy vendor import; T-099 tracks refactor"), no genérica. La intención: que
auditar el repo por grep "brand: off" muestre el costo real.

How to read a violation

Cuando el publisher (scripts/publish.ts) atrapa un BrandViolationError, sale con código
22 y emite este payload por stderr:

{
  "code": "brand_violation",
  "message": "3 brand violation(s) in cofoundy/my-doc",
  "violations": [
    {
      "code": "brand_violation",
      "rule": "raw-color",
      "element": "div",
      "attribute": "style",
      "got": "color: #FF0000",
      "suggestion": "color: var(--cof-danger)",
      "position": { "line": 12, "column": 4 }
    }
  ],
  "docs_url": "https://docs.cofoundy.dev/cofoundy/brand-validator-guide"
}

Campo por campo:

  • code — siempre brand_violation en el envelope y en cada item; permite filtrar en logs.
  • message — resumen humano con el conteo y el slug del doc.
  • violations[] — array completo, no se trunca. Si un doc tiene 47 violaciones, las 47 salen.
  • violations[].rule — el chequeo específico que falló (raw-color, unknown-class, disallowed-font, disallowed-protocol, security-element, etc.). Es el ancla para buscar el fix.
  • violations[].element / attribute — dónde está la ofensa (qué tag, qué atributo).
  • violations[].got — el valor literal que el autor escribió.
  • violations[].suggestion — el reemplazo recomendado, si el validator pudo derivarlo (p. ej. mapear #FF0000 al token --cof-danger por distancia perceptual). Si la sugerencia es null, el color cayó fuera del umbral y el autor debe elegir un token a mano.
  • violations[].position — línea y columna en el MDX original (no en el HTML compilado), para que el editor pueda saltar al lugar.
  • docs_url — esta página. Apunta acá precisamente para que el agente o el humano sepa qué leer cuando ve un 422.

El exit code 22 es estable: cualquier wrapper (CI, /publish skill, agente) puede detectarlo
sin parsear stderr. El payload JSON es para humanos y para herramientas que sí quieren leer
las violaciones individuales.

Common fixes

La mayoría de violaciones caen en seis patrones. Esta tabla es el atajo:

ForeignBrand
style={{ color: '#FF0000' }}style={{ color: 'var(--cof-danger)' }}
style={{ color: 'red' }}style={{ color: 'var(--cof-danger)' }}
className="bg-red-500"className="bg-cof-danger"
className="text-blue-600"className="text-cof-accent"
style={{ fontFamily: 'Arial' }}usar Inter / Space Grotesk / JetBrains Mono, o var(--cof-font-body)
<form> para preguntas inlineusar un componente nombrado (fuera de alcance para V2.5)

La regla general es: si el valor que escribiste es un literal (un hex, una keyword CSS, una
clase de Tailwind sin prefix cof-), probablemente no está en el allowlist. Los tokens de
marca tienen forma estable: classes con prefix cof- (bg-cof-*, text-cof-*, border-cof-*),
variables CSS con prefix --cof- (var(--cof-danger), var(--cof-accent)), o las tres
familias tipográficas aprobadas.

Si lo que necesitas es un color, un spacing, o una sombra que no existe en el allowlist, ese
es un cambio de design system — abre un ticket en @cofoundy/ui, no inventes el token en el
doc. La idea es que el sistema de marca tenga un solo SSOT.

CustomPanel usage

<CustomPanel> es el escape hatch de vocabulario abierto. El autor compone HTML arbitrario
adentro y los hijos siguen pasando por el validator — el contrato es ese exactamente: tienes
libertad de composición pero no libertad de salirte del vocabulario.

<CustomPanel topic="adoption">
  <div className="cof-card grid grid-cols-3 gap-4">
    <div className="cof-callout">A</div>
    <div className="cof-callout">B</div>
    <div className="cof-callout">C</div>
  </div>
</CustomPanel>

El attribute topic es libre — sirve para que el componente decida estilos por tema, pero
no es validado contra una lista cerrada. Lo que sí se valida son los hijos: si pones
<div style={{ background: '#000' }}> adentro, el validator lo va a marcar exactamente
igual que si estuviera en el doc raíz. CustomPanel no es un bypass.

Si encuentras que constantemente metes el mismo patrón dentro de CustomPanel, esa es la
señal para crear un componente nombrado con su propio contrato — no para seguir copiando
HTML.

Escape hatches

Hay dos vías para apagar el validador, en orden de menor a mayor:

1. data-brand-trusted en un elemento específico. El walker desciende al subtree pero
la capa de marca no chequea ese nodo ni sus hijos. La capa de seguridad sigue corriendo.
Hoy ningún producer (ningún componente, ningún script) emite este atributo — está reservado
para casos futuros donde un componente sintetice HTML que ya validó por su cuenta. No lo
escribas a mano en un doc.

2. brand: off + brand_off_reason en el frontmatter. Apaga la capa de marca para el
doc entero. Solo úsalo cuando integras HTML de un vendor que no se puede limpiar y existe
un plan documentado para resolver el caso. El brand_off_reason debe ser específico
("vendor HTML from Stripe checkout snippet; refactor blocked by T-099"), no genérico
("vendor"). La capa de seguridad sigue activa: ni siquiera off te deja meter <script>.

Ambas vías quedan auditables por grep. Si un día decides endurecer la política, sabes
exactamente cuántos docs y cuántos nodos están exentos.

FAQ

¿Por qué mi atributo MDX se saltó silenciosamente?

Cuando escribes style={{ ...computed }} con un valor de expresión (en lugar de un objeto
literal), el validator no puede evaluarlo estáticamente — no sabe qué va a producir esa
expresión en runtime. La política es saltarlo sin error: marcar todo lo dinámico como
violación tendría demasiados falsos positivos. La consecuencia es que tu doc se publica,
pero el valor real no pasó por el validator. Si quieres validación garantizada, usa literales
de string (style={{ color: 'var(--cof-danger)' }}).

¿Por qué mi color no tiene sugerencia auto-fix?

Las sugerencias usan ΔE (distancia perceptual entre colores). Solo los colores que caen
dentro de un umbral cercano a un token de marca reciben sugerencia. Un color muy lejano
(p. ej. neón verde) se marca como violación pero sin recomendación — el validator no quiere
adivinar un token que perceptualmente no se parece. En ese caso, el autor elige el token de
marca a mano.

¿De dónde sale el allowlist?

De lib/brand-allowlist.json, que se deriva de @cofoundy/ui (tokens.css + el preset de
Tailwind) por scripts/derive-allowlist.ts. Después de actualizar @cofoundy/ui, corres
pnpm allowlist:write para regenerar el archivo. El gate de CI pnpm allowlist:check
bloquea drift — si alguien edita el JSON a mano sin tocar @cofoundy/ui, el build falla.
El allowlist es output derivado, no fuente de verdad.

¿Puedo agregar una clase nueva al allowlist?

No directamente. El allowlist es output del design system; agregarle clases sin pasar por
@cofoundy/ui rompería el SSOT. Si necesitas una clase nueva, agrégala primero a
@cofoundy/ui, publica el paquete, actualiza la dependencia acá, y corre
pnpm allowlist:write. Solo entonces la clase queda disponible para los docs. La fricción
es a propósito: garantiza que toda clase aprobada exista en algún componente real del
design system.