git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit0a7cf42568afcf616fa082de734a2ecaf68f59f2
parent69d83c2fcf
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-28 14:31
refactor: retire the relief's flat geometry keys; no alias precedence is left

`relief.height` / `.profile` (the wall's) and `relief.edge_height` /
`.edge_profile` (the edge's) were aliases of the `wall` / `edge` node
spellings since this morning's rename. An alias that keeps working is a
second spelling forever, so the loader no longer reads them: their
flatten arms go, and a config still carrying one is reported by path
with the other retired surface keys. That was the last pair
`prefer_relief_spellings` had to decide between, so the pass goes too —
every legacy spelling of the relief is retired, a file is what it says,
and a comment where the pass stood says why it existed. cce-relief still
reads a flat key once as a seed, and its Save writes the node spelling
and removes the flat one, as before.

The precedence test folds into the flatten test: every retired spelling
reaches NO registry key, and a file carrying both spellings of a key is
what its current one says with nothing in between.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 CLAUDE.md             |  27 +++++++-----
 src/bin/cce-relief.rs |   5 ++-
 src/color.rs          |  31 ++++++++-----
 src/layout.rs         | 120 +++++++++++++++-----------------------------------
 4 files changed, 74 insertions(+), 109 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 83e0934..980026b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -567,10 +567,8 @@ Spellings that are NOT current, and what the loader does with each:
 | Spelling | Status |
 |---|---|
 | `window_manager.bevel_depth` / `.bevel_width` | compat aliases of `relief.light` / `.width` — the compositor's old block, a different family |
-| `relief.height` / `.profile`, `relief.edge_height` / `.edge_profile` | aliases of `wall.*` / `edge.*`; the node wins when both are present |
-| `window_manager.bevel_width` | alias of `relief.width` |
 | `param.color` (+ top-level `plate_opacity`) | alias of `plate.pane.color`, multiplied by the opacity line |
-| `plate.blur` / `.radius` / `.backdrop_compression` / `.refraction`, `frost.backdrop_compression`, `plate.bevel_width`, `relief.depth`, a material's `finish depth=` | RETIRED: reported by path (`color::retired_surface_keys`), not read; cce-relief seeds from `depth` once and writes `light` |
+| `plate.blur` / `.radius` / `.backdrop_compression` / `.refraction`, `frost.backdrop_compression`, `plate.bevel_width`, `relief.depth`, a material's `finish depth=`, `relief.height` / `.profile` / `.edge_height` / `.edge_profile` | RETIRED: reported by path (`color::retired_surface_keys`), not read; cce-relief seeds from each once and its Save writes the current spelling and removes the old |
 | `relief.wall.knobs` / `edge.knobs`, `profile_knobs` / `edge_knobs` | not style: cce-relief's own state (`~/.config/cce/cce-relief/state.kdl`); read once as a seed, removed on its next Save |
 
 Where each rule is argued, by its lead-in: **Frost is one block** and
@@ -578,10 +576,12 @@ Where each rule is argued, by its lead-in: **Frost is one block** and
 editor's knobs are not a style key** and **There is one roll width** under
 "Units" (the geometry is unit-aware, which is why they sit there); the
 `menu` block under "The context menu draws in its own popup surface".
-`prefer_relief_spellings` in `layout.rs` is the one place the alias
-precedence is decided, `color::retired_surface_keys` the one place a retired
-key is named (`retired_surface_keys`), and cce-relief's Save is the migration for all of it: it
-writes the current spellings and removes every superseded one it finds.
+There is no alias precedence to decide any more — every legacy spelling of
+the relief is retired, so a file is what it says — and
+`color::retired_surface_keys` is the one place a retired key is named;
+cce-relief's Save is the migration for all of it: it seeds from a retired
+key once, writes the current spelling and removes every superseded one it
+finds.
 
 ## The standard app — root plate, rungs, and the spacing ladder
 
@@ -770,11 +770,14 @@ quadrant for an edge). The registry keys never moved — `bevel_height`
 downstream of the registry knows. Until 2026-09-28 the keys were flat on the
 node with the wall UNNAMED (`height`, `profile`) and the edge prefixed
 (`edge_height`, `edge_profile`), which read as one shape with an "edge"
-variant rather than two shapes; those spellings survive as aliases in
-`layout.rs`'s flatten table, and `prefer_relief_spellings` drops a flat one
-whenever its node spelling is present, so a file carrying both is decided by
-the node and not by line order (`the_current_spelling_of_a_relief_key_wins_over_the_legacy_one`).
-`cce-relief`'s Save writes the node spellings and REMOVES the flat ones
+variant rather than two shapes; those spellings were aliases for the rest
+of that day and are RETIRED — not read, reported by path with the other
+retired surface keys, reaching no registry key (the flatten test asserts
+it). While they were aliases a `prefer_relief_spellings` pass dropped a flat
+one whenever its node spelling was present, because two spellings of one
+registry key were otherwise decided by line order; with nothing left to
+prefer, the pass is gone. `cce-relief` seeds from a flat key once, and its
+Save writes the node spellings and REMOVES the flat ones
 (`config::remove_config_value`), so a file migrates the first time it is
 saved; `wall` and `edge` are `PROP_NODES` members so their keys land as
 properties.
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 043c4b5..3976f54 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -1417,8 +1417,9 @@ impl Application for BevelPopup {
         // A key of one of the relief's two SHAPES: `relief { wall k=… }` first,
         // then the flat legacy spelling a file saved before 2026-09-28 still
         // carries (`height` / `profile` / `profile_knobs` for the wall,
-        // `edge_*` for the edge) — the same precedence the registry load
-        // applies through `layout::prefer_relief_spellings`.
+        // `edge_*` for the edge). The loader does not read those any more
+        // (retired, reported); this is the one-time seed, and Save writes
+        // the node spelling and removes the flat one.
         let rel_shape = |node: &str, k: &str, legacy: &str| {
             let r = target_relief.as_ref()?;
             r.get(node).and_then(|n| n.get(k)).or_else(|| r.get(legacy)).cloned()
diff --git a/src/color.rs b/src/color.rs
index 43cf279..3fe9f8d 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -683,7 +683,7 @@ fn parse_and_set_colors(content: &str) {
     let retired = retired_surface_keys(&val);
     if !retired.is_empty() {
         log::warn!(
-            "retired style.surface keys in config: {} — the frost is `plate {{ frost radius= compression= refraction= }}` (a material's `frost` child spells `compression`), every roll's width is `relief width=`, and the light strength is `relief light=` (a material's `finish light=`)",
+            "retired style.surface keys in config: {} — the frost is `plate {{ frost radius= compression= refraction= }}` (a material's `frost` child spells `compression`), every roll's width is `relief width=`, the light strength is `relief light=` (a material's `finish light=`), and the relief's geometry is `relief {{ wall height= profile= ; edge height= profile= }}`",
             retired.join(", ")
         );
     }
@@ -774,9 +774,11 @@ fn parse_and_set_colors(content: &str) {
 /// material's), whose one name is `compression`; `plate.bevel_width`, the
 /// pane roll's former width of its own (every roll is `relief.width`); and
 /// `relief.depth` with `depth` inside any material's `finish` child, the
-/// light strength's former name (`light`, since it is not a length). Each
-/// was an alias for part of 2026-09-28 and is not read now. Empty for a
-/// clean config.
+/// light strength's former name (`light`, since it is not a length); and
+/// the relief's flat geometry keys `height` / `profile` (the wall's, now
+/// `wall.height` / `wall.profile`) and `edge_height` / `edge_profile` (the
+/// edge's, now `edge.height` / `edge.profile`). Each was an alias for part
+/// of 2026-09-28 and is not read now. Empty for a clean config.
 pub fn retired_surface_keys(val: &serde_json::Value) -> Vec<String> {
     let mut found = Vec::new();
     for k in ["blur", "radius", "backdrop_compression", "refraction", "bevel_width"] {
@@ -787,8 +789,10 @@ pub fn retired_surface_keys(val: &serde_json::Value) -> Vec<String> {
     if val.pointer("/style/surface/plate/frost/backdrop_compression").is_some() {
         found.push("style.surface.plate.frost.backdrop_compression".to_string());
     }
-    if val.pointer("/style/surface/relief/depth").is_some() {
-        found.push("style.surface.relief.depth".to_string());
+    for k in ["depth", "height", "profile", "edge_height", "edge_profile"] {
+        if val.pointer(&format!("/style/surface/relief/{k}")).is_some() {
+            found.push(format!("style.surface.relief.{k}"));
+        }
     }
     if let Some(mats) = val.pointer("/style/surface/material").and_then(|v| v.as_object()) {
         for (name, node) in mats {
@@ -2245,21 +2249,22 @@ mod tests {
     }
 
     /// The four flat frost keys, the `backdrop_compression` spelling inside
-    /// a `frost` child, `plate.bevel_width` and `depth` (on the relief, or
-    /// in a material's `finish`) are reported by path; the block itself and
-    /// a material's `compression` / `light` are not.
+    /// a `frost` child, `plate.bevel_width`, `depth` (on the relief, or in a
+    /// material's `finish`) and the relief's flat geometry keys are
+    /// reported by path; the blocks themselves and a material's
+    /// `compression` / `light` are not.
     #[test]
     fn retired_surface_keys_are_named_by_path_and_the_block_is_not() {
         let clean: serde_json::Value = serde_json::json!({ "style": { "surface": {
             "plate": { "frost": { "radius": 5.5, "compression": 0.0, "refraction": 0.0 }, "color": "#6c6c7bf2" },
-            "relief": { "light": 0.15 },
+            "relief": { "light": 0.15, "wall": { "height": 1.0, "profile": "a" }, "edge": { "height": 2.0, "profile": "b" } },
             "material": { "glass": { "frost": { "compression": 0.6 }, "finish": { "light": 0.2 } } }
         } } });
         assert!(retired_surface_keys(&clean).is_empty());
         let old: serde_json::Value = serde_json::json!({ "style": { "surface": {
             "plate": { "blur": true, "radius": 1.5, "backdrop_compression": 0.85, "refraction": 0.3,
                        "bevel_width": 12.0, "frost": { "backdrop_compression": 0.2 } },
-            "relief": { "depth": 0.15 },
+            "relief": { "depth": 0.15, "height": 1.0, "profile": "a", "edge_height": 2.0, "edge_profile": "b" },
             "material": { "glass": { "frost": { "backdrop_compression": 0.6 }, "finish": { "depth": 0.2 } } }
         } } });
         assert_eq!(retired_surface_keys(&old), vec![
@@ -2270,6 +2275,10 @@ mod tests {
             "style.surface.plate.bevel_width",
             "style.surface.plate.frost.backdrop_compression",
             "style.surface.relief.depth",
+            "style.surface.relief.height",
+            "style.surface.relief.profile",
+            "style.surface.relief.edge_height",
+            "style.surface.relief.edge_profile",
             "style.surface.material.glass.frost.backdrop_compression",
             "style.surface.material.glass.finish.depth",
         ]);
diff --git a/src/layout.rs b/src/layout.rs
index 59ef75b..8d8a807 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -259,25 +259,21 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 // the carve's drop, the roll's rise; unset = follow the
                 // width) and `profile` (the ramp spec of its curve, written
                 // by cce-relief, installed by reload_config). The registry
-                // keys keep their old names. The flat spellings on the
-                // second line of each pair are the pre-rename aliases
-                // (`height` / `profile` were the wall's, `edge_*` the
-                // edge's); `prefer_relief_spellings` drops a flat one
-                // whenever its node spelling is present, so a config
-                // carrying both is decided by the node, not by line order.
-                // cce-relief's slider knobs are NOT a style key any more
+                // keys keep their old names. The pre-rename flat spellings
+                // (`height` / `profile` were the wall's, `edge_height` /
+                // `edge_profile` the edge's) were aliases for the rest of
+                // that day and are RETIRED: not read, reported by path
+                // (`color::retired_surface_keys`), removed by cce-relief's
+                // Save, which also reads them once as a seed. cce-relief's
+                // slider knobs are NOT a style key any more
                 // (`relief.wall.knobs` / `edge.knobs`, before that
                 // `profile_knobs` / `edge_knobs`): editor state, kept in
                 // that app's own state.kdl, and read off a config only by
                 // the editor itself, as a one-time seed.
-                "style.surface.relief.wall.height"
-                | "style.surface.relief.height" => "bevel_height",
-                "style.surface.relief.edge.height"
-                | "style.surface.relief.edge_height" => "roll_height",
-                "style.surface.relief.wall.profile"
-                | "style.surface.relief.profile" => "bevel_profile_spec",
-                "style.surface.relief.edge.profile"
-                | "style.surface.relief.edge_profile" => "roll_profile_spec",
+                "style.surface.relief.wall.height" => "bevel_height",
+                "style.surface.relief.edge.height" => "roll_height",
+                "style.surface.relief.wall.profile" => "bevel_profile_spec",
+                "style.surface.relief.edge.profile" => "roll_profile_spec",
                 "style.container.section.depth" => "section_depth",
                 "style.surface.param.backdrop_compression" => "param_compression",
                 "style.surface.param.label_layout" => "param_label_layout",
@@ -398,38 +394,19 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
     }
 }
 
-/// The relief's current spellings win over its legacy ones: the `wall` /
-/// `edge` node spellings over the flat geometry keys. Each pair flattens to
-/// ONE registry key (`relief.wall.height` and `relief.height` are both
-/// `bevel_height`), and `reload_config` loads the flat lines in the order
-/// the JSON hands them out, so a config carrying both spellings — a file
-/// cce-relief has saved once under the new names while an older line
-/// survives, or a per-app override written in the other spelling — would
-/// otherwise be decided by which line came first. Run on the parsed config
-/// before it is flattened. (`depth` versus `light` was a pair here for the
-/// rest of 2026-09-28; `depth` is retired now and never flattens at all.)
-pub(crate) fn prefer_relief_spellings(val: &mut serde_json::Value) {
-    const NODE_PAIRS: &[(&str, &str, &str)] = &[
-        ("height", "wall", "height"),
-        ("profile", "wall", "profile"),
-        ("edge_height", "edge", "height"),
-        ("edge_profile", "edge", "profile"),
-    ];
-    let Some(relief) = val.pointer_mut("/style/surface/relief").and_then(|v| v.as_object_mut()) else {
-        return;
-    };
-    for (flat, node, key) in NODE_PAIRS {
-        if relief.get(*node).and_then(|n| n.get(*key)).is_some() {
-            relief.remove(*flat);
-        }
-    }
-}
+// There is no alias-precedence pass any more. For the rest of 2026-09-28
+// `prefer_relief_spellings` ran here, dropping a relief key's legacy
+// spelling whenever its current one was present (`height` under
+// `wall.height`, `depth` under `light`), because two spellings of one
+// registry key would otherwise be decided by the order the JSON handed
+// the lines out. Every legacy spelling of the relief is retired now — not
+// read at all, reported by `color::retired_surface_keys` — so there is
+// nothing left to prefer, and a config is what it says.
 
 fn read_config() -> Option<String> {
     let path = crate::config::get_config_path();
     if let Ok(content) = std::fs::read_to_string(&path) {
-        let mut val = crate::config::parse_kdl_to_json(&content);
-        prefer_relief_spellings(&mut val);
+        let val = crate::config::parse_kdl_to_json(&content);
         let mut flat_props = String::new();
         flatten_json_to_flat_props(&val, "", &mut flat_props);
         return Some(flat_props);
@@ -440,8 +417,7 @@ fn read_config() -> Option<String> {
 pub fn read_config_value(target_key: &str) -> Option<String> {
     let path = crate::config::get_config_path();
     if let Ok(content) = std::fs::read_to_string(&path) {
-        let mut val = crate::config::parse_kdl_to_json(&content);
-        prefer_relief_spellings(&mut val);
+        let val = crate::config::parse_kdl_to_json(&content);
         let mut flat_props = String::new();
         flatten_json_to_flat_props(&val, "", &mut flat_props);
         for line in flat_props.lines() {
@@ -6985,60 +6961,36 @@ mod tests {
         assert_eq!(value("roll_height").as_deref(), Some("4"), "{flat}");
         assert_eq!(value("bevel_profile_spec").as_deref(), Some("smooth;0:0,1:1"), "{flat}");
         assert_eq!(value("roll_profile_spec").as_deref(), Some("smooth;0:0,1:0.9"), "{flat}");
-        // And the flat legacy spellings still land on the same keys.
+        // The retired spellings — the flat geometry keys, `depth`, and the
+        // knob keys that are editor state — reach NO registry key: a config
+        // that says only these draws the defaults, and the load-time report
+        // (`color::retired_surface_keys`) is what says why.
         let old: serde_json::Value = serde_json::json!({
             "style": { "surface": { "relief": {
+                "depth": 0.3,
                 "height": 2.0, "edge_height": 3.0, "profile": "a", "edge_profile": "b",
                 "wall": { "knobs": "1,1,1" }, "edge_knobs": "2,2,2"
             } } }
         });
         let mut flat = String::new();
         flatten_json_to_flat_props(&old, "", &mut flat);
-        for k in ["bevel_height", "roll_height", "bevel_profile_spec", "roll_profile_spec"] {
-            assert!(flat.lines().any(|l| l.starts_with(&format!("{k} = "))), "{k} missing: {flat}");
+        for k in ["bevel_depth", "bevel_height", "roll_height", "bevel_profile_spec", "roll_profile_spec", "profile_knobs"] {
+            assert!(!flat.lines().any(|l| l.starts_with(&format!("{k} = "))), "{k} landed from a retired spelling: {flat}");
         }
-        // The knobs are editor state, not style: no registry key of their own.
-        assert!(!flat.contains("profile_knobs"), "{flat}");
-    }
-
-    /// A config carrying both spellings of one relief key is decided by the
-    /// current spelling — the node one for the geometry — whichever line
-    /// the file wrote first: the flat pass loads in JSON order, and
-    /// `prefer_relief_spellings` is what makes the order irrelevant. A key
-    /// with no current spelling beside it is left alone. The retired
-    /// `depth` is not a pair any more: it never reaches a registry key.
-    #[test]
-    fn the_current_spelling_of_a_relief_key_wins_over_the_legacy_one() {
-        let retired: serde_json::Value = serde_json::json!({
-            "style": { "surface": { "relief": { "depth": 0.3 } } }
-        });
-        let mut flat = String::new();
-        flatten_json_to_flat_props(&retired, "", &mut flat);
-        assert!(!flat.contains("bevel_depth"), "a retired `depth` flattens to no registry key: {flat}");
-        let mut val: serde_json::Value = serde_json::json!({
+        // And a file carrying BOTH spellings is what its current one says,
+        // with no precedence pass in between — the retired one is not read.
+        let both: serde_json::Value = serde_json::json!({
             "style": { "surface": { "relief": {
-                "light": 0.15,
+                "light": 0.15, "depth": 0.9,
                 "height": 2.0, "wall": { "height": 5.0 },
-                "edge_profile": "old", "edge": { "profile": "new" },
-                "profile": "flat-only"
+                "edge_profile": "old", "edge": { "profile": "new" }
             } } }
         });
-        prefer_relief_spellings(&mut val);
-        let relief = val.pointer("/style/surface/relief").unwrap();
-        assert!(relief.get("height").is_none(), "the flat wall height yields: {relief}");
-        assert!(relief.get("edge_profile").is_none(), "the flat edge profile yields: {relief}");
-        assert_eq!(relief.pointer("/wall/height").and_then(|v| v.as_f64()), Some(5.0));
-        assert_eq!(relief.pointer("/edge/profile").and_then(|v| v.as_str()), Some("new"));
-        assert_eq!(relief.get("profile").and_then(|v| v.as_str()), Some("flat-only"), "no node spelling: kept");
-        assert_eq!(relief.get("light").and_then(|v| v.as_f64()), Some(0.15));
         let mut flat = String::new();
-        flatten_json_to_flat_props(&val, "", &mut flat);
+        flatten_json_to_flat_props(&both, "", &mut flat);
         assert_eq!(flat.lines().filter(|l| l.starts_with("bevel_height = ")).count(), 1, "{flat}");
-        assert!(flat.contains("bevel_height = 5"), "{flat}");
-        // A config with no relief block at all is untouched.
-        let mut none: serde_json::Value = serde_json::json!({ "style": {} });
-        prefer_relief_spellings(&mut none);
-        assert_eq!(none, serde_json::json!({ "style": {} }));
+        assert!(flat.contains("bevel_height = 5") && flat.contains("bevel_depth = 0.15"), "{flat}");
+        assert!(flat.contains("roll_profile_spec = \"new\""), "{flat}");
     }
 
     /// The control rung's key flattens to `control_corner_radius`, beside a