{"record":{"id":"7a776c814b2eecdc","repo":"nexu-io/open-design","slug":"brand-colors-must-include-at-least-the-backgrou","errorCode":null,"errorMessage":"brand: `colors` must include at least the background, foreground and accent roles, each with a #rrggbb hex","messagePattern":"brand: `colors` must include at least the background, foreground and accent roles, each with a #rrggbb hex","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/brands/validate.ts","lineNumber":77,"sourceCode":"  for (const c of rawColors) {\n    if (!c || typeof c !== 'object') continue;\n    const co = c as Record<string, unknown>;\n    const role = BRAND_COLOR_ROLES.includes(co.role as BrandColorRole)\n      ? (co.role as BrandColorRole)\n      : null;\n    const hex = isStr(co.hex) && /^#[0-9a-fA-F]{6}$/.test(co.hex) ? co.hex.toLowerCase() : null;\n    if (!role || !hex) continue;\n    colors.push({\n      role,\n      hex,\n      oklch: isStr(co.oklch) ? co.oklch : '',\n      name: isStr(co.name) ? co.name : role,\n      usage: isStr(co.usage) ? co.usage : '',\n    });\n  }\n  const roles = new Set(colors.map((c) => c.role));\n  if (!roles.has('accent') || !roles.has('background') || !roles.has('foreground')) {\n    throw new Error(\n      'brand: `colors` must include at least the background, foreground and accent roles, each with a #rrggbb hex',\n    );\n  }\n\n  const logoRaw = (o.logo ?? {}) as Record<string, unknown>;\n  const voiceRaw = (o.voice ?? {}) as Record<string, unknown>;\n  const vocabRaw = (voiceRaw.vocabulary ?? {}) as Record<string, unknown>;\n  const imageryRaw = (o.imagery ?? {}) as Record<string, unknown>;\n  const layoutRaw = (o.layout ?? {}) as Record<string, unknown>;\n  const typoRaw = (o.typography ?? {}) as Record<string, unknown>;\n  const seed = sanitizeSeedOverrides(o.seed);\n\n  return {\n    name: o.name.trim(),\n    tagline: isStr(o.tagline) ? o.tagline : '',\n    description: isStr(o.description) ? o.description : '',\n    sourceUrl,\n    ...(seed ? { seed } : {}),","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/brands/validate.ts#L59-L95","documentation":"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'.","triggerScenarios":"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').","commonSituations":"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.","solutions":["Normalize hexes to #rrggbb (expand #rgb, reject the rest) before validateBrand.","Alias role synonyms and map primary->accent / secondary->foreground when background/foreground/accent are absent.","Improve the extraction prompt to require the three roles with #rrggbb hexes.","Re-prompt with the dropped-color diagnostics so the agent can self-correct."],"exampleFix":"// before\nbrand.colors = [\n  { role: 'primary', value: '#3366aa' },\n  { role: 'background', value: '#fff' },\n  { role: 'text', value: '#111' },\n];\nvalidateBrand(brand, url); // throws\n// after — normalize keys + expand hexes\nfor (const c of brand.colors) {\n  if (!c.hex) c.hex = c.value ?? c.color;\n  if (/^#[0-9a-fA-F]{3}$/.test(c.hex)) c.hex = '#' + c.hex.slice(1).split('').map(x=>x+x).join('');\n}\nmapRoleAliases(brand.colors); // primary->accent, text/secondary->foreground\nvalidateBrand(brand, url);","handlingStrategy":"validation","validationCode":"for (const c of brand.colors ?? []) {\n  if (!c.hex) c.hex = c.value ?? c.color;\n  if (/^#[0-9a-fA-F]{3}$/.test(c.hex)) {\n    c.hex = '#' + c.hex.slice(1).split('').map(x => x + x).join('');\n  }\n  c.role = ({ primary: 'accent', secondary: 'foreground', text: 'foreground' })[c.role] ?? c.role;\n}","typeGuard":"const REQUIRED_ROLES = ['background', 'foreground', 'accent'] as const;\nconst hasRequiredColorRoles = (o: Record<string, unknown>): boolean => {\n  if (!Array.isArray(o.colors)) return false;\n  const roles = new Set(o.colors\n    .filter((c): c is Record<string, unknown> => !!c && typeof c === 'object')\n    .filter(c => typeof c.role === 'string' && /^#[0-9a-fA-F]{6}$/.test(String(c.hex)))\n    .map(c => c.role));\n  return REQUIRED_ROLES.every(r => roles.has(r));\n};","tryCatchPattern":null,"preventionTips":["Expand #rgb to #rrggbb before validation.","Map role synonyms (primary->accent, secondary/text->foreground).","Prompt the extractor for the three required roles with #rrggbb hexes."],"tags":["brand","validation","schema","colors","required-field","llm-output"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}