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
- 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.
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
- 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.
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
- brand: missing required `name`
- brand is not a JSON object
- brand.json failed validation: ${errorMessage(err)}
- invalid brand id: ${brandId}
- invalid brand id: ${id}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/7a776c814b2eecdc.
Report an issue: GitHub.