Skip to main content

facett_core/
devid.rs

1//! **The DEV-ID badge** — every pane says who it is, in non-release builds only.
2//!
3//! Rickard's ask, verbatim: *"show an uniqe ID on each pane/component in non release
4//! mode … make it easy to copy this one to claude and explain where i am"*.
5//!
6//! # Why this is the errcode scheme's other half
7//!
8//! [`crate::errcode`] already answers *"what went wrong"* with a stable
9//! `facet-<component>-<n>`, and its [`REGISTRY`](crate::errcode::REGISTRY) maps a
10//! component to the crate dir, the source file and a Codeberg URL. This module answers
11//! the sibling question — ***"where am I"*** — with the same vocabulary, so a badge
12//! pasted into a chat resolves to a crate and a file with no guessing on either side.
13//! It is not an error; a pane showing its dev-id is perfectly healthy.
14//!
15//! # The trap this is built to avoid
16//!
17//! `facett-core/tests/errcode_raise_sites.rs` found that `facet_err!` **fabricates** its
18//! code with `concat!` and never asks the registry whether it exists, so
19//! `facet_err!(map, 99, …)` compiles and yields an authoritative-looking `facet-map-99`
20//! that `lookup` cannot resolve. A dev-id badge has exactly that hole: an id that *looks*
21//! canonical but names nothing wastes the time of whoever pastes it AND whoever reads it.
22//!
23//! So the badge is honest about its own confidence, and the two cases are rendered so
24//! they cannot be mistaken for each other:
25//!
26//! * **registered** — `facet-geomap` in the registry's own colour. [`resolve`] hands back
27//!   the crate dir + source file, so the id is actionable on arrival.
28//! * **unregistered** — `⟨unregistered⟩ Some Title`, dimmed and bracketed. It still
29//!   identifies the pane (better than nothing when you are staring at a bug), while
30//!   saying plainly that it will not resolve to a file. Every one of these is a pane
31//!   waiting to declare [`Facet::component`](crate::Facet::component) — the badge
32//!   doubles as the to-do list.
33//!
34//! Two failures that render identically make a bug immortal (the ⚒ Build Thing
35//! post-mortem, quoted in `errcode_raise_sites.rs`). The same reasoning applies to two
36//! *confidences* that render identically.
37//!
38//! # When it shows
39//!
40//! ON in debug builds. In a RELEASE build it is off by default but can be switched on
41//! with **`FACETT_DEVID=1`** — because the binaries people actually run on this fleet
42//! are release builds. Gating on `debug_assertions` ALONE made the whole feature
43//! invisible to the person who asked for it: every pane was wired, every test was
44//! green, and the installed `korp-ui` painted nothing, because `badge` had compiled to
45//! an empty function. A debug-only debugging aid is not a debugging aid. The function is
46//! always *present* so no call site needs a `cfg`.
47
48use egui::{Align2, Color32, FontId, Rect, Sense, Stroke, Ui};
49
50/// The prefix every dev-id atom carries in the a11y tree, so a headless test can find
51/// the chip among a pane's own atoms without matching on the id text itself.
52pub const DEVID_ATOM: &str = "dev-id:";
53
54/// The suffix the chip's atom carries when its pane is the one the pointer is in.
55///
56/// This is what makes FOCUS assertable rather than merely visible: a robot drive can ask
57/// the AccessKit tree *which pane am I in* instead of inferring it from what it clicked
58/// last. Rickard's ask — *"you could make it flash when in focus, that would be nice for
59/// robot tests"* — with the flash made deterministic (see [`badge`]).
60pub const FOCUSED_MARK: &str = "[focused]";
61
62/// The marker a pane that has not declared a component renders instead of an id.
63/// Deliberately not id-shaped — it can never be mistaken for a `facet-*` code.
64pub const UNREGISTERED: &str = "⟨unregistered⟩";
65
66/// **Normalise a raw dev-id to a registry component.**
67///
68/// The elm bridge macro fills [`Facet::component`](crate::Facet::component) with
69/// `module_path!()`, which expands at the CALL site — so a pane in `facett-geomap`
70/// reports `facett_geomap` (or `facett_geomap::inner`) with no per-pane code at all.
71/// This maps that to the registry's vocabulary: take the crate segment, drop the
72/// `facett_` prefix, and spell `_` as `-`. `facett_geomap::x` → `geomap`,
73/// `facett_map3d` → `map3d`.
74///
75/// A hand-written `component()` returning a bare `"geomap"` passes through unchanged, so
76/// both spellings are legal and neither needs to know about the other.
77pub fn normalize(raw: &str) -> &str {
78    let head = raw.split("::").next().unwrap_or(raw);
79    head.strip_prefix("facett_").unwrap_or(head)
80}
81
82/// **Resolve a dev-id to its source** — `(crate_dir, src_file)` for a component that the
83/// [`errcode`](crate::errcode) registry knows, `None` otherwise.
84///
85/// This is what makes a pasted badge actionable rather than decorative: the component
86/// carried by every `facet-<component>-<n>` code is the same string the badge shows, so
87/// one lookup turns *"I am here"* into a file to open. `None` is the honest answer for a
88/// pane that has not registered — the badge renders [`UNREGISTERED`] in that case rather
89/// than inventing a path.
90pub fn resolve(component: &str) -> Option<(&'static str, &'static str)> {
91    let want = normalize(component);
92    crate::errcode::REGISTRY
93        .iter()
94        .find(|e| e.component == want)
95        .map(|e| (e.crate_dir, e.src_file))
96}
97
98/// Is this component known to the [`errcode`](crate::errcode) registry?
99pub fn is_registered(component: &str) -> bool {
100    resolve(component).is_some()
101}
102
103/// **What the badge should say** for a pane — the pure decision, split out from any
104/// painting so it is testable without a GPU (and so the release build's no-op cannot
105/// silently diverge from what debug shows).
106///
107/// Returns `(text, registered)`. An empty `component` falls back to the pane title, and
108/// so does a component the registry does not know: claiming an unregistered string is a
109/// valid dev-id is exactly the `facet-map-99` failure.
110pub fn badge_text(component: &str, title: &str) -> (String, bool) {
111    if !component.is_empty() && is_registered(component) {
112        let comp = normalize(component);
113        // `component/pane` — the component alone is not enough to locate a human. korp
114        // draws every one of its panes from ONE `SceneHost::slot` dispatch, so a chip
115        // reading just "korp" would be identical on all of them, which is no better
116        // than no chip. With the slot it reads `korp/ingest_load`, and for a facett
117        // deck it reads `map3d/OSM 3D` — a crate to open AND the pane inside it.
118        let t = title.trim();
119        if t.is_empty() { (comp.to_string(), true) } else { (format!("{comp}/{t}"), true) }
120    } else {
121        let what = if component.is_empty() { title } else { normalize(component) };
122        (format!("{UNREGISTERED} {what}"), false)
123    }
124}
125
126/// **The chip's colours** — `(foreground, background plate)`, split out so the OPACITY of
127/// the plate is assertable without a GPU.
128///
129/// The plate must be **fully opaque**, and that is not a style preference. The chip is
130/// painted over the top-right corner of a pane that has already drawn there, so a
131/// translucent plate does not put the id *on* the pane, it superimposes two strings in the
132/// same pixels. Measured on a real release window (facett-demo, `FACETT_DEVID=1`): the
133/// chip `⟨unregistered⟩ Velo L1` landed on the pane's own header
134/// `L0 + L1 — vello off (feature off / no GPU)` and both became hard to read through each
135/// other, with the old `from_black_alpha(120)` plate letting the text underneath bleed
136/// straight through. An id you have to decode is an id you do not paste.
137///
138/// Registered ids read as actionable (bright blue); unregistered ones stay deliberately
139/// dim in the FOREGROUND, so the two confidences are still tellable apart at a glance —
140/// the dimming lives in the text colour, never in the plate.
141pub fn chip_colors(registered: bool) -> (Color32, Color32) {
142    if registered {
143        (Color32::from_rgb(150, 200, 255), Color32::from_rgb(12, 14, 20))
144    } else {
145        (Color32::from_gray(150), Color32::from_rgb(20, 20, 22))
146    }
147}
148
149/// **The chip's colours when its pane HAS focus** — brighter foreground, a lifted plate.
150///
151/// Same shape as [`chip_colors`] so the registered/unregistered distinction survives being
152/// focused: a focused unregistered pane must still read as unregistered, or the marker
153/// stops meaning anything.
154pub fn chip_colors_focused(registered: bool) -> (Color32, Color32) {
155    if registered {
156        (Color32::from_rgb(235, 245, 255), Color32::from_rgb(24, 48, 90))
157    } else {
158        (Color32::from_rgb(225, 225, 230), Color32::from_rgb(52, 52, 58))
159    }
160}
161
162/// **Paint the dev-id chip** in the top-right of `rect`, and copy the id to the clipboard
163/// when clicked (so it lands in a chat with one click, which is the entire point).
164///
165/// A no-op in release builds. `rect` is the pane's own rect — the caller passes it rather
166/// than reading `ui.max_rect()` after the pane drew, because a pane that consumed the
167/// whole `Ui` leaves the cursor somewhere unhelpful.
168pub fn badge(ui: &mut Ui, rect: Rect, component: &str, title: &str) {
169    if !enabled() {
170        return;
171    }
172    let (text, registered) = badge_text(component, title);
173    let font = FontId::monospace(10.0);
174    // FOCUS: the pane the pointer is in. Deliberately NOT an animated pulse — the render
175    // oracle compares snapshot images, so anything time-based would make every snapshot
176    // differ from the last and the oracle would be worthless. Pointer containment is
177    // deterministic under a driver (the pointer is scripted) and reads as a highlight to a
178    // human watching a visible drive, which was the point of the request.
179    let focused = ui.rect_contains_pointer(rect);
180    let (fg, bg) = if focused { chip_colors_focused(registered) } else { chip_colors(registered) };
181
182    let galley = ui.painter().layout_no_wrap(text.clone(), font, fg);
183    let size = galley.size();
184    let anchor = egui::pos2(rect.right() - 4.0, rect.top() + 4.0);
185    let chip = Rect::from_min_size(egui::pos2(anchor.x - size.x - 6.0, anchor.y), size)
186        .expand2(egui::vec2(4.0, 2.0));
187
188    // Emitted as a proper a11y ATOM, not bare painted glyphs: a headless test (and the
189    // robot driver) reads the id off the AccessKit tree, so "the chip is really on
190    // screen" is assertable rather than assumed. Painted-only text is invisible to every
191    // check we have — which is how a badge ships green and never appears.
192    let resp = crate::a11y::node(
193        ui,
194        ui.id(),
195        ("devid", &text),
196        Sense::click(),
197        chip,
198        // The atom carries the focus mark too, so `label_contains("[focused]")` answers
199        // "which pane is the robot in" off the tree — no guessing from the last click.
200        crate::a11y::Semantics::button(if focused {
201            format!("{DEVID_ATOM} {text} {FOCUSED_MARK}")
202        } else {
203            format!("{DEVID_ATOM} {text}")
204        }),
205    );
206    let p = ui.painter();
207    p.rect_filled(chip, 3.0, bg);
208    if resp.hovered() {
209        p.rect_stroke(chip, 3.0, Stroke::new(1.0, fg), egui::StrokeKind::Inside);
210    }
211    p.text(chip.center(), Align2::CENTER_CENTER, &text, FontId::monospace(10.0), fg);
212
213    if resp.clicked() {
214        // Copy the bare id, never the ⟨unregistered⟩ decoration — pasting the marker
215        // into a chat would send a string that resolves to nothing.
216        let payload =
217            if registered { normalize(component).to_string() } else { format!("{title} (unregistered pane)") };
218        crate::clipboard::put(ui.ctx(), payload);
219    }
220    let hover = match resolve(component) {
221        Some((dir, file)) => format!("{text}\n{dir}/{file}\nclick to copy"),
222        None => format!("{text}\nthis pane has not declared Facet::component()\nclick to copy"),
223    };
224    resp.on_hover_text(hover);
225}
226
227/// **The gate decision, as a pure function** — `$FACETT_DEVID` (if set and understood)
228/// wins, otherwise the build profile decides.
229///
230/// Split out from [`enabled`] for one reason: `debug_assertions` is a *compile-time*
231/// fact, and under `cargo test` it is always `true`. A test calling `enabled()` can
232/// therefore never observe the release branch — which is precisely how the badge shipped
233/// invisible to every release binary on this fleet with five green tests behind it. With
234/// the profile as a PARAMETER the release branch is reachable from a debug test, and
235/// there is still exactly ONE copy of the rule (LAW 5): [`enabled`] is a one-liner over
236/// this, so a table test here cannot drift from what ships.
237///
238/// It does NOT replace a real release build — a rule that is right and a function that
239/// got compiled away are different failures. See `facett-app/tests/devid_release_profile.rs`
240/// for the guard that actually runs `--release`.
241pub fn resolve_enabled(var: Option<&str>, debug_assertions: bool) -> bool {
242    match var {
243        Some("1" | "true" | "on" | "yes") => true,
244        Some("0" | "false" | "off" | "no") => false,
245        // Unset, empty, or a value we do not understand: the profile decides. An
246        // unrecognised value must never read as "on" — a typo'd `FACETT_DEVID=yes!`
247        // silently enabling a debug overlay in production is the wrong default.
248        _ => debug_assertions,
249    }
250}
251
252/// **Is the dev-id chip switched on?** Debug builds: yes. Release builds: only with
253/// `FACETT_DEVID=1`, so the aid is reachable in the binaries that actually ship.
254/// `FACETT_DEVID=0` turns it off even in debug. Read once and cached — this is called
255/// per pane per frame.
256pub fn enabled() -> bool {
257    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
258    *ON.get_or_init(|| {
259        resolve_enabled(std::env::var("FACETT_DEVID").ok().as_deref(), cfg!(debug_assertions))
260    })
261}
262
263/// **The gate, as observable data** — folded into `FacetDeck::state_json` and
264/// `facett_app::scene::render`'s state under the key `devid`, so a headless oracle can
265/// report *why* there are no chips without anyone opening a window.
266///
267/// ```json
268/// { "enabled": true, "env": "1", "debug_assertions": false, "source": "env" }
269/// ```
270///
271/// * `enabled` — the resolved answer [`badge`] acts on.
272/// * `env` — the raw `$FACETT_DEVID`, or `null`. Distinguishes *"nobody set it"* from
273///   *"it was set to something I did not understand"*, which look identical in `enabled`.
274/// * `debug_assertions` — the profile this binary was compiled in. This is the field that
275///   would have named the third failure on sight.
276/// * `source` — `"env"` when the variable decided, `"profile"` when it did not.
277///
278/// # This does NOT mean a chip is on screen
279///
280/// It reports the GATE, nothing more. `enabled: true` is consistent with a badge wired
281/// into a draw path nobody calls (miss #1), with a container that never calls it at all
282/// (miss #2), and with a chip painted off-screen or behind another pane. Each of those
283/// shipped, and each would have reported `enabled: true` here. This field narrows a
284/// diagnosis; only a screenshot of the real window on the real build profile closes it.
285/// See `facett-app/tests/devid_release_profile.rs` for the assertion on applied output.
286pub fn gate_json() -> serde_json::Value {
287    let env = std::env::var("FACETT_DEVID").ok();
288    let understood = matches!(
289        env.as_deref(),
290        Some("1" | "true" | "on" | "yes" | "0" | "false" | "off" | "no")
291    );
292    serde_json::json!({
293        "enabled": enabled(),
294        "env": env,
295        "debug_assertions": cfg!(debug_assertions),
296        "source": if understood { "env" } else { "profile" },
297    })
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    /// **A badge must never claim an id the registry cannot resolve.** This is the
305    /// `facet-map-99` hole from `errcode_raise_sites.rs`, applied to identity rather
306    /// than to errors: an authoritative-looking id that names nothing costs the reader
307    /// more than showing no id at all.
308    #[test]
309    fn an_unknown_component_is_marked_unregistered_not_rendered_as_an_id() {
310        let (text, ok) = badge_text("definitely-not-a-registered-component", "Some Pane");
311        assert!(!ok, "an unknown component must NOT report as registered");
312        assert!(text.starts_with(UNREGISTERED), "must be visibly marked, got {text:?}");
313        assert!(resolve("definitely-not-a-registered-component").is_none());
314
315        // The empty case (a pane that never declared one) falls back to the title, and
316        // is marked just the same — no component is not the same as a valid component.
317        let (text, ok) = badge_text("", "Some Pane");
318        assert!(!ok);
319        assert!(text.contains("Some Pane"), "the fallback still identifies the pane: {text:?}");
320        assert!(text.starts_with(UNREGISTERED));
321    }
322
323    /// **The elm bridge fills `component()` with `module_path!()`** — so the mapping
324    /// from a crate path to a registry component has to hold, or every bridged pane
325    /// silently renders ⟨unregistered⟩ despite being registered all along.
326    #[test]
327    fn a_module_path_normalises_to_its_registry_component() {
328        assert_eq!(normalize("facett_geomap"), "geomap");
329        assert_eq!(normalize("facett_map3d::inner::deep"), "map3d");
330        assert_eq!(normalize("geomap"), "geomap", "a hand-written bare component passes through");
331        assert!(is_registered("facett_geomap"), "the bridged spelling must resolve");
332        let (text, ok) = badge_text("facett_map3d", "3-D Map");
333        assert!(ok);
334        assert_eq!(text, "map3d/3-D Map", "the chip shows the registry name + the pane");
335        // Two panes of the SAME crate must be tellable apart — the korp case, where
336        // every pane shares one component.
337        let (a, _) = badge_text("korp", "ingest_load");
338        let (b, _) = badge_text("korp", "map");
339        assert_ne!(a, b, "same component, different pane, identical chip: {a} vs {b}");
340    }
341
342    /// **The RELEASE branch of the gate, reached from a debug test.**
343    ///
344    /// This is the hole that let the feature ship invisible: `cargo test` compiles with
345    /// `debug_assertions` ON, so every test saw the debug branch and none of them could
346    /// see that a release binary showed nothing. Passing the profile as a parameter makes
347    /// both branches reachable. The `debug = false` rows are the ones that matter — they
348    /// are the build Rickard actually runs.
349    #[test]
350    fn the_gate_honours_the_env_var_in_a_release_profile_too() {
351        // RELEASE (debug_assertions off): off unless explicitly switched on.
352        assert!(!resolve_enabled(None, false), "release + unset must be OFF");
353        for on in ["1", "true", "on", "yes"] {
354            assert!(
355                resolve_enabled(Some(on), false),
356                "FACETT_DEVID={on} must switch the chip ON in a RELEASE build — that is \
357                 the whole escape hatch, and without it the aid is invisible to the \
358                 binaries people run"
359            );
360        }
361        for off in ["0", "false", "off", "no"] {
362            assert!(!resolve_enabled(Some(off), false), "FACETT_DEVID={off} must stay OFF");
363        }
364        // An unrecognised value must fall back to the profile, never to "on".
365        assert!(!resolve_enabled(Some("banana"), false), "an unknown value must not enable it");
366        assert!(!resolve_enabled(Some(""), false), "an empty value must not enable it");
367
368        // DEBUG: on by default, and switchable off — so a developer can silence it.
369        assert!(resolve_enabled(None, true), "debug + unset must be ON");
370        assert!(!resolve_enabled(Some("0"), true), "FACETT_DEVID=0 must silence it in debug");
371        assert!(resolve_enabled(Some("banana"), true), "an unknown value falls back to profile");
372
373        // And the SHIPPED entry point must agree with the rule for THIS build, or the
374        // table above is testing a function nothing calls (the tautology this replaced).
375        // `enabled()` caches, so this only reads it — the env cases live above.
376        if std::env::var_os("FACETT_DEVID").is_none() {
377            assert_eq!(
378                enabled(),
379                cfg!(debug_assertions),
380                "enabled() disagrees with resolve_enabled for this build profile"
381            );
382        }
383    }
384
385    /// **The chip's plate must OCCLUDE, not tint.**
386    ///
387    /// The chip is drawn over a corner the pane has already painted. With a translucent
388    /// plate that is not a badge on a pane, it is two strings sharing pixels — seen on a
389    /// real release window, where `⟨unregistered⟩ Velo L1` sat on top of the pane's own
390    /// header and neither was readable. "The chip is there" and "the chip is legible" are
391    /// different claims and this asserts the second.
392    #[test]
393    fn the_chip_plate_is_opaque_so_it_does_not_superimpose_two_strings() {
394        for registered in [true, false] {
395            let (fg, bg) = chip_colors(registered);
396            assert_eq!(
397                bg.a(),
398                255,
399                "the chip plate is translucent (alpha {}) for registered={registered} — the \
400                 pane's own text bleeds through it and the id becomes unreadable",
401                bg.a()
402            );
403            // A plate that occludes is worthless if the text on it does not stand out.
404            let lum = |c: Color32| 0.299 * c.r() as f32 + 0.587 * c.g() as f32 + 0.114 * c.b() as f32;
405            assert!(
406                (lum(fg) - lum(bg)).abs() > 60.0,
407                "chip fg/bg are too close to read (registered={registered}): {fg:?} on {bg:?}"
408            );
409        }
410        // The two CONFIDENCES must still be tellable apart — that distinction was the
411        // point of the ⟨unregistered⟩ marker, and it must survive the opacity fix.
412        assert_ne!(chip_colors(true).0, chip_colors(false).0, "registered/unregistered look identical");
413    }
414
415    /// **The gate must be REPORTED, not just obeyed.** A headless oracle reads this off
416    /// `state_json`; if it disagreed with what `badge` acts on it would be worse than
417    /// absent — a confident wrong answer during a hunt.
418    #[test]
419    fn the_reported_gate_matches_the_one_the_badge_acts_on() {
420        let g = gate_json();
421        assert_eq!(
422            g["enabled"], enabled(),
423            "state_json reports a different gate than badge() obeys: {g}"
424        );
425        assert_eq!(
426            g["debug_assertions"], cfg!(debug_assertions),
427            "the reported profile is not this binary's profile: {g}"
428        );
429        // `source` must name who decided, so "unset" and "set to nonsense" are tellable
430        // apart — they resolve identically in `enabled` and mean very different things.
431        let by_env = std::env::var("FACETT_DEVID")
432            .map(|v| matches!(v.as_str(), "1" | "true" | "on" | "yes" | "0" | "false" | "off" | "no"))
433            .unwrap_or(false);
434        assert_eq!(g["source"], if by_env { "env" } else { "profile" }, "wrong source: {g}");
435    }
436
437    /// The happy path, driven off a component the registry really carries — so this test
438    /// cannot pass by the lookup being dead (the identity-value trap: a resolver that
439    /// always returns `None` would pass the test above alone).
440    #[test]
441    fn a_registered_component_resolves_to_its_crate_and_file() {
442        let known = crate::errcode::REGISTRY.first().expect("the registry is not empty");
443        let (text, ok) = badge_text(known.component, "ignored title");
444        assert!(ok, "{} is in the registry but did not report registered", known.component);
445        assert_eq!(text, format!("{}/ignored title", known.component), "registered ⇒ component/pane");
446        let (dir, file) = resolve(known.component).expect("resolves");
447        assert!(!dir.is_empty() && !file.is_empty(), "a resolved id must name a real place");
448    }
449}