facett-core 0.1.15

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! **The APP SHAPE** — `App` / `Interface` / `Infrastructure`, the one runtime shape every
//! workspace_nordisk suite app wears.
//!
//! Designed 2026-08-02 with Rickard; the full argument is in
//! `workspace_nordisk/.nornir/app-shape-design-2026-08-02.md`, and its build-time sibling
//! (four atoms per suite member) is `.nornir/SUITE-MODEL.md`.
//!
//! # The bug this exists to make impossible
//!
//! An app stores a CHOICE and then never lets that choice decide what is drawn under it.
//! The selection is written to a field and read back only where it is *displayed* —
//! nothing downstream comes FROM it. So a pane offers controls that do not apply to what
//! is selected, and hides ones that do.
//!
//! Rickard, having hit it in two unrelated panes of one binary on one day: *"the korp map
//! 2d/3d choice should change settings below … do we see a pattern? infrastructure should
//! show correct choices/ui"*. Both instances were real, and the second was measured: korp's
//! `chosen_infra` had exactly ONE behavioural use in the entire UI — a filter hiding two
//! tabs — while **37 of 56 production `draw_*` fns named a backend directly**. Choosing an
//! embedded backend hid two tabs and changed nothing else.
//!
//! **A control that is offered is a promise that it applies.** 3-D lighting settings under
//! a 2-D map is not a missing feature; it is the UI asserting something false about its own
//! state. Same failure as two `enabled` flags that disagreed in one Settings pane, and as a
//! state file reporting a selected region while the select box rendered empty: **two pieces
//! of code work out the same answer separately, somebody has to remember to change both,
//! and eventually nobody does.** The cure is not a philosophy — delete the second copy.
//!
//! # The shape
//!
//! ```ignore
//! trait App { fn infrastructure(&self) -> &dyn Infrastructure; fn interface(&mut self) -> &mut dyn Interface; }
//! ```
//!
//! Two halves, and that is the whole top level. [`Interface`] is **recursive**: a tab and a view
//! mode are the same kind of node at different depths, so the controls under a choice ARE
//! [`Interface::selected`] rather than being looked up. [`Infra`] hands out faces; a face
//! returning `None` is *why* a tab does not exist, which is what replaces a
//! `is_spark_backend()`-style predicate with a capability answering for itself.
//!
//! # Why `Interface` is a supertrait of [`Facet`](crate::Facet), not a new pane contract
//!
//! `Facet` already carries `title` / `ui` / `state_json` / `update_json` / `severity` /
//! `component`. Redeclaring a `render` here would be a second way to draw a pane — the
//! twin this whole module exists to prevent (LAW 5). So `Interface: Facet` adds exactly three
//! methods and inherits the rest.
//!
//! All three are **defaulted**, so `impl Interface for MyPane {}` is a legal one-liner meaning
//! "a leaf: I have no sub-choices". Every addition to `Facet` this year has been defaulted
//! for the same reason and that precedent holds here.
//!
//! There is deliberately **no blanket `impl<T: Facet> Interface for T`**: it would make every
//! facet a leaf *and forbid any pane from overriding*, which is precisely the interesting
//! case. Opting in is one line and it is explicit.

use serde::{Deserialize, Serialize};

/// A stable machine key for one option — a tab id, a view mode, a layer. Not a label:
/// this is what `select` takes, what `state_json` reports, and what a robot driver
/// addresses, so it must survive a rename of the human-facing title.
pub type Key = String;

/// **What kind of thing a [`Control`] is** — enough for any renderer to draw it, and
/// nothing about how.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ControlKind {
    /// A free-text field (a URL, a path, a connection string).
    Text {
        /// Placeholder / help text shown when empty.
        hint: String,
    },
    /// An on/off switch.
    Toggle,
    /// One-of-many, from a fixed list of [`Key`]s.
    Choice {
        /// The selectable options, in display order.
        options: Vec<Key>,
    },
    /// A number with an inclusive range.
    Number {
        /// Smallest accepted value.
        min: f64,
        /// Largest accepted value.
        max: f64,
    },
    /// A read-only fact — a measured count, a status line. Never editable.
    Readout,
}

/// **One control, as DATA** — never a widget.
///
/// This is what lets an egui pane, a TUI, a robot driver and a web client render the same
/// set from one description. It is also what makes the guard possible: two choices can be
/// compared by their control ids, which is impossible if `controls()` were a pile of
/// immediate-mode calls.
///
/// The `id` is the stable address (see [`Key`]); `label` is human-facing and may change
/// freely.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Control {
    /// Stable machine id — what a driver addresses and what a guard compares.
    pub id: Key,
    /// Human-facing text. Free to change without breaking anything.
    pub label: String,
    /// What kind of control this is.
    pub kind: ControlKind,
    /// The current value, rendered as a string (`"true"`, `"12"`, a URL). `None` when the
    /// control has no value yet — which is DIFFERENT from an empty string, and must render
    /// differently, for the same reason empty-vs-unreadable must.
    pub value: Option<String>,
}

impl Control {
    /// A read-only fact.
    pub fn readout(id: impl Into<Key>, label: impl Into<String>, value: impl Into<String>) -> Control {
        Control {
            id: id.into(),
            label: label.into(),
            kind: ControlKind::Readout,
            value: Some(value.into()),
        }
    }

    /// An on/off switch.
    pub fn toggle(id: impl Into<Key>, label: impl Into<String>, on: bool) -> Control {
        Control {
            id: id.into(),
            label: label.into(),
            kind: ControlKind::Toggle,
            value: Some(on.to_string()),
        }
    }

    /// A free-text field.
    pub fn text(id: impl Into<Key>, label: impl Into<String>, hint: impl Into<String>, value: &str) -> Control {
        Control {
            id: id.into(),
            label: label.into(),
            kind: ControlKind::Text { hint: hint.into() },
            value: (!value.is_empty()).then(|| value.to_string()),
        }
    }
}

/// **A node in the app's ui tree** — recursive, so a tab and a view mode are the same kind
/// of thing at different depths.
///
/// ```ignore
/// app.ui().select("map");
/// app.ui().selected()?.select("3d");     // the SAME call, one level down
/// ```
///
/// The controls under a choice are not looked up: they **are** [`selected`](Interface::selected).
/// Pick `3d` and you get the 3d node; its `Facet::ui` draws 3d's controls, and nothing else
/// can appear because nothing else was returned.
///
/// A leaf overrides nothing — `impl Interface for MyPane {}` is complete and means "no
/// sub-choices". See the module docs for why there is no blanket impl.
pub trait Interface: crate::Facet {
    /// The choices available AT THIS LEVEL, in display order — tabs at the top, view modes
    /// one level down.
    ///
    /// This must be the **only** list. korp's tab strip, its state oracle and its own test
    /// each carried a separate copy — 17, 16 and 9 entries — and the test compared its copy
    /// to itself, so it was green forever while the first tab was unreachable. One list
    /// cannot disagree with itself.
    fn options(&self) -> Vec<Key> {
        Vec::new()
    }

    /// Choose one of [`options`](Interface::options). Returns `false` for a key that is not on
    /// offer — a caller that ignores the result is asking for the same silent-no-op class
    /// this module exists to kill.
    fn select(&mut self, _key: &str) -> bool {
        false
    }

    /// The currently-selected child, or `None` for a leaf.
    ///
    /// This is the load-bearing method. Rendering the selected child is what makes the
    /// dependent ui change when the choice changes, with nothing to keep in sync.
    fn selected(&mut self) -> Option<&mut dyn Interface> {
        None
    }

    /// The controls this node offers — as DATA, so every renderer draws the same set and a
    /// guard can compare two choices.
    fn controls(&self) -> Vec<Control> {
        Vec::new()
    }
}

/// **One backend capability.** A face exists only if swapping [`Infra`] swaps the
/// implementation.
///
/// That rule caught two near-misses on the day it was written, both by measuring rather
/// than by taste: FalkorDB is a URL passed around under EVERY infra, and object storage is
/// the same embedded store under every infra. Neither is a choice, so neither is a face.
pub trait Face {
    /// A short human name for the implementation actually in use (`"Iceberg"`,
    /// `"Postgres"`) — for an honest status line, never for a branch.
    fn label(&self) -> &str;

    /// The controls this face offers, as DATA. Postgres vends a connection string;
    /// Iceberg vends a warehouse path. This is why the settings BELOW an infra choice
    /// change when the choice does.
    fn controls(&self) -> Vec<Control> {
        Vec::new()
    }
}

/// **The swappable half of an app** — hands out [`Face`]s, and is the only thing that
/// knows which backend answered.
///
/// A `None` face is not a degraded state to apologise for: it is the REASON a surface does
/// not exist. That is what replaces a `is_spark_backend()`-shaped predicate scattered
/// across panes — the capability answers for itself, and there is nothing to keep in sync.
///
/// The concrete face traits (a relational/catalog face, a graph face) live with the app
/// that defines the operations, because `search` and `cypher` are domain calls, not
/// framework ones. What is shared is the SHAPE: ask, get `Option<&dyn Face>`, and never
/// name the implementation.
pub trait Infrastructure {
    /// Which infra this is, for an honest status line (`"skade (embedded)"`).
    fn label(&self) -> &str;

    /// Every face this infra offers, by name — the generic surface a settings pane, a
    /// robot driver or a status line can enumerate without knowing the app's domain.
    ///
    /// Apps add typed accessors (`fn relation(&self) -> Option<&dyn RelationFace>`) on
    /// their own trait; this is the untyped roster, so a pane can render "what does this
    /// backend actually offer" without a match.
    fn faces(&self) -> Vec<(&'static str, &dyn Face)> {
        Vec::new()
    }
}

/// **An app**: a ui tree and a swappable infra. The whole top level.
///
/// Named per suite member in the app's own crate (`KorpApp`, `NornirApp`), because the
/// name is the app's; this is the shape they share. A test or a robot driver holds
/// `&mut dyn App` instead of a struct private to a `main.rs` — which is not theoretical:
/// korp's `struct Korp` being bin-private meant 29 integration test files each redeclared
/// their own copy and drove THAT, with one reference to the real shell in the whole
/// directory.
pub trait App {
    /// The swappable half.
    fn infrastructure(&self) -> &dyn Infrastructure;
    /// The ui tree's root.
    fn interface(&mut self) -> &mut dyn Interface;
}

/// **Walk to the deepest selected node** — the leaf the user is actually looking at.
///
/// The path a robot driver reports and a bug report should name: `map → 3d → buildings`.
pub fn selected_path(root: &mut dyn Interface) -> Vec<Key> {
    let mut path = Vec::new();
    let mut node: &mut dyn Interface = root;
    loop {
        // `options` is the level's own list; the SELECTED key is whatever the child is.
        // Ask the child for its title only after we know there is one.
        let has_child = node.selected().is_some();
        if !has_child {
            return path;
        }
        node = node.selected().expect("checked");
        path.push(crate::Facet::title(node).to_string());
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A two-level node: options `a`/`b`, each child a leaf with its OWN control.
    struct Node {
        title: String,
        chosen: usize,
        kids: Vec<Node>,
    }

    impl crate::Facet for Node {
        fn title(&self) -> &str {
            &self.title
        }
        fn ui(&mut self, _ui: &mut egui::Ui) {}
        fn state_json(&self) -> serde_json::Value {
            serde_json::json!({ "title": self.title })
        }
    }

    impl Interface for Node {
        fn options(&self) -> Vec<Key> {
            self.kids.iter().map(|k| k.title.clone()).collect()
        }
        fn select(&mut self, key: &str) -> bool {
            match self.kids.iter().position(|k| k.title == key) {
                Some(i) => {
                    self.chosen = i;
                    true
                }
                None => false,
            }
        }
        fn selected(&mut self) -> Option<&mut dyn Interface> {
            self.kids.get_mut(self.chosen).map(|k| k as &mut dyn Interface)
        }
        fn controls(&self) -> Vec<Control> {
            // Deliberately DIFFERENT per node, which is the property the guard checks.
            vec![Control::readout(format!("{}_readout", self.title), &self.title, &self.title)]
        }
    }

    fn tree() -> Node {
        Node {
            title: "root".into(),
            chosen: 0,
            kids: vec![
                Node { title: "2d".into(), chosen: 0, kids: Vec::new() },
                Node { title: "3d".into(), chosen: 0, kids: Vec::new() },
            ],
        }
    }

    /// **THE GUARD THIS MODULE EXISTS FOR: a choice must change the ui.**
    ///
    /// Snapshot the control ids for each option and require them to differ pairwise. This
    /// is the assertion no suite app had on 2026-08-02, and its absence is why korp could
    /// ship a 2D/3D switch that left the settings beneath it untouched, and an infra
    /// chooser that hid two tabs and changed nothing else.
    ///
    /// A `select` that silently does nothing, or a `selected` that returns the same node
    /// for every key, both fail here.
    #[test]
    fn every_choice_yields_a_different_ui() {
        let mut root = tree();
        let opts = root.options();
        assert!(opts.len() >= 2, "need two options to compare, got {opts:?}");

        let mut seen: Vec<(Key, Vec<Key>)> = Vec::new();
        for key in &opts {
            assert!(root.select(key), "select({key}) refused a key its own options() offered");
            let child = root.selected().expect("a selected option must yield a node");
            let ids: Vec<Key> = child.controls().into_iter().map(|c| c.id).collect();
            assert!(!ids.is_empty(), "option {key} yielded a node with NO controls");
            seen.push((key.clone(), ids));
        }

        for (i, (ka, a)) in seen.iter().enumerate() {
            for (kb, b) in seen.iter().skip(i + 1) {
                assert_ne!(
                    a, b,
                    "options {ka:?} and {kb:?} render the SAME controls — the choice \
                     changed nothing, which is the bug this guard exists to catch"
                );
            }
        }
    }

    /// `select` must REFUSE a key that is not on offer, rather than silently doing
    /// nothing — a caller cannot tell those apart, and a driver that ignores the result
    /// would report success for a click that never happened.
    #[test]
    fn selecting_an_unoffered_key_is_refused_not_ignored() {
        let mut root = tree();
        assert!(!root.select("no-such-mode"), "an unknown key must return false");
        // And the selection is unchanged — a refused select must not move anything.
        assert_eq!(crate::Facet::title(root.selected().expect("still selected")), "2d");
    }

    /// A leaf is a legal `Interface` with no overrides at all — the additive property that lets
    /// existing panes adopt this one line at a time.
    #[test]
    fn a_leaf_needs_no_overrides_and_reports_no_choices() {
        struct Leaf;
        impl crate::Facet for Leaf {
            fn title(&self) -> &str {
                "leaf"
            }
            fn ui(&mut self, _ui: &mut egui::Ui) {}
            fn state_json(&self) -> serde_json::Value {
                serde_json::Value::Null
            }
        }
        impl Interface for Leaf {}

        let mut leaf = Leaf;
        assert!(leaf.options().is_empty());
        assert!(leaf.selected().is_none());
        assert!(!leaf.select("anything"));
        assert!(leaf.controls().is_empty());
    }

    /// The path a bug report should name — `map → 3d`, not "somewhere in the map tab".
    #[test]
    fn the_selected_path_names_the_leaf_the_user_is_looking_at() {
        let mut root = tree();
        assert!(root.select("3d"));
        assert_eq!(selected_path(&mut root), vec!["3d".to_string()]);
        assert!(root.select("2d"));
        assert_eq!(selected_path(&mut root), vec!["2d".to_string()]);
    }

    /// A `Control` distinguishes "no value yet" from "empty value" — the same
    /// empty-vs-unreadable distinction that has cost this codebase repeatedly.
    #[test]
    fn a_control_with_no_value_is_not_a_control_with_an_empty_one() {
        let unset = Control::text("url", "URL", "https://…", "");
        let set = Control::text("url", "URL", "https://…", "http://localhost:9000");
        assert_eq!(unset.value, None, "an empty string means NO value, not a blank one");
        assert_eq!(set.value.as_deref(), Some("http://localhost:9000"));
    }
}