The drawing of a room is not decoration. It is what the decisions settled in that room look like. For it to work, a color has to get in, and then it has to be told which surface it is on. The first half ships today, three ways. The second half is a field in a JSON blob that nothing validates, nothing indexes, and no hand-typed color ever gets.
Cabinets, counter, floor and wall paint are settled. The drawing follows.
The colors themselves. A color is a row in zone_memories with memory_type='color', holding a hex, a title, a free-text brand and a soft paint_id. There is a real paint catalog behind it, table paint_colors, with typed brand, name, code, hex_screen, LRV and collection.
The drawing, and the link between a color and the surface it paints. A recolorable room illustration does not exist in any form, and neither does a queryable answer to "what color is on the kitchen cabinets". Frames 8 and 9 are that link.
The two images here are placeholder blocks, not the illustration. Only the proportions and the recolor are being argued.
The first three. Extraction from a saved photo works two ways: a client-side palette generator in extract_colors_dialog.dart, and POST /api/colors/extract-palette, which returns {hex, role, confidence} per entry. Catalog search sits on paint_colors, and GET /api/colors/match-hex already does nearest-match by delta-E in CIELAB. Typing it in is add_memory_sheet.dart.
The fourth. There is no color spec-field type: the seven are text, long_text, number_with_unit, boolean, single_select, multi_select and dimensions (spec_field_registry.dart:21-29). Eighty-eight color-shaped field keys sit in data/library/, seventy of them typed text, so the value lands in specification_fields.value as free text and nothing parses it into a hex or looks it up against the catalog that is sitting right there.
The fourth door is also the only one that fills the drawing without asking. The other three all require the homeowner to have already decided to go and add a color.
Two of these are not in the catalog. They still keep, as a color with a hex and no name, because a color you can see is worth more than a color we can spell.
Both halves. A client-side palette generator in extract_colors_dialog.dart, and POST /api/colors/extract-palette, which returns {hex, role, confidence} per entry. The nearest-catalog line under each name is GET /api/colors/match-hex, delta-E in CIELAB against paint_colors.
The third value the API returns has no place on this screen. 78% confident is a number a homeowner cannot act on and will worry about anyway, and the honest form of it is already in the caption — looks like the cabinets against not sure where. A machine's certainty is the machine's business.
Two of the six match nothing in the catalog, and the screen keeps them rather than dropping them. This is the room's palette, not the paint aisle: the floor stain and the grout are colors in the drawing whether or not anybody sells them in a can.
Pewter Green on the cabinets is the same color, same surface, as what's already kept for this room (frame 6). The extractor returns a role with every entry, so this door has what it needs to say so — nothing in color_extract_screen.dart checks for it today. Non-blocking, same as the catalog search two frames over: a match is worth saying, not stopping.
All of it. paint_colors carries typed brand, name, code, hex_screen, LRV and collection, and GET /api/colors/match-hex does nearest-match by delta-E in CIELAB — which is what makes one field able to take a hex and a name without a mode switch.
The alternative was brand, then collection, then a grid, which is how a paint manufacturer organizes a catalog and not how anybody arrives at one. A person turns up with a code off a lid or a color off a screen. Both of those are one box.
Light reflectance is what decides whether a north-facing kitchen reads as the color on the chip. It is already a typed column, it is already on every row, and it costs nothing to show. The brand's marketing collection is not on the row for the same reason.
Someone here is still deciding — comparing a hex against what's already on the wall, not confirming a color they already have in hand. color_find_screen.dart:205-207 says so in its own comment: shown so nobody adds a second color that's nearly the first, and nothing stops them if they meant to. The other three doors get firmer as the act gets more certain: a photo of the room and a settled decision both know exactly what's already there.
Which surface it's on. Nothing asks you that yet. So a color you type by hand lands in this room's palette, and the drawing has nowhere to put it.
add_memory_sheet.dart:476-490 writes hex, brand, paint_id and a tintType. Four values, and no role. The two frames on either side of this one both produce a surface; this one cannot, and it is the door most people will use.
It would have been easy to draw the surface picker here and let the page read as finished. It is not finished: the form does not write a role today and the enum has no counter and no backsplash. Drawing the gap where a reader will meet it is worth more than drawing a screen nobody has built.
A floor stain has no LRV, no collection and often no code a catalog would recognize. The floor stain Nora picked is the only handle that will still mean something in eighteen months, and it is a column that already exists.
Every other door knows a surface — a settled decision has one, a search result and an extracted swatch both carry one. This form doesn't ask, so there is no surface to match against and nothing honest to say. The refusal above is the reason; a duplicate check here would have to guess the surface first, which is exactly the guess this door was built to avoid.
It shows the value that is in the box and it opens frame 7. That is deliberate placement rather than convenience: a field whose value is a color should draw that color anyway, so the affordance costs nothing and the wheel never has to be advertised. Nothing else on this page points at it.
Four decisions in this room have a color in them. Take any of them and it comes across knowing where it goes.
The other three doors all need you to decide to go and add a color. This one runs the moment a decision is settled, which is the difference between a drawing you maintain and a drawing that is simply true.
Every row here is a drawing. There is no color spec-field type — the seven are text, long_text, number_with_unit, boolean, single_select, multi_select and dimensions (spec_field_registry.dart:21-29) — so a settled color lands in specification_fields.value as free text and nothing parses it into a hex. Eighty-eight color-shaped field keys sit in data/library/ with seventy of them typed text.
Browsing to this screen and taking a color off it works today, including the suppression above: color_from_decision_screen.dart:64-65 computes _identity(title, surface) and disables an already-kept row, and its copy is exactly "already on the board" (app_en.arb:362) — the strongest of the four doors, because a settled decision is the strongest claim to already knowing what you have. What the paragraph above is about is the trigger, not the screen: nobody has to have visited this door for the storage question to matter.
This page has said in prose since it was written that the fourth door is the one that fills the drawing in on its own. That claim is a sentence until somebody sees two rows carrying Cabinet and Wall without anybody having chosen a surface for them. The deck draws unbuilt things elsewhere for the same reason — mockup 40's People tab has no table behind any of it.
Counter material has a color and no surface to hold it, which is frame 8's gap arriving from a different direction. Island electrical has no color in it at all and is shown rather than filtered out, because a list that silently drops the decisions it cannot use teaches somebody that the room has fewer decisions in it than it does.
A screen isn't a paint chip. If somebody is about to buy this, check the code against a real one first.
Frame 2 stays at four doors and none of them is this. A person who knows their color by name, by code or by photograph never meets the wheel, and a person who has none of those taps a swatch that is already showing them the value they are about to change. The alternative — a fifth door reading Pick it by eye — puts guessing on the same shelf as the three ways that start from something real.
It is the one control in this set that invents a value nobody measured. The other three start from a photograph of the actual room, a code off a lid, or a chip in a hand; this starts from an uncalibrated screen, and the nearest catalog match to a dragged thumb is nearest to the drag. That is why it is behind a swatch and why the callout is inside the phone: the honest version of a wheel is one that tells you what it is not.
The whole tail. The closest-paints card is GET /api/colors/match-hex, delta-E in CIELAB, the same call frame 3's captions make. What is new is the ring, the track and the thumb, which is one painter and one gesture and no new storage: a color picked here keeps exactly as a hand-typed one does, so it gets no code and no surface either.
The seven chips are the real vocabulary: wall | trim | accent | ceiling | floor | cabinet | other, at ai/extract_palette.go:16,81, and again in the same file's system prompt at :45-53. It is never a column and never a shared constant — the only other place the seven appear is a Dart doc comment on CanvasItemResponse.role (decision_detail_response.dart:673-675), which describes them rather than enforcing them. It is a field in the AI extractor's JSON schema, written into an unvalidated JSON string in zone_memories.metadata text (0001_init.sql:1810-1821). Unvalidated, unindexed, unqueryable: the only indexes on the table are on project_id and zone_id, so you cannot ask what color is on the kitchen walls without scanning and parsing every row.
This picker. The manual color form writes hex, brand, paint_id and a tintType, and no role at all (add_memory_sheet.dart:476-490). Counter and backsplash are not in the enum, so both chips on the clay card are new values, not new UI.
Which is the reason frame 1 cannot be built on what is there. A drawing fed by the extractor alone recolors from a machine's guess and never from what the homeowner told it.
Two colors are on the wall.
The rows themselves. Every field drawn here is on the color today: hex, title, free-text brand and a soft paint_id pointing at paint_colors, which is where the code and the LRV come from.
Both problems on the Needs a look card, and the app's ability to see either. A duplicate surface is not detectable when the role lives in an unvalidated JSON string with no index, and "needs a surface" cannot be a state when the manual form never writes a surface in the first place. Every color a homeowner types by hand lands on that bottom row.
The resolution is one line and two words on purpose. A conflict between two paint colors is not an error, it is a question nobody has answered yet, and it should not arrive as a warning.
The whole of In this room. Decisions in the zone, zone memories (images, colors, notes), space questions and answers, coach observations, and coach notes, which are filtered on the client today. Per-zone spec overrides read decision-first. Every one of those is already scoped to a single space with no backend work.
Kept for the whole house, and each of the three costs something different. Drawings: project_documents has no zone_id column at all. Money: allowances are decision-scoped and quote line items carry no zone, but a per-room rollup is derivable through decision_zones. Care: a maintenance task's room lives only in the English of its title, so it is one column plus hand-tagging 53 library tasks.
Nobody yet, and not in a small way. There is no contacts table anywhere in the schema. People is greenfield.
Frame 1 is the promise and frame 3 is the reason it cannot be kept yet. The gap is not "color support". Color support ships. The gap is that a color does not know what it is painted on, and that fails in five separate ways rather than one.
wall | trim | accent | ceiling | floor | cabinet | other is at legacy/functions/api-gateway/ai/extract_palette.go:16,81 — the AI extractor's output type and its JSON schema — and spelled out again in the same file's system prompt at :45-53. The only other appearance in the codebase is a Dart doc comment on CanvasItemResponse.role (decision_detail_response.dart:673-675). So it is not a column, not a constant shared with Dart, and not a validated value anywhere downstream: a comment that repeats a list is not a second declaration of it.zone_memories with memory_type='color', and everything about it beyond the title lives in a metadata text column (0001_init.sql:1810-1821). There is no color table. The write path requires only memory_type and title and stores metadata as an opaque string, so nothing checks that the role is one of the seven and nothing would notice if it were not.paint_id and a tintType, and no role (add_memory_sheet.dart:476-490). Only the AI extraction path sets one. So a drawing built on today's data recolors from a machine's guess and never from what the homeowner said.floor is annotated in the prompt as often not being a paint at all. A kitchen's two largest surfaces are stone and tile, and neither has a value to be assigned to.spec_field_registry.dart:21-29). Eighty-eight color-shaped field keys are defined in the library, seventy of them typed text, so a value like "Sherwin-Williams Alabaster" lands in specification_fields.value as free text and nothing parses it into a hex or looks it up. Frame 2's fourth door is this, and it is the only door that fills the drawing without being asked.What is not missing is worth saying plainly, because it changes the size of the job. There is a real paint catalog: table paint_colors with typed brand, name, code, hex_screen, lrv and collection (0001_init.sql:1275-1294), and a working nearest-match endpoint, GET /api/colors/match-hex?hex=&top=&max_delta=, matching by delta-E in CIELAB. Pulling colors out of a saved photo works two ways today, a client-side palette generator and POST /api/colors/extract-palette, which returns {hex, role, confidence} per entry. The hard, taste-dependent parts are built. The missing piece is a typed, validated, queryable link from a color to a surface, and a way for a decision to make one.
Frame 5's In this room is not aspirational. All of it is already scoped to a single space with no backend work at all: the decisions in the zone, the zone memories, which covers images, colors and notes, the space questions and their answers, the coach observations, the coach notes, which are filtered on the client today, and the per-zone spec overrides, readable decision-first.
The three under Kept for the whole house cost three different things, and lumping them together as "not scoped yet" hides that:
project_documents has no zone_id on it at all. Adding one is the whole shape of the work, and then deciding what a sheet spanning four rooms does.decision_zones. No new storage, a new read.trade: "appliances" and no room field anywhere. That is one column plus hand-tagging 53 library tasks, and the content half is the larger half.One dead end worth naming so nobody finds it and gets hopeful: the appliances table has a zone_id column (0001_init.sql:90) and no handler, no route and no Dart model. It is a schema artifact of a feature that was never built.
Everything below the surface line is client work against endpoints that are already in the gateway, which is worth stating plainly because "color support" sounds like a backend project and is not one.
GET /api/colors/lookup and GET /api/colors/match-hex are live, and a color keep's swatch_hex and paint_code both reach a synced client today: the gateway lifts a checked hex out of zone_memories.metadata and joins the code through paint_id, without sending the blob.POST /api/colors/extract-palette does the work server-side and attaches the catalog matches, so there is no camera and no client-side palette library. But it takes image_base64 and mime_type and has no path that accepts a stored ref, so a picture already sitting in the room's own storage gets pulled down, re-encoded a third larger, and pushed back up to be looked at. On cellular that is most of the feature's cost in one request. A ref-shaped variant of the endpoint is the only backend work any of the live doors would benefit from, and it is an addition rather than a change. The other thing that would not ship as drawn is the looks like the cabinets caption, which is the extractor's role and is precisely the field frames 8 and 9 spend three annotations explaining nothing can store or query.index.html already spends green on "in the six-week proof" and indigo on "deferred", and a badge reusing them for "exists in code" would mean two different things on two pages._ds/hc.css under body class="hc-air", along with the rhythm, the stacked row, the nav dock and the annotation block. The bar is _ds/xnav.css and _ds/xnav.js, unmodified. Every card carries the hairline; there is no other kind.#CDD2CA, Pewter Green #5F665B, Warm oak #B4915F, Alabaster #E4DED2 and Hale Navy #3C4A54 all come from _ds/FICTION.md. Alabaster and Hale Navy exist only to carry frames 3 and 4, and their values are pinned there because this page is the one they serve. Pewter Green's swatches used to read #93A48F here, which is a third value for a paint that already had two; it is the arbiter's now, on the palette rows and in the settled drawing alike.OPEN_QUESTIONS.md Q1.DB_DIAGRAM.md §3.4 already tables color as a real entity — COLORS, referenced by COLOR_PLACEMENTS.color_id — for the rebuild's new database. The four doors' grading above still holds as UX design; once that schema ships, _identity(title, surface) becomes a foreign-key lookup for all four doors, typed included, since COLOR_PLACEMENTS.surface_id is nullable by design.Grounded in: IKEA's Rooms, where the picture of the room is the subject of the screen rather than an ornament at the top of it, which is what lets frame 1 be two images and one sentence with no card around either; and Crate & Barrel's collection detail, where every material is both shown and named with its own identifier, which is the shape of the palette rows in frames 3 and 4: swatch, name, brand and code, and the surface it sits on.