Skip to main content

Notes from building an AI design collaborator · part 10

The AI told me our brand colour with total confidence, and it was wrong

· 4 min read

I was building the thing that reads a product's real colours and spacing off its own code, and early on I did the lazy version. I just asked the model. "What's the primary colour here?" It told me instantly, a hex value, confident and specific and wrong. Not wildly wrong, which would have been fine because I'd have caught it. One shade off. The worst possible kind of wrong.

I didn't notice for about a week. By then that almost-right blue had gone into a pile of wireframes, quietly making everything look a little bit not-our-product in a way nobody could put their finger on. That's the whole danger with a made-up fact. It doesn't announce itself. It sits there being nearly true while everything built on top of it inherits the nearly.

So I stopped asking it for facts

The rule I landed on is a little strict and I've kept it ever since. The model never touches the ground truth.

What the model is genuinely brilliant at is writing code, so I let it do that instead. It looks at the product, works out how this particular stack stores its colours and spacing, and writes a small script to pull them. Then the script runs and reads the actual values. The model wrote the reader. The reader read the facts. At no point did I ask the model to remember a colour.

The hard part is "where", not "what"

Which sounds like it moves the problem rather than solving it, so let me be concrete about the bit the model is actually doing.

Design values are never in one place, and every stack hides them somewhere different. On ours the palette and the spacing scale live in the framework config, the type scale is in a stylesheet, and neither tells you which of those values the product actually uses, because that's decided by the classes each component applies. A theme file might list forty colours when the app uses eleven.

So the model's job is reconnaissance. Find the config, find the stylesheets, work out how they combine, then write something dull like this:

// Resolve the theme, then count what the components actually apply.
const theme = require('./tailwind.config.js').theme.extend
const used  = new Map()
for (const file of componentFiles) {
  for (const cls of readClassNames(file)) {
    const token = resolveToken(cls, theme)   // "bg-blue-600" -> colors.blue.600
    if (token) used.set(token.name, token.value)
  }
}

Nothing clever. That's the point. Once it exists, the answer to "what's our primary blue" is whatever that map says, and it will still be right next month when someone changes it, because the script gets re-run rather than the answer being remembered.

It sounds like a pedantic distinction and it completely isn't. When the model guesses, you get a vibe with a hex code stuck on it. When the model writes a script and the script reads the file, you get the value. Same model, same effort, and one path can hallucinate while the other simply can't, because there's no step left where anyone is asking it to recall a fact instead of go and fetch one.

There's a corollary I only understood later. Your own documentation is not ground truth either. We had a perfectly respectable written document describing our colours, and when we finally diffed it against the code it was wrong in a couple of places. A human wrote it accurately, and then the code moved. Same failure as the model's memory, just slower.

Where it still bites

I want to be honest that this makes the extraction safe, not everything. Judgment sneaks back in at the edges, and it does it while wearing the costume of a measurement.

Inferring a spacing scale is the clearest case. The script gives you the real numbers, and the real numbers are 4, 8, 12, 16, 18, 24, 32, and 40, because somebody once needed an 18. Saying "the scale is a 4-point scale and the 18 is a mistake" is a judgment. A defensible one. Still a judgment. Naming a component is worse, because a name is an opinion with a straight face.

So those don't get to sit in the same bucket as the measured values. Anything derived rather than read is labelled as derived, kept separate, and carries what it was inferred from, so the next person can disagree with the inference without having to re-do the measurement. Quarantine, not deletion. Pretending a guess is a fact is the same mistake I started with, in nicer clothes.

But for anything the product has actually written down somewhere, the rule holds, and it's saved me from shipping a confident wrong answer more than once. So I don't ask the model what the colour is anymore. I ask it to write the thing that goes and reads it, and I let the file have the last word.


Part of a series on building an AI collaborator for our design team at Xflow. Each post stands on its own.

  • ai
  • tooling

The work behind the series

Designing the Instructions

This post is one thread out of a three-month project: an AI design collaborator for a payments team. The case study is the whole of it: what worked, what broke, and what is still unproven.

Read the case study →