nexu-io/open-design · error · Error

brand: `colors` must include at least the background, foregr

Error message

brand: `colors` must include at least the background, foreground and accent roles, each with a #rrggbb hex

What it means

Thrown by validateBrand after color filtering when the surviving colors do not include all three required roles: 'background', 'foreground', and 'accent'. Each required role must survive with a hex matching /^#[0-9a-fA-F]{6}$/. Colors whose role is unknown or whose hex is malformed are silently dropped, so the failure is often 'looks present but was filtered out'.

Source

Thrown at apps/daemon/src/brands/validate.ts:77

  for (const c of rawColors) {
    if (!c || typeof c !== 'object') continue;
    const co = c as Record<string, unknown>;
    const role = BRAND_COLOR_ROLES.includes(co.role as BrandColorRole)
      ? (co.role as BrandColorRole)
      : null;
    const hex = isStr(co.hex) && /^#[0-9a-fA-F]{6}$/.test(co.hex) ? co.hex.toLowerCase() : null;
    if (!role || !hex) continue;
    colors.push({
      role,
      hex,
      oklch: isStr(co.oklch) ? co.oklch : '',
      name: isStr(co.name) ? co.name : role,
      usage: isStr(co.usage) ? co.usage : '',
    });
  }
  const roles = new Set(colors.map((c) => c.role));
  if (!roles.has('accent') || !roles.has('background') || !roles.has('foreground')) {
    throw new Error(
      'brand: `colors` must include at least the background, foreground and accent roles, each with a #rrggbb hex',
    );
  }

  const logoRaw = (o.logo ?? {}) as Record<string, unknown>;
  const voiceRaw = (o.voice ?? {}) as Record<string, unknown>;
  const vocabRaw = (voiceRaw.vocabulary ?? {}) as Record<string, unknown>;
  const imageryRaw = (o.imagery ?? {}) as Record<string, unknown>;
  const layoutRaw = (o.layout ?? {}) as Record<string, unknown>;
  const typoRaw = (o.typography ?? {}) as Record<string, unknown>;
  const seed = sanitizeSeedOverrides(o.seed);

  return {
    name: o.name.trim(),
    tagline: isStr(o.tagline) ? o.tagline : '',
    description: isStr(o.description) ? o.description : '',
    sourceUrl,
    ...(seed ? { seed } : {}),

View on GitHub (pinned to 5be4028344)

Solutions

  1. Normalize hexes to #rrggbb (expand #rgb, reject the rest) before validateBrand.
  2. Alias role synonyms and map primary->accent / secondary->foreground when background/foreground/accent are absent.
  3. Improve the extraction prompt to require the three roles with #rrggbb hexes.
  4. Re-prompt with the dropped-color diagnostics so the agent can self-correct.

Example fix

// before
brand.colors = [
  { role: 'primary', value: '#3366aa' },
  { role: 'background', value: '#fff' },
  { role: 'text', value: '#111' },
];
validateBrand(brand, url); // throws
// after — normalize keys + expand hexes
for (const c of brand.colors) {
  if (!c.hex) c.hex = c.value ?? c.color;
  if (/^#[0-9a-fA-F]{3}$/.test(c.hex)) c.hex = '#' + c.hex.slice(1).split('').map(x=>x+x).join('');
}
mapRoleAliases(brand.colors); // primary->accent, text/secondary->foreground
validateBrand(brand, url);
Defensive patterns

Strategy: validation

Validate before calling

for (const c of brand.colors ?? []) {
  if (!c.hex) c.hex = c.value ?? c.color;
  if (/^#[0-9a-fA-F]{3}$/.test(c.hex)) {
    c.hex = '#' + c.hex.slice(1).split('').map(x => x + x).join('');
  }
  c.role = ({ primary: 'accent', secondary: 'foreground', text: 'foreground' })[c.role] ?? c.role;
}

Type guard

const REQUIRED_ROLES = ['background', 'foreground', 'accent'] as const;
const hasRequiredColorRoles = (o: Record<string, unknown>): boolean => {
  if (!Array.isArray(o.colors)) return false;
  const roles = new Set(o.colors
    .filter((c): c is Record<string, unknown> => !!c && typeof c === 'object')
    .filter(c => typeof c.role === 'string' && /^#[0-9a-fA-F]{6}$/.test(String(c.hex)))
    .map(c => c.role));
  return REQUIRED_ROLES.every(r => roles.has(r));
};

Prevention

When it happens

Trigger: validateBrand receives a brand whose colors array is missing one of the three roles, has the role but with a malformed hex ('#abc', 'rgb(...)', '#000'), has the role under a different key ('value' instead of 'hex'), or has the role with extra casing ('Background').

Common situations: LLM emits 3- or 8-character hexes; agent uses 'value'/'color' instead of 'hex'; role names are uppercase or localized; only 'primary'/'secondary' roles are emitted with no background/foreground/accent; oklch-only colors with no hex.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/7a776c814b2eecdc. Report an issue: GitHub.