scroll # Color Chain — Rebirth (Day 382) Correctedmodel from design discussion, superseding the"collapse Color" and "Swatch→Swatch alias"detours explored earlier same day (see[[Palette-Swatch-Color-Chain.00382]]). This isthe model going forward — not yetimplemented in Portal. ## The model **Colorhas no invented name. Its identity is derivedfrom the value itself — and that value isOKLCH, not hex.** The original cut of thismodel used hex as the identity (`FF0000`instead of `sacred-red`/`blood-red`).Superseded by the gamut/accuracy discussionbelow: hex is sRGB at 8 bits/channel — anarrow gamut with visible quantization —while OKLCH is perceptually uniform andgamut-flexible. So the real identity is thenormalized OKLCH naming string(`nnn_nn-n_nnn-nnn_n`, spec below), and hexbecomes a **derived display value**, not thesource of truth. - A Color Thing's identity isits normalized OKLCH name (e.g.`062_50-0_130-210_0`), not an invented wordand not raw hex. - Color carries hex, RGB,HSB, ANSI — whatever's needed for directconsumption — as **derived fields computedfrom the OKLCH value**, not the other wayaround. - **Swatch is the only place ahuman-meaningful name exists** — `error`,`hot`, `ache.identity`, `sacred-accent`, etc.Swatch links to a Color by its OKLCH name.**Sharing is automatic, not curated.** TwoSwatches (`error`, `hot`) pointing at the sameColor (same normalized OKLCH name) are sharing*because they reference the same value*, fullstop — no judgment call about "is this Colorgeneric enough to justify a shared row," noinvented intermediate name to coordinate. Thiswas the actual flaw in the live data: Colorslike `ache-identity-orange` or `sacred-red`are invented names that happen to lookswatch-derived, because someone minted a namewhere the value itself should have been theidentifier. **Palette → Swatch → Color**,same shape as before, just with Color'sidentity fixed — and fixed twice over: firstby dropping invented names for the raw value,then by using the more accurate value (OKLCH)instead of hex as that identity. ## What'swrong in the current schema -`Portal/rust/src/things/theme.rs:18-22,37,61`and`Portal/rust/src/server/at_routes.rs:7121-7167`key the `colors` map by Color's `Gem::Name`field (an invented string), and`Swatch.linked_color` (in `Gem::Mark`) storesthat same invented string as the join key —not the hex. - This produced the false signalinvestigated earlier: ~19 of 25 Color Thingsare 1:1 with a single Swatch and exist onlybecause a name was invented for them; only 6(`sacred-red`, `moss-green`, `spring-green`,`gold`, `bone-white`, `portal-cyan`) areactually referenced by more than one Swatchtoday. Under the hex-as-name model thisdistinction disappears — sharing is just "dotwo Swatches cite the same hex," not aproperty of how the Color happened to getnamed. - Separately (orthogonal bug, not partof this model fix): `GET /api/palette/:name`resolves Palette metadata correctly but theswatch query (`at_routes.rs:7145-7146`) has nopalette filter — it returns every`sapphire=swatch` Thing in the DB regardlessof `:name`. Confirmed via `curl/api/palette/timepiece-palette` returning theidentical swatch set as`/api/palette/hexcraft-dark`. Two PaletteThings exist (`hexcraft-dark`,`timepiece-palette`) but only one isfunctionally in effect. Not fixed here —flagged for whenever Palette membershipbecomes real. ## Every swatch, regrouped byvalue (the rebirth view) Old table([[Palette-Swatch-Color-Chain.00382]]) listedeach Swatch with whatever invented Color nameit happened to link to — that made sharinglook like a property of naming. Regrouped bythe underlying color value, sharing is justwhichever rows land in the same bucket.**Bold** groups are real multi-Swatch shares;the rest are singles that needed no Color-nameinvention at all. Grouped here by hex (thedata we actually have today) as a stand-in —the real post-migration grouping key is thenormalized OKLCH name (spec below), not hex.Converting all 35 of these hex values to theirOKLCH names is unfinished work, not done inthis pass; the grouping/sharing logic itselfis unaffected by which of the two values isused as the key, since OKLCH and hex are justtwo representations of the same color. | Hex(today's stand-in value) | Swatches | Today'sinvented Color name(s) | Note ||---|---|---|---| | **`FF0000`** | `error`,`sacred-accent`, `terminal-red` | `blood-red`**and** `sacred-red` (two different names!) |**Duplicate Color rows for one hex** —`error`→`blood-red`, the othertwo→`sacred-red`. Proves the invented-namelayer actively fragments what should be onerow. | | **`FFD700`** | `grimoire.right-hand`,`subu.today` | `gold` (consistent) | Realshare, correctly named once — stillredundant once hex is the key. | |**`FFFFFF`** | `terminal-cursor`,`terminal-foreground` | `bone-white`(consistent) | Real share. | | **`00FF00`** |`success`, `terminal-green` | `moss-green`(consistent) | Real share. | | **`00FFFF`** |`command`, `terminal-cyan` | `portal-cyan`(consistent) | Real share. | | **`FF00FF`** |`sacred`, `terminal-magenta` |`sacred-magenta` (consistent) | Real share. || **`33CC33`** | `ache.chip`, `subu.minor` |`spring-green` (consistent) | Real share. | |`8E44AD` | `ache.arcana` |`ache-arcana-purple` | Single — inventedname added nothing. | | `F39C12` |`ache.chronicle` | `ache-chronicle-amber` |Single. | | `D4A017` | `ache.economy` |`ache-economy-gold` | Single. | | `50C878` |`ache.fid` | `ache-fid-green` | Single. | |`4269E1` | `ache.gap` | `ache-gap-blue` |Single — note: visually close to `4169E1`below but a distinct hex, correctly notmerged. | | `E67E22` | `ache.identity` |`ache-identity-orange` | Single. | | `27AE60`| `ache.infrastructure` | `ache-infra-green` |Single. | | `9B59B6` | `ache.protocol` |`ache-protocol-purple` | Single. | | `4A90D9`| `ache.scroll` | `ache-scroll-blue` | Single.| | `2ECC71` | `ache.system` |`ache-system-green` | Single. | | `556677` |`ache.unknown` | `ache-unknown-gray` | Single.| | `4169E1` | `grimoire.left-hand` |`royal-blue` | Single — distinct from`ache.gap`'s `4269E1` above. | | `9370DB` |`grimoire.membrane` | `orchid-purple` |Single. | | `88AAFF` | `hexcube.air` |`air-blue` | Single. | | `44AA44` |`hexcube.earth` | `earth-green` | Single. | |`FF6644` | `hexcube.fire` | `fire-orange` |Single. | | `A9A9A9` | `info` | `slate-gray` |Single. | | `ADD8E6` | `muse-response` |`sky-blue` | Single. | | `FFFF00` |`muse-user` | `gold-yellow` | Single. | |`9933CC` | `subu.future-major` |`medium-purple` | Single. | | `660066` |`subu.future-mega` | `deep-purple` | Single. || `CC66FF` | `subu.future-minor` |`light-purple` | Single. | | `8899FF` |`subu.glass` | `glass-blue` | Single. | |`006600` | `subu.major` | `emerald-green` |Single. | | `004400` | `subu.mega` |`forest-green` | Single. | | `000000` |`terminal-background` | `void-black` | Single.| | `4444FF` | `terminal-blue` |`terminal-blue` | Single — name happens toequal Swatch name, coincidence not reuse. | |`FFFF44` | `terminal-yellow` |`terminal-yellow` | Single — samecoincidence. | **Count:** 43 Swatches, 35distinct hex values today (would be 35 Colorrows post-migration, down from 25invented-name rows — because the `FF0000`duplicate splits back into the one true bucketit always should have been, and every "single"row drops its invented name in favor of itsown hex). ## Color identity naming spec —OKLCH (the actual identity) Color's identityis OKLCH, not raw hex — see "The model"above. Perceptually-accurate math (blending,tints, contrast checks) is the reason; thename format is: ``` nnn_nn-n_nnn-nnn_n L C H``` - **L** — `nnn_nn`, three digits beforethe decimal, two after. Covers the full`000_00`–`100_00` range losslessly (twodigits alone tops out at `99_99`, short ofpure white). - **C** — `n_nnn`, one digitbefore the decimal, three after. Covers`0_000`–`9_999`; real OKLCH chroma neverexceeds ~`0_500`, so this has deliberateheadroom for any wide-gamut/out-of-gamutvalue. - **H** — `nnn_n`, three digitsbefore the decimal, one after (tenth-degreeprecision). Range `000_0`–`359_9` — hue isreduced mod 360 before naming so `360_0` neverappears (it's identical to `000_0`). Example:`oklch(62.5% 0.130 210)` →`062_50-0_130-210_0`. Normalization rulesapplied before naming (so the same coloralways produces exactly one name): 1. Round Lto 2-decimal precision (no clamp needed —`nnn_nn` already covers the full 0–100 rangeexactly; clamping was only a workaround forthe earlier, narrower `nn_nn` draft, nowobsolete). 2. Round C to 3-decimal precision.3. Reduce H mod 360, then round to 1-decimalprecision. **Rounding here is quantization,not a digit-count bug.** L, C, and H arecontinuous real numbers; any fixed-digit nameformat has to truncate somewhere, no matterhow many `n`s you add — more digits shrinkthe rounding error, they never eliminate it,the same tradeoff hex's 8-bit-per-channelencoding already makes against continuouslight. "Lossless" in this spec means**idempotent**: the same color alwaysnormalizes to the same name. It does not meanzero error versus the infinite-precisionsource value — that guarantee doesn't existfor any finite encoding, hex included. This isthe canonical identity — hex is a deriveddisplay value computed from it, not a fallbackor an alternate identity sitting "alongside"it. ## Locked Color Thing schema Gemplacement, settled: - **onyx** (`Gem::Name`)— the normalized OKLCH name itself.Identity. - **sapphire** (`Gem::Kind`) —`"color"`. - **pearl** (`Gem::Body`) —free-text description, optional. - **ruby**(`Gem::Mark`) — classification, notmeasurement: which gamut it fits(`in_srgb_gamut`), which space it's defined in(`space`), and `ansi` (a categoricalterminal-color label, not a numericmeasurement — moved here from topaz). -**topaz** (`Gem::Data`) — all numericmeasurements, **as triplets matching the shapeRGB/HSB already use** rather than splittingL/C/H into separate scalar fields. Accessing`topaz.l` vs `topaz.lch[0]`/`topaz.lch.0` islogistically identical for querying orindexing — bundling costs nothing andremoves the asymmetry of treating LCHdifferently from RGB/HSB. - **citrine**(`Gem::Loc`) — locality, e.g. whichring/palette grouping it sits in. -**emerald** (`Gem::Time`) — created/sealedtimestamps, standard. - **amethyst**(`Gem::Access`) — visibility, standard. -**diamond** (`Gem::Link`) — reverse edge:which Swatches currently link to this Color(not in the live schema today — the onlypointer that exists is the forward`Swatch.linked_color`; this would be new).```json { "onyx": "062_50-0_130-210_0","sapphire": "color", "pearl": "mutedteal-blue, used for ache/info-class swatches","ruby": { "in_srgb_gamut": true, "space":"oklch", "ansi": "blue" }, "topaz": { "lch":[62.5, 0.130, 210.0], "hex": "#5FA8C9", "rgb":[95, 168, 201], "hsb": [197, 53, 79] },"citrine": { "ring":"ac1d91a9-ba40-491a-84e1-ce968752c861" },"emerald": { "created":"2026-06-25T00:00:00Z", "sealed":"2026-06-25T00:00:00Z" }, "amethyst": {"visibility": "public" }, "diamond": {"swatches": ["info", "ache.unknown"] } } ```## Migration shape (not yet done) 1. Convertevery Color Thing's current hex to OKLCH,normalize per the spec above, and rename theColor Thing's identity to that name (drop theinvented word). 2. Repoint every Swatch's`linked_color` to the normalized OKLCH nameinstead of the invented name. 3. Rebuild everyColor Thing's gem_values to the locked schemaabove:`topaz.lch`/`topaz.hex`/`topaz.rgb`/`topaz.hsb`as derived measurement triplets,`ruby.ansi`/`ruby.space`/`ruby.in_srgb_gamut`as classification — not the old flat`ruby.{hex,rgb,ansi}` shape`theme.rs`/`at_routes.rs` read today. 4. Add`diamond.swatches` as the reverse edge fromColor back to its linking Swatches. 5. LeavePalette as-is for now — its membership bugis a separate, later fix. ## Bridge status(unaffected by this) The CSS bridge builtearlier today (`being-themes.js``hexPalette.applyFromPortal()` →`--swatch-<name>` vars, 5 `--sacred-*` tokensrewired in `styles.css`) reads Swatch name +resolved hex from `/api/palette`, not Color'sinternal name — so this Color-identity fixis invisible to the CSS layer. No changesneeded there when this migrates. ## Sourcepointers -`Portal/rust/src/things/theme.rs:18-22`(`ColorDef`), `:37` (`query_sapphire(pool,"color")`), `:61` (`query_sapphire(pool,"swatch")`) -`Portal/rust/src/server/at_routes.rs:7121-7167`(color/swatch resolution), `:7174-7193`(palette metadata lookup — the only place`:name` actually matters today)