AI-Driven Development

Which artifact is actually the design

When several artifacts describe one screen, only the highest-fidelity one is read by codegen. Editing a lower tier looks like a change and reaches nothing.

MobileByteSensei 10 min read
The curve the moulds are taken from, and the amended drawing in the corner
aidesigncodegen

A screen is described in three places: a structured specification, a rendered preview, and a high-fidelity mockup. All three are in the repository. All three are about the same screen.

Somebody adjusts the specification, regenerates, and nothing they changed appears.

The case this post is about

One screen described in three places, and only one of them is what gets built:

stitch markup    the generated design — highest tier when present
LLM preview      the rendered HTML preview — second
ui.yaml / SPEC   the structural spec — LAST, and the one everybody edits

The recolour I made to fix a screen's palette went into ui.yaml. The screen had a stitch design on disk, so codegen read that and ignored my edit entirely. The change looked real — the spec said the new colour — and reached nothing.

This post follows that one edit, because "the spec layer is the source of truth" and "ui.yaml is the source of truth" are different claims, and the second one is false whenever a higher tier exists.

The symptom

The change was real, the file was saved, the generation ran, and the output is unaffected. There is no error to read, because nothing failed — the generator consulted a different artifact, which did not change, and produced the same thing it produced last time.

The second thing is that the edited artifact now looks current and is not. Somebody reading the specification sees the new value, believes it describes the screen, and is wrong in a way that is invisible from inside the file. The lie is not in the content; it is in the assumption that this file is the one that counts.

The third is how reasonable the edit was. The specification is the structured, readable, diffable artifact — the one that looks most like a source of truth, and the one a careful person would reach for first. Preferring it is good instinct applied to the wrong question.

The fourth is that the two artifacts drift apart permanently from that moment. Each subsequent edit to the lower-fidelity file widens the gap, and because neither is generated from the other, nothing in the system ever notices. Six months later they describe different screens and both look maintained.

The three artifacts are not peers, and the hierarchy between them is usually implicit — established by whichever one the generator happens to read, discovered by whoever first notices their edit did nothing. Nobody decided it; it fell out of an implementation detail and then hardened into a fact about the project.

That is the part that costs. A single ignored edit is an afternoon; a specification that has been diverging from the real design for half a year is a document somebody will read and act on, and the action will be wrong in ways that have nothing to do with the original change.

Why the obvious fix failed

Keep them in sync by hand. The intention everybody has. It requires remembering which artifacts exist for a screen, and the number varies per screen — some have a mockup and some do not — so the rule is conditional and the condition is invisible at edit time.

Delete the redundant ones. Appealing, and it throws away real work. The high-fidelity mockup carries visual decisions nothing else records; the specification carries structure and behaviour the mockup cannot express. They are not duplicates, they are different projections of one thing.

Generate the lower tiers from the higher. Correct in direction and hard in practice. Deriving a structured specification from a rendered design means inferring intent from pixels, which produces something plausible and unreliable, and the unreliability is worst exactly where the detail matters.

Make the generator read all of them. Then conflicts have to be resolved, and resolution needs a rule — which is the tier order, arrived at the long way round after building a merge mechanism nobody wanted.

The actual mechanism

Declare a tier order, resolve against what is present on disk, and make the highest present tier the one that wins.

tier 1   high-fidelity mockup       wins whenever it exists
tier 2   rendered preview           wins when there is no mockup
tier 3   structured specification   the floor — always present, often not the design

The important and slightly uncomfortable part is that the structured file is the floor, not the source of truth. It is the most readable artifact and the one most people assume is authoritative, and for visual questions it is the last resort — consulted only when nothing better exists for that screen.

What this does not mean is that the specification is unimportant. It holds structure, behaviour, state, the contract for what the screen does. The tier order governs one axis — what the screen looks like — and on that axis the richer artifact wins because it is the one carrying the decisions.

That axis-by-axis split is what makes the rule tolerable rather than arbitrary. It is not that one file outranks another in general — it is that for each kind of question there is one artifact carrying the real answer, and the others are carrying a copy of it that may or may not have been kept up.

Two artifacts, each complete about its own question and silent about the other's

The sub-case that produces the original symptom is the reverse-direction edit. Changing a colour in the specification while a mockup exists is a change to an artifact nothing reads for colour. It will be saved, reviewed, merged, and will have no effect — and nothing in the workflow says so, because from the file's own point of view the edit succeeded.

The remedy is to make the edit direction explicit: to change what a screen looks like, regenerate the highest present tier. That is more expensive than editing a field, which is exactly why people do not do it, and the cost is the honest cost of changing a design rather than describing one.

There is a second, quieter version of the same mistake that is worth naming because it looks like diligence: recolouring the mockup by hand so it matches the specification somebody already edited. Now the two agree, the screenshot looks right, and the underlying structure — what elements exist, what states they have — never moved. The artifact looks current without being current, which is a worse state than visible disagreement.

Immaculately maintained, with the amendment sitting inside wood that was never cut away

The fix

declare   the tier order once, as a resolution rule rather than a convention
resolve   per screen, from what is actually present on disk
read      the highest present tier for visual questions; the spec for structure
guard     refuse an edit to a lower tier's visual fields when a higher tier exists

Resolution is a file-system question, so it is answerable before anything is generated — and the guard falls straight out of it:

const TIERS = [
  { tier: 1, name: "mockup",  file: (s: string) => `design/${s}/mockup.html` },
  { tier: 2, name: "preview", file: (s: string) => `design/${s}/preview.html` },
  { tier: 3, name: "spec",    file: (s: string) => `design/${s}/ui.yaml` },
] as const;

export async function resolveTier(screen: string) {
  for (const t of TIERS) {
    if (await exists(t.file(screen))) return { ...t, path: t.file(screen) };
  }
  throw new Error(`${screen}: no design artifact at any tier`);
}

/** The guard: a VISUAL edit below the winning tier is a no-op in waiting. */
export async function assertWritable(screen: string, editing: string, fields: string[]) {
  const winner = await resolveTier(screen);
  const VISUAL = new Set(["colour", "color", "spacing", "typography", "elevation", "radius"]);
  const visual = fields.filter((f) => VISUAL.has(f));
  if (visual.length && editing !== winner.path) {
    throw new Error(
      `${screen}: ${visual.join(", ")} edited in ${editing}, but ${winner.name} ` +
      `(${winner.path}) is the tier codegen reads. Regenerate that instead — this edit ` +
      `would be saved, reviewed, merged, and have no effect.`,
    );
  }
}

On a screen that has been through design:

$ resolve profile
tier 1 · mockup · design/profile/mockup.html

$ edit design/profile/ui.yaml --fields colour,spacing
error: profile: colour, spacing edited in design/profile/ui.yaml, but mockup
       (design/profile/mockup.html) is the tier codegen reads. Regenerate that
       instead — this edit would be saved, reviewed, merged, and have no effect.

$ edit design/profile/ui.yaml --fields states,on_click
ok                                 # structure lives in the spec; not a visual field

The second line is the one that makes this usable. Tiers are not uniform across a project — a screen that has been through design has a mockup; one that has not, has a specification and nothing else — so the resolution has to be a per-screen question answered from the file system, not a project-wide setting.

The fourth line is the guard, and it has to be mechanical to be worth anything. A convention that says "regenerate the mockup instead" is exactly the kind of rule that holds while somebody remembers, and the edit it is preventing is one that produces no error of its own. A refusal at write time is the only form of it that survives contact with a hurry.

What the guard should say matters as much as that it fires. "This file is not the design for this screen; the mockup is, regenerate it" is actionable. A generic rejection produces a workaround, because the person had a real change to make and the tool has told them no without telling them where to go.

The third line deserves a note too, because it is the half people skip when adopting this. Reading the highest tier for visual questions is only correct if something still reads the specification for structure — a system that resolves to the mockup and stops has traded one silent authority for another, and the behaviour the spec described is now the thing going unread.

How we proved it

The ignored edit reproduces on demand. Change a visual field in the specification for a screen that has a mockup, regenerate, and diff the output: unchanged. That takes two minutes and settles the question of which artifact is being read, which was previously a matter of belief.

An amendment to the drawing, and the moulds that keep coming out the same

The tier resolution is directly observable because it is a file-system query. For any screen you can list what exists and say which tier wins, before generating anything — and the surprise, when there is one, is usually that a screen has a mockup nobody remembered.

What I have not measured is how much drift accumulated before the rule existed. The specifications and the mockups disagreed in an unknown number of places, and reconciling them means deciding case by case which was right — work I have done opportunistically rather than as a sweep, so I cannot give you a number.

What I have not solved: regenerating a high-fidelity tier is expensive enough that the guard creates real friction for small changes. Adjusting one colour means regenerating a whole mockup, and I do not have a mechanism for a targeted edit at the top tier that preserves everything else. The honest state is that the cheap path is wrong and the right path is slow.

And a limitation of the tier order itself: it answers which artifact wins, not whether the artifacts agree. Two tiers can describe different screens entirely and the rule is perfectly happy — it just reads the top one. Detecting disagreement is a separate mechanism, and without it the lower tiers can rot quietly while the system behaves correctly.

The thing that reframed it was noticing that "source of truth" is usually said about a file and is really a property of a question. The specification is the source of truth for what a screen does. The mockup is the source of truth for what it looks like. Asking which file is the source of truth produces an argument; asking which file answers this question produces a rule.

That framing also explains why the mistake is so persistent. Everybody involved is applying a correct principle — edit the structured, readable, version-controlled artifact — to a question that artifact does not answer. It is not carelessness, and telling people to be careful does nothing, which is why the resolution has to live in the tooling.

What this generalizes to

When several artifacts describe one thing, something is already choosing between them, and it is usually not written down.

The shape recurs everywhere: a schema and a migration and an ORM model; a design system's tokens and the stylesheet and the component's own overrides; an API specification and the handler and the generated client. In each case one of them is what actually runs, the others are descriptions, and people edit whichever is most pleasant to edit.

So the question worth asking of any such set is which one is read, for which question, and what happens to an edit to the others. If the answer to the last part is "nothing, silently", that is not a documentation gap — it is a guard that has not been written, and the edits it would have caught are already in the repository looking like changes.

One post a week, no digest

New writing when it lands. No roundups, no course pitch, no "10 tips".

Follow by RSS