The image pipeline: Source, Design, Translate, Compose, Review, Publish
My method, my stack and a plan you can hand to a coding agent
The frames I built by hand were not thrown away. They turned out to be the specification, and almost every value we tried to guess instead was already sitting in them.
For the first few dozen products I made the gallery images by hand. Open the 3D file, switch on the collections for the variant, aim the camera, render. Then build the gallery image around it: headline pair, spec table, the callout circles magnifying the parts worth looking at. Then again in the second language.
That produced 270 design files over roughly two years. It is a perfectly good way to make product images and I would defend every one of them. What it is not is a way to reach ten languages, because ten languages across the current catalogue is 1,340 images, and 1,340 frames built by hand is not a project, it is a permanent job.
The number that decides the architecture is a different one. Images are not written once: a render improves, a translation gets corrected, a template gets fixed, and each of those multiplies out along the same three axes again. The operation you perform most often is not the first build, it is the tenth partial rebuild. That is a build system, and treating it as one is the whole idea.
PART ONE OF THREE
The method
Six stages, five invariants, and the parts that survive a change of tools.
SIX STAGES · READ AS A BUILD
- 1
source
3D scene · photo library · generated plate
- in
- whatever actually produces base imagery for this family: a rendered scene, an existing shoot, a licensed library, a generated plate
- out
- one raster per shot, on an alpha channel where the source allows it, at the size the largest consumer needs, written over its predecessor so the file keeps its identity
- unit
- a shot request: family, variant state, camera or crop, output target. Resolved by naming convention, never by internal id
- guard
- the manifest and the source assets must agree before anything runs, and an input on synced storage must be complete and quiet before it is opened
- cost
- minutes of GPU for a rendered shot, near zero for a library lookup. Which is exactly why this stage is a contract and not a technique
- 2
design
design-file exports · structural analysis
- in
- every layout ever built by hand, exported to a structured format
- out
- a small template set, plus the layout rules read out of those files: spacing, alignment groups, growth, crop
- unit
- a layout structure, reduced by exact equality after an explicit decision about what to ignore
- guard
- every value is read, none is guessed. Content and cardinality may become parameters; geometry may not
- cost
- a week of parsing, once, and it sets the quality ceiling for every stage after it
- 3
translate
machine translation · one glossary · a cache
- in
- the source strings lifted out of the design files, one per layout role
- out
- one resolved text set per locale, per product, per shot
- unit
- a source string. The cache key is the string itself, not the pair of product and string
- guard
- hand-corrected entries are never machine-overwritten, and terminology comes from exactly one glossary rather than one per system
- cost
- one call per string the cache has not seen, which is the entire argument for the key above
- 4
compose
templates · layout engine · raster encoder
- in
- the rasters, the resolved text sets, and the template set
- out
- one finished image per product, shot and locale, each carrying a stamp of what produced it
- unit
- an image. Its identity is its path; its freshness is inside the file
- guard
- the layout checker runs inside the same engine that produces the image, against the state that ships rather than the state the design was drafted in
- cost
- well under a second per image, so a full batch is bounded by process startup and not by layout
- 5
review
grouped diff · live view · typed findings
- in
- the composed batch, and the record of what was approved before
- out
- approvals and findings, both keyed by the same stamp that decides freshness
- unit
- an artifact that changed. Anything unchanged since its approval never comes back
- guard
- grouped by layout rather than by product, and the thing under review is the document that ships, not a re-implementation of it
- cost
- the only stage that costs human attention, which is why it is designed around impatience
- 6
publish
channel APIs · staged uploads
- in
- the approved set, plus whatever the channel already holds
- out
- media, metadata fields and per-variant primary images, in a defined order
- unit
- one image mapped onto one object that already exists, re-found before anything new is created
- guard
- match by derived filename, then by recorded id, then by image content with a margin over the runner-up; only the positions the pipeline owns are ever reordered
- cost
- network-bound, and the only stage that can damage something which currently works
One command runs the chain, and any stage boundary is a place to cut it. Every stage has to be re-runnable, which is a requirement rather than a nicety: a stage nobody dares re-run is a stage nobody fixes.
One thing shaped every stage, so it comes before the parts. The hand-built files were not discarded and re-derived from scratch. They are the specification. Every layout value the renderer uses is read out of those exports, and the single most consistent finding of the whole project is that wherever a value got guessed instead, the guess was wrong, and the correct number was already sitting in the file.
Five invariants
Every mistake worth writing down was a violation of one of these.
- The design files are the specification, not a prototype. They are the most precise description of the intended output that exists.
- Nothing is bound by internal identifier. Scenes, cameras, variant states and text roles are located by naming convention, so adding a product is a naming job plus a manifest entry rather than a code change.
- Identity in external systems is re-found, never allocated. A published image is the same object with new bytes, because things downstream hold references your pipeline cannot see.
- Freshness is a property of the artifact. Each finished file carries the fingerprint of what produced it. No build database, so nothing can disagree with reality.
- Measure the thing instead of reasoning about it. The values in this domain are all confidently guessable and quietly wrong.
Where templates come from
A template is a function from content to a laid-out document. Three sources are in common use, and which one fits depends on what you already have rather than on one being better than the others.
Derived from existing design files. Parse what was built by hand and extract the values. Costs a parser and a tolerance for whatever quirks the design tool exports. Pays off in proportion to how much history exists.
Hand-authored in code. A developer writes the templates, ideally next to the person who designs them. Fastest route to something that renders, and often the only sensible one for a new catalogue. The decision it forces is which of code and design file is authoritative, because the cost arrives later, when the two drift and nothing reports it.
Generated by a model. UI generation pointed at a different target: hand a model the reference, get the component back. It changes who writes the template, not what makes it correct, so the acceptance test is the part to get right. Generate from extracted values rather than from a picture alone, because a model given only a screenshot produces numbers that look plausible, and close the loop with a pixel diff against the original export.
What is true of all three is that the reduction below still has to happen. A template per layout is not a template set, whoever or whatever wrote it.
The reduction
This is not a clustering problem, and treating it as one is how you get templates that almost fit. Every step is an exact equality reached after deciding what to ignore:
FROM EVERY EXPORT TO A TEMPLATE SET
- every exportSTART
one file per product, per shot, per locale
whatever exists after years of building layouts by hand, including the ones nobody would build that way today
- per structure÷ LOCALES
a locale is a text set, not a layout
the duplicates that exist only because someone rebuilt a frame in a second language collapse first, and for free
- exact shapes÷ DUPLICATES
compare node trees, not content
same roles, same nesting, same order. No tolerance and no distance metric, because a similarity threshold produces templates that almost fit
- layout shapes÷ ARTWORK
ignore geometry driven by the artwork
leader lines, measurement ticks and anything anchored to the rendered subject vary with the picture rather than with the layout
- templates÷ REPETITION
collapse repetition into cardinality
two callouts is one callout twice; four spec rows is three plus one. What survives this step is genuinely different layouts
Expect one to two orders of magnitude, and expect one collapse you should refuse. A block that appears at two heights could be one template with a position parameter; every parameter that controls a position moves that decision out of the design file and into code.
The refusal in that last note is the tension of the whole stage: any parameter that controls a position moves the decision out of the design file and into code. Parameterise content and cardinality freely, geometry never.
Verification, and where it sits
Review is a stage between composing and publishing for a structural reason: everything before that line can be thrown away and re-run, and once an image is on a channel, other systems have seen it.
FOUR GATES · AND THEIR BLIND SPOTS
STAGE 1, BEFORE ANYTHING EXPENSIVE RUNS
agreement
✓the manifest and the source assets disagreeing: a family renamed, a variant that no longer exists, a camera that was never built. Also an input that is still arriving, on any filesystem that publishes metadata before the bytes behind it.
⊘everything about how the result looks. This gate says the job can run, not that the job is right.
STAGE 4, WHILE THE ARTIFACT IS PRODUCED
layout check
✓text past its column, blocks that collide, a label crossing the line it labels. It runs inside the engine that produces the image and measures the state that ships, not the state the design file was drafted in.
⊘a translation that fits and is wrong. Every failure this gate can see is geometric; nothing in it reads meaning.
STAGE 4, BEFORE A FILE IS WRITTEN
provenance equality
✓work that does not need doing. The stamp inside the existing artifact is compared with the stamp the current inputs would produce: code, fonts, template, sources, resolved text, locale.
⊘anything the stamp does not name, and it fails in the quietest way available. Delete a stage the stamp still refers to and the comparison keeps passing.
STAGE 5, ONCE THE BATCH EXISTS
human pass
✓intent. Whether this is the right shot, whether the headline says the right thing, whether the product reads as cheap. No other gate holds an opinion about any of that.
⊘scale, unaided. Thousands of artifacts are only reviewable if the tool groups identical layouts and remembers what was already approved.
Gate two is only worth running if the document under review and the document being photographed are the same document. A checker that measures an approximation of the output is checking the approximation.
Publishing is a matching problem
The hard part is not uploading but that the images being replaced already exist and other systems point at them. So publishing re-finds before it creates: by derived filename, then recorded id, then image content with a clear margin over the runner-up. Position matching is simpler and wrong, because galleries acquire gaps. Content matching holds only while the stored file still resembles the published one, which fixes the order of any migration: adopt what exists first, replace it second.
PART TWO OF THREE
One concrete stack
What I actually run in each stage, what it costs, and what I would consider instead.
ONE TOOL PER STAGE · AND WHAT IT COSTS
1 · SOURCE
Blender, headless, Cycles on a local GPU
whyOne file holds every set, the materials and each variant as a toggle, so a new shot is a naming job plus a manifest entry rather than a new project.
costA large binary on shared storage, which makes saving and rendering compete, and a GPU backend that lies about being available.
insteadA photo library or a generated plate for families where a model is not worth the modelling time. The stage is a contract, so this is a per-family decision.
2 · DESIGN
Figma, exported as SVG, parsed
whyThe layouts already existed there. Exports carry the geometry the templates need, and the designer keeps working in the tool they know.
costFrozen alignment, historical layer names, and a declared frame width that is not the available space. All handled in the parser, none of it fixable upstream.
insteadModel-generated templates checked against the same exports by pixel diff, which is a good fit once the reduction is already done.
3 · TRANSLATE
A cache keyed by source string, one shared glossary
whyThe same sentence gets the same translation catalogue-wide, and the glossary is the same one the shop copy uses, so terminology cannot fork between systems.
costThe glossary has to live in exactly one place, and a hand-exported copy of it is a silent quality regression waiting to happen.
insteadNothing. This is the one choice I would repeat without thinking about it.
4 · COMPOSE
React and HTML in headless Chrome, encoded with sharp
whyHyphenation, balanced headlines and columns come from the browser instead of from me, and the preview a reviewer opens is literally the document being photographed.
costA browser per worker, process lifecycle to manage, and no path to a serverless runtime.
insteadA vector layout library plus a rasteriser, if and only if the deployment target rules a browser out. Budget a week for hyphenation.
5 · REVIEW
A local tool: grouped grid, live view, keyboard, typed findings
whyGrouping by layout makes a large batch reviewable, and the live view turns a template fix into a page refresh instead of a pipeline run.
costIt is a second application to maintain, and it only stays useful if it opens instantly.
insteadA contact sheet and a spreadsheet, which is worse but real. Anything is better than reviewing in the channel after publishing.
6 · PUBLISH
The commerce platform's admin API with staged uploads
whyIt is the integration surface everything downstream already reads, so publishing there propagates without a second pipeline per marketplace.
costEvery mistake propagates outward, list position carries meaning, and matching existing media is the hardest code in the project.
insteadWriting to each channel directly, which multiplies the identity problem by the number of channels. I would not.
Source: one Blender file and a manifest
There is one Blender file, about 237 textures and 46 product families in it. The manual step it replaces is not the rendering, which the GPU was doing anyway. It is the setup: knowing that a 48-pack means switching on every quantity collection up to 48, that this family is shot in that scene with that camera, and that the output belongs in a particular folder under a particular name.
All of that is now a manifest of 227 jobs, resolved entirely through naming
conventions: a family’s scene is MAIN <name> <family>, its camera is
<family> CAM 1, its collections are <family>/<label>. Nothing is wired by id,
so adding a product is a naming job plus a manifest entry, and a validation pass
says whether the two agree before anything renders.
Three things I would tell anyone setting this up. Enumerate the GPU, do not configure it: setting a compute backend succeeds whether or not it has devices behind it, and the fallback is a correct image about a hundred times slower. Wait for the file to be quiet: a mount publishes new metadata before the last bytes arrive. Overwrite in place, so the storage id survives, which is what stops a republish orphaning every reference downstream.
The catalogue also does not cut along the axes the 3D file does. A product page is a folder, and a folder can be fed by several families: one page carries five thicknesses as variants while nine colours of the same part are a separate page. So a job can name a different output folder than its own family, and a family without a built set can borrow another’s scene and camera. Both are one optional key in the job.
Blender is not the point, though. The stage is a contract: deterministic name, known geometry, alpha where possible, overwrite in place. Families not worth modelling get their base imagery from a photo library or a generated plate, and nothing downstream can tell.
Design: 270 exports, seven templates
Clustering the exports by structure gives 88 exact shapes, then 27 once you ignore dimension-line geometry, which hangs off the product’s outline rather than the layout, and then 7 once you notice that most of what is left is repetition. Two callout circles instead of one. Four spec rows instead of three.
Two examples are worth the specifics. One spec layout declares 1346 px of frame width for a label whose neighbouring column already starts at 1013: trusting it pushes that column 400 px outward and runs a phrase off the edge. And one headline role appears at 1626, 1716, 1774, 1778, 1790 and 1806 px across the catalogue, which is drift, not design.
Alignment arrives frozen into coordinates, so lines that are visibly right-aligned in the editor come out as different left-aligned values and the intent has to be recovered from shared edges. Binding runs in a fixed order, weakest last, because a design tool names a text layer after whatever was first typed into it and keeps that name when the text changes: that is how you get a layer called “100 Microns” containing the words “Great for”. 748 of 756 text nodes carry a usable explicit id, so binding prefers that, then the layer name, then position.
Nothing was redesigned. Every template is the existing layouts with the repetition factored out, which is why the first render looked right instead of looking like a developer’s idea of the brand.
Translate: the glossary is the system
The full account is a separate article, Kompatibel mit. Three things matter at the pipeline boundary: the cache is keyed by the English source string rather than by the pair of product and string, so one sentence has one translation everywhere; hand-written entries are never machine-overwritten; and terminology comes from the same glossary the workflow engine uses for the shop’s copy. There used to be two of those, a hand-exported CSV and the sheet a workflow read live. They drifted, nothing failed, and the translations simply got quietly worse.
Storage: a shared Drive, and what a mount costs
Composed images and renders live on Google Drive, the choice that gets the most raised eyebrows and the one I would still make: the designers and the print side already live there, and Drive ids survive renames and moves, which makes them a real identity to bind to.
The bill arrives at the mount. Reading an EXIF header through it downloads the entire file, so a first run on a fresh machine spent 16 minutes and 1.2 GB answering what size and modified time already answer. The fix is one committed index of 1,784 entries from Drive id to stamp, whose only purpose is avoiding transfers. The artifact stays the source of truth: every composed image carries a stamp of code, fonts, template, sources, texts and locale, and if it matches what would be produced now, the image is skipped.
Compose: React, headless Chrome, sharp
This stage was built twice. The first implementation composed with Satori for type and resvg for the raster, aimed at running in a Cloudflare Worker. It lasted two days.
It was not wasted. Measuring text that way costs about 144 ms per instantiation
because the font buffers are re-parsed each time, which forced two findings worth
keeping: text width scales linearly with font size, within 0.2% across the range
in use, and every string in a language can be measured in one document. A
language went from 4228 ms to roughly 330. Then Satori turned out to give every
output the same clip-path id, so two blocks in one document silently share a mask
and a 926-pixel-wide block got clipped to its neighbour’s box. And German needs
hyphenation, which Satori applies only if something supplies the break positions:
the obvious library compiles its patterns with new WebAssembly.Module, which a
Worker cannot do, so the pattern format got bound directly in about a hundred
lines and checked character by character against the library.
All of it is deleted. Chrome hyphenates from a lang attribute, balances
headlines with one CSS property, and does columns with flexbox. The reason to
keep Chrome is not the typography, though. It is that the preview and the render
are the same page: the layout corrections live in one module that both the
renderer and the preview server inject, so what you look at in a browser is what
gets photographed. Measured difference, 12 pixels out of four million.
Review: the tool that makes 1,340 images finite
GROUPED BY LAYOUT
- ●lifestylede
- ●featuresde
- ●specsde
- ●recommendde
- ●lifestyleen
- ●featuresen
- ●specsen
- ●recommenden
- ●lifestylepl
- ●featurespl
- ●specspl
- ●recommendpl
the real page, not a render
rebuilt on every template edit
open finding · specs
dimension label 8 mm crosses the leader line
- j / k next, previous
- a approve
- x raise a finding
- l live view
- c compare vs Drive
- / search
The live view is the same page the renderer photographs, injected with the same layout corrections: measured difference between preview and render, 12 of 4,000,000 pixels.
Five views of one image behind a single bar, and the fifth is the one that matters: not a render, but the actual page, rebuilt on every edit, with layout warnings live. The design constraint is impatience. A fresh scan of the Drive takes two minutes, and a tool that takes two minutes to open is a tool nobody opens, so it serves a cached listing immediately and refreshes behind you. Findings are typed onto the image and answered from the command line with a reason.
Publish: one integration surface
ONE SET OF RENDERS · SEVEN DESTINATIONS
◉WRITTEN BY PUBLISH
Webshop (B2C)
Ten languages. Gallery images tagged per language, three image metafields, one main image per quantity and colour variant.
B2B shop
English only, one variant per product, no image metafields. What a channel carries is configuration, not a second code path.
◉READ OUT OF THE SHOP BY A WORKFLOW
Amazon gallery
Fed from the shop's media list, where the position decides which image lands in which slot. PNG rather than WebP, which the CDN already handles by content negotiation.
Amazon A+
Wider modules that do not fit the square gallery format. The compositing scene keeps a separate output for them.
eBay
Listings built and published on a schedule, reading the product back out of the shop rather than out of the Drive.
◉TAKEN FROM THE DRIVE BY HAND
B2B catalogue
Laid out by hand. Wants the product on transparency at print size, not the finished square with a headline on it.
Print and packaging
Same again, and the reason renders are produced with an alpha channel rather than flattened onto their background.
Only the first group is what publish means. The rest is why the renders are kept as source material rather than only as finished squares.
The marketplaces read the product back out of the shop rather than out of the Drive: one integration surface, not one per channel. The cost is that a mistake propagates outward instead of staying local, and that the order of images on a product decides which one a marketplace treats as which gallery slot. Only the positions the pipeline occupies get touched, so anything placed by hand stays put.
Publishing re-finds by filename, then Drive id, then a 32×32 greyscale signature where the best match has to be clearly ahead of the second best. That last rung is weak on purpose: it exists for the first run against a shop full of images uploaded by hand, and it breaks as soon as a scene is re-rendered, measured at 28 points of distance against a threshold of 12.
My own hand era left real work behind, both failures arriving from opposite directions. The images I uploaded carried no language tag, and untagged means shown in every language, so the shared closing image was standing in front of the translated ones in nine of them. And a file I renamed in the Drive left 44 media hanging off the wrong product, because publish could not recognise them afterwards, created new ones, and orphaned the old.
PART THREE OF THREE
The plan to copy
A build plan with four blanks in it, written to survive contact with a coding agent.
A coding agent is very good at the code here and quite bad at the two decisions that matter: what is authoritative, and when something is done. Left alone it will infer a value that was sitting in the export, build a database of what it produced, and report six finished stages that have never been run twice. That is a plan failing, not a model failing.
Four things are yours to fill in: the catalogue shape, where base imagery comes from per family, where layouts live, and the channels plus whatever reads from them rather than from your storage. The rest is generic.
SIX WORK PACKAGES · EACH ONE PROVEN BEFORE THE NEXT
WP1 the source contract
buildone command that produces base imagery for one shot request and writes it to a deterministic path, overwriting in place
proverun it twice. The second run changes no bytes and the storage identifier is unchanged
traphard-coding one production method. The contract is name, geometry, alpha and overwrite; how the pixels are made is per family
WP2 the parser
buildread the design exports and emit the geometry as data: baselines, set widths, alignment groups, gaps, crops
proveprint the extracted values for three layouts next to the same values measured off the rendered original
trapinferring a value that is not in the export. If it cannot be read, it stops and asks
WP3 the template set
buildreduce the parsed layouts by exact equality, then implement the surviving templates
proverender each template with its original content and diff against the original export, pixel count printed
trapone template per layout. The reduction is the work; a generated component per export is the same problem with more files
WP4 text resolution
builda cache keyed by source string, a single glossary, and a hand-written flag that wins over the machine
provetranslate the catalogue twice and show the second run makes no calls, then edit one entry by hand and show it survives
trapkeying the cache by product and string, which produces two translations of one sentence
WP5 compose and stamp
buildproduce the images, each carrying a provenance stamp, and skip anything whose stamp already matches
provefull run, then immediate re-run: zero files written. Change one template and show only the affected images rebuild
trapa database of what was built. Freshness lives inside the artifact or it will disagree with reality
WP6 publish, dry first
buildmatch existing remote media by filename, then recorded id, then content; report create versus update; then write
provedry run against the real channel printing every intended action, reviewed by a human before the first write
trapmatching by position, and creating instead of updating. Both look like success and break everything downstream
An agent will report every one of these as finished without the proof line. The proof is the plan; the rest is scaffolding.
PLAN.md
The file the agent re-reads at the start of every session, so it holds context, invariants and the definition of done, and nothing that changes hourly.
# Catalogue image pipeline
## Context
- CATALOGUE: {n} products x {n} shots x {n} locales = {n} artifacts
- BASE IMAGERY: {rendered scenes | photo library | generated plates | mixed}
- LAYOUTS: {tool}, exported as {format}, one frame per {product, shot, locale}
- STORAGE: {where artifacts live}; stable identifier: {what survives a rename}
- CHANNELS: {where images are published}
- CONSUMERS: {what reads from those channels instead of from storage}
## Invariants
1. The design files are the specification. Every layout value is read from an
export. If a value cannot be read, stop and ask. Never infer geometry.
2. Nothing is bound by internal identifier.
3. Identity in external systems is re-found, never allocated. Publishing
updates the object that already exists. It never replaces it.
4. Freshness is a provenance stamp inside the artifact, compared with what the
current inputs would produce. There is no database of what was built.
5. Content and cardinality may be parameters. Geometry may not.
6. Every claim is backed by a printed measurement.
## Definition of done, for every work package
- A command that can be re-run, and that is a no-op when nothing changed.
- A printed measurement that proves the acceptance criterion.
- One paragraph in DECISIONS.md: what was chosen, what it costs, what would
make you change it.
## Order of work
WP1 source contract -> WP2 parser -> WP3 template set -> WP4 text resolution
-> WP5 compose and stamp -> WP6 publish (dry run first, then writes)
Do not start a package before the previous one has printed its proof.
The anti-pattern block
Paste this into the agent’s standing instructions. Each item produces working code and a broken pipeline:
Check yourself against this list before claiming a package is done.
- You guessed a geometry value instead of reading it from the export.
- You treated a declared frame width as the available space.
- You added a database or cache of what was built.
- You matched existing remote images by their position in a list.
- You created a new remote object where an update was intended.
- You concluded a capability exists because configuring it did not throw.
- You allowed a missing font or asset to fall back silently.
- You produced one template per layout instead of reducing the set.
- You built a preview that re-implements the renderer instead of being it.
- You reported a stage complete without running it twice.
One package, one prompt
Every package gets a fresh session and a prompt of this shape. The two lines that do the work are “may touch”, which stops a package becoming a refactor, and the numbered proof:
Read PLAN.md first. Work package: WP2, the parser.
Build: a module that reads one exported layout and returns its geometry as
data. Per text node: resolved font and size, distance between rendered
baselines, set width of the longest line, the alignment group and the edge it
shares, the gap to the block below measured from this block's last baseline.
Per image node: the crop recovered from the fill transform.
May touch: src/parse/**, tests/parse/**. Nothing else.
Rules:
- Every number comes from the export. If a value cannot be read, return null
and list it. Do not infer it. No rounding to "nice" numbers.
- Node binding order: explicit identifier, then layer name, then position.
Print how many nodes resolved at each level.
Proof required before you call this done:
1. Run over every export. Print node count, resolution level counts, and every
field that came back null.
2. For three random layouts, print extracted values beside the same values
measured off the rendered original, with the difference.
3. State plainly which fields you are not confident in.
Do not proceed to WP3.
Generating the templates
WP3 is where model generation genuinely helps, and it is the same motion as UI generation. What makes it safe is that the reference is not a picture and the acceptance test is not an opinion:
Inputs: reference.svg (authoritative for every value), extracted.json (WP2
output), reference.png (how it should look).
Rules:
- Every number comes from extracted.json. Do not read measurements off the
image. Props may carry content and cardinality, never positions or sizes.
- Emit one component and its prop types. No abstraction spanning templates.
Loop:
1. Render the component with the reference content at the reference size.
2. Diff against reference.png. Print the differing pixel count and the
bounding box of the largest differing region.
3. Above {threshold}, fix the single largest region and repeat.
Stop under {threshold}, then freeze: further changes are design decisions and
belong in the design tool.
Running it
One package per session with fresh context, because the plan file carries context and a long conversation carries drift. A branch per package, and read the DECISIONS.md paragraph before merging: if it is vague, the work is vague. And never let WP6 write on its first run. A dry run against the real channel, printing every intended create and update with the reason it matched, is the highest-value review in the project.
The pipeline did not replace the design work, and it was never going to. The design file still decides what a product image looks like, the 3D file still holds the set and the lighting, and both are jobs for a person with taste. What got automated is the part that was the same 1,340 times, and the way to automate it was to stop guessing at what the hand-built files meant and start reading them.