Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

dwarves-gui/src/coderoom.rs

//! **The Coderoom tab** — the code room of the SELECTED workspace, as nornir's server
//! answers it.
//!
//! The pane is the shared, presentational [`facett_coderoom_view::CoderoomPane`]. The
//! host seam ([`RoomCoderoom`]) asks the room through the forge (`CodeRoom.Answer`,
//! [`crate::room`]) and maps the answers onto the pane's shapes — nothing is computed
//! here:
//!
//! | what the pane draws | asked of | answer |
//! |---|---|---|
//! | the crate graph: one node per `name@version`, a join per pair with its dependency / call / type-use counts | `Viz.CrateGraph` | the room's quotient by crate |
//! | what changed: crates added / removed, symbols added / removed / changed per crate | `Coderoom.SnapshotDiff` per consumed repo | the room at its last two consumed commits |
//! | compat verdicts | `Coderoom.Compat` | `not_wired` — the world ring (crates.io versions of other crates) has no producer |
//! | what breaks | `Coderoom.WhatBreaks` | `not_wired`, for the same reason |
//!
//! ## No verdict without a producer
//! A compatibility verdict answers *may I bump this?*. Nothing produces one today, so
//! the pane draws the two `not_wired` states with nornir's own sentences, no red or
//! green edge, no "0 breaking edges" and no impact set. The joins it does draw are
//! counts, in a neutral colour.
//!
//! ## Checkable
//! Under the `via …` line the pane prints the room the graph was read from —
//! `workspace w · repo@seq (commit) · N nodes · M edges` — and the two states of each
//! diff. The same question asked of the server (`CodeRoom.Answer`, `Viz.CrateGraph`)
//! gives the same numbers.
//!
//! ## What this replaced
//! The tab used to read a DEDICATED pre-v2 "coderoom" warehouse (`symbol_facts` /
//! `semver_diffs` / `call_edges`) and compute the compat verdict, the snapshot diff
//! and a transitive impact closure in the UI. Those tables have no writer, and a UI
//! does not compute verdicts. All of it is gone; `$DWARVES_CODEROOM_WORKSPACE` is no
//! longer read.

use std::sync::Arc;

use facett_coderoom_view::{
    CoderoomDataSeam, CoderoomPane, CoderoomSnapshotDiff, CompatEdge, CrateDelta, CrateLink,
    CrateNode, LayerState, WhatBreaks, LAYER_DIFF, LAYER_GRAPH, LAYER_VERDICTS,
};
use facett_core::Facet;

use crate::room::{self, ForgeRoom, Pending, RoomAnswer, RoomSource};

/// The crate-graph surface.
pub const S_GRAPH: &str = "Viz.CrateGraph";
/// The snapshot-diff surface (asked once per consumed repo).
pub const S_DIFF: &str = "Coderoom.SnapshotDiff";
/// The compat-verdict surface (waits for the world ring).
pub const S_COMPAT: &str = "Coderoom.Compat";
/// The what-breaks surface (waits for the world ring).
pub const S_WHAT_BREAKS: &str = "Coderoom.WhatBreaks";

/// The pane's fourth layer name (the facett pane names the other three).
pub const LAYER_WHAT_BREAKS: &str = "what breaks";

/// Is the dependency ring folded into the crate graph? NO. The tab draws the room of
/// the SELECTED WORKSPACE: its own crates, and every crate their code depends on or
/// calls into (a dependency is then the reference stubs the workspace's code points
/// at — its `nodes` are those stubs, and the reader says so in the evidence).
///
/// Measured 2026-10-13 on znippy-zoomies (one repo, 12 local crates): ring off = 60
/// crates / 137 joins / 18 668 nodes, answered in under a second; ring on = 483 crates /
/// 2 843 joins / 725 787 nodes, 53 s for the first fold on a debug server — the whole
/// transitive closure and std, which is the world ring's picture, not this workspace's.
pub const WITH_RING: bool = false;

/// Everything one read of the room brought back for this tab.
#[derive(Debug, Clone)]
pub struct Reading {
    pub graph: RoomAnswer,
    pub compat: RoomAnswer,
    pub what_breaks: RoomAnswer,
    /// One per consumed repo of the graph answer, in its order.
    pub diffs: Vec<RoomAnswer>,
}

impl Reading {
    /// Ask the room. The diff is asked per repo the graph answer names; a graph that
    /// did not answer names none, and the diff layer says so.
    pub fn read(source: &dyn RoomSource) -> Self {
        let graph = source.ask(S_GRAPH, &serde_json::json!({ "universe": WITH_RING }));
        let compat = source.ask(S_COMPAT, &serde_json::json!({}));
        let what_breaks = source.ask(S_WHAT_BREAKS, &serde_json::json!({}));
        let diffs = graph
            .rows("repos")
            .iter()
            .filter_map(|r| r["repo"].as_str())
            .map(|repo| source.ask(S_DIFF, &serde_json::json!({ "repo": repo })))
            .collect();
        Self {
            graph,
            compat,
            what_breaks,
            diffs,
        }
    }

    /// The reading a tab holds before its first read landed.
    fn pending(source: &dyn RoomSource) -> Self {
        let via = source.describe();
        Self {
            graph: RoomAnswer::pending(S_GRAPH, &via),
            compat: RoomAnswer::pending(S_COMPAT, &via),
            what_breaks: RoomAnswer::pending(S_WHAT_BREAKS, &via),
            diffs: Vec::new(),
        }
    }
}

fn u(v: &serde_json::Value) -> u64 {
    v.as_u64().unwrap_or(0)
}

/// The crate nodes of a graph answer — the reader's rows, field for field.
pub fn crates_of(graph: &RoomAnswer) -> Vec<CrateNode> {
    graph
        .rows("crates")
        .iter()
        .map(|c| {
            CrateNode::new(
                c["name"].as_str().unwrap_or("?"),
                c["version"].as_str().unwrap_or(""),
                u(&c["symbols"]),
            )
        })
        .collect()
}

/// The joins of a graph answer — the reader's rows, field for field.
pub fn links_of(graph: &RoomAnswer) -> Vec<CrateLink> {
    graph
        .rows("edges")
        .iter()
        .map(|e| CrateLink {
            from: e["from"].as_str().unwrap_or("?").to_string(),
            to: e["to"].as_str().unwrap_or("?").to_string(),
            depends_on: u(&e["depends_on"]),
            calls: u(&e["calls"]),
            call_sites: u(&e["call_sites"]),
            uses_type: u(&e["uses_type"]),
        })
        .collect()
}

/// The pane's "what changed" model from the diff answers that are measurements: the
/// reader's per-crate counts, the states' names joined as the two labels. `None` when
/// no diff was measured (the mode is then not offered; the layer says why).
pub fn diff_of(diffs: &[RoomAnswer]) -> Option<CoderoomSnapshotDiff> {
    let measured: Vec<&RoomAnswer> = diffs.iter().filter(|d| d.is_measured()).collect();
    if measured.is_empty() {
        return None;
    }
    let strs = |v: &serde_json::Value| -> Vec<String> {
        v.as_array()
            .map(|a| {
                a.iter()
                    .filter_map(|x| x.as_str().map(str::to_string))
                    .collect()
            })
            .unwrap_or_default()
    };
    let label = |key: &str| {
        measured
            .iter()
            .filter_map(|d| d.body[key].as_str())
            .collect::<Vec<_>>()
            .join(", ")
    };
    let mut out = CoderoomSnapshotDiff {
        snapshot_a: label("from"),
        snapshot_b: label("to"),
        crates_added: Vec::new(),
        crates_removed: Vec::new(),
        crates_changed: Vec::new(),
    };
    for d in measured {
        let by = &d.body["by_crate"];
        out.crates_added.extend(strs(&by["crates_added"]));
        out.crates_removed.extend(strs(&by["crates_removed"]));
        for c in by["crates"].as_array().map(Vec::as_slice).unwrap_or(&[]) {
            let (a, r, ch) = (
                u(&c["symbols_added"]),
                u(&c["symbols_removed"]),
                u(&c["symbols_changed"]),
            );
            if a + r + ch > 0 {
                out.crates_changed.push(CrateDelta::new(
                    c["crate"].as_str().unwrap_or("?"),
                    a,
                    r,
                    ch,
                ));
            }
        }
    }
    Some(out)
}

/// One line per diff: the two states, the node counts, and the state when it is not a
/// measurement.
fn diff_lines(diffs: &[RoomAnswer]) -> Vec<String> {
    diffs
        .iter()
        .map(|d| {
            if d.is_measured() {
                format!(
                    "diff {} → {}: {} nodes added · {} removed · {} changed",
                    d.body["from"].as_str().unwrap_or("?"),
                    d.body["to"].as_str().unwrap_or("?"),
                    u(&d.body["added_nodes"]),
                    u(&d.body["removed_nodes"]),
                    u(&d.body["changed_nodes"]),
                )
            } else {
                format!("diff [{}]: {}", d.state, d.evidence_text())
            }
        })
        .collect()
}

/// The diff layer's one state: the first diff that is not a measurement decides (its
/// state and sentence), else the first measured one.
fn diff_layer(reading: &Reading) -> LayerState {
    if reading.diffs.is_empty() {
        let why = if reading.graph.is_measured() {
            "the crate graph names no consumed repo to diff"
        } else {
            "the crate graph did not answer, so no repo is known to diff"
        };
        return LayerState::new(LAYER_DIFF, "not_asked", false, why);
    }
    let pick = reading
        .diffs
        .iter()
        .find(|d| !d.is_measured())
        .unwrap_or(&reading.diffs[0]);
    LayerState::new(
        LAYER_DIFF,
        pick.state.clone(),
        reading.diffs.iter().all(RoomAnswer::is_measured),
        pick.evidence_text(),
    )
}

/// **The host seam**: one [`Reading`] of the room, mapped onto the pane's shapes.
pub struct RoomCoderoom {
    reading: Reading,
    source: String,
}

impl RoomCoderoom {
    pub fn new(reading: Reading, source: String) -> Self {
        Self { reading, source }
    }
}

impl CoderoomDataSeam for RoomCoderoom {
    fn source(&self) -> &str {
        &self.source
    }
    fn crates(&self) -> Vec<CrateNode> {
        crates_of(&self.reading.graph)
    }
    /// None: no producer writes a verdict (`Coderoom.Compat` is `not_wired`).
    fn compat_edges(&self) -> Vec<CompatEdge> {
        Vec::new()
    }
    /// None, always: "what breaks" has no producer either.
    fn what_breaks(&self, _target: &str) -> Option<WhatBreaks> {
        None
    }
    fn snapshot_diff(&self) -> Option<CoderoomSnapshotDiff> {
        diff_of(&self.reading.diffs)
    }
    fn links(&self) -> Vec<CrateLink> {
        links_of(&self.reading.graph)
    }
    fn layers(&self) -> Vec<LayerState> {
        let r = &self.reading;
        let layer = |name: &str, a: &RoomAnswer| {
            LayerState::new(name, a.state.clone(), a.is_measured(), a.evidence_text())
        };
        vec![
            layer(LAYER_GRAPH, &r.graph),
            layer(LAYER_VERDICTS, &r.compat),
            layer(LAYER_WHAT_BREAKS, &r.what_breaks),
            diff_layer(r),
        ]
    }
    fn identity(&self) -> Vec<String> {
        let mut lines = vec![format!("room: {}", self.reading.graph.room_line())];
        lines.extend(diff_lines(&self.reading.diffs));
        lines
    }
}

/// What the Coderoom tab shows: the connections between crates (the reused
/// [`CoderoomPane`]) or the code as a room in 3-D ([`crate::coderoom3d`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
    Connections,
    Room3D,
}

/// The Coderoom tab: the reused [`CoderoomPane`] over the selected workspace's room,
/// and — the second mode of the same tab — the code room in 3-D.
pub struct CoderoomTab {
    /// The pane over the last reading whose graph was a measurement; `None` until one
    /// lands (the tab then draws the states, never an empty graph).
    inner: Option<Box<dyn facett_core::Panel>>,
    source: Arc<dyn RoomSource>,
    /// The forge endpoint + bearer a `{"set_workspace":…}` re-binds with (`None` for an
    /// injected source, which is not re-bound).
    forge: Option<(String, String)>,
    workspace: String,
    reading: Reading,
    /// A read in flight. The frame polls it; `wait` blocks on it.
    pending: Option<Pending<Reading>>,
    mode: Mode,
    /// The 3-D room, made the first time the mode is picked (it asks its own surface).
    room3d: Option<crate::coderoom3d::CodeRoom3DTab>,
}

impl CoderoomTab {
    /// The shipped tab: the room of `workspace`, asked through the forge at `endpoint`.
    pub fn new(
        endpoint: impl Into<String>,
        token: impl Into<String>,
        workspace: impl Into<String>,
    ) -> Self {
        let (endpoint, token, workspace) = (endpoint.into(), token.into(), workspace.into());
        let mut me = Self::with_source(ForgeRoom::shared(&endpoint, &token, &workspace), workspace);
        me.forge = Some((endpoint, token));
        me
    }

    /// The tab over an injected source (tests). Starts its first read.
    pub fn with_source(source: Arc<dyn RoomSource>, workspace: impl Into<String>) -> Self {
        let reading = Reading::pending(&*source);
        let mut me = Self {
            inner: None,
            source,
            forge: None,
            workspace: workspace.into(),
            reading,
            pending: None,
            mode: Mode::Connections,
            room3d: None,
        };
        me.bind();
        me
    }

    /// **(Re-)read the room** off the egui thread. A re-bind started while one is in
    /// flight is not a second round trip.
    pub fn bind(&mut self) {
        if self.pending.is_some() {
            return;
        }
        let source = Arc::clone(&self.source);
        self.pending = Some(Pending::spawn("coderoom-read", move || {
            Reading::read(&*source)
        }));
    }

    /// Non-blocking: land the in-flight read if the worker finished. Called once/frame.
    pub fn poll(&mut self) {
        let Some(p) = &self.pending else { return };
        if let Some(res) = p.try_take() {
            self.pending = None;
            self.land(res);
        }
    }

    /// Blocking: wait for the in-flight read (a scripted caller; the window never does).
    pub fn wait(&mut self) {
        if let Some(p) = self.pending.take() {
            let res = p.wait();
            self.land(res);
        }
    }

    pub fn is_pending(&self) -> bool {
        self.pending.is_some()
    }

    fn land(&mut self, res: Result<Reading, ()>) {
        let reading = res.unwrap_or_else(|()| {
            let mut r = Reading::pending(&*self.source);
            r.graph = room::worker_died(S_GRAPH, &*self.source);
            r
        });
        // A graph that is not a measurement mounts NO pane: the tab draws the state.
        self.inner = reading.graph.is_measured().then(|| {
            Box::new(
                CoderoomPane::new(Box::new(RoomCoderoom::new(
                    reading.clone(),
                    self.source.describe(),
                )))
                .with_title("Code room"),
            ) as Box<dyn facett_core::Panel>
        });
        self.reading = reading;
    }

    /// The workspace whose room the tab reads.
    pub fn workspace(&self) -> &str {
        &self.workspace
    }

    /// Show `mode`; the 3-D room is made (and asks the server) the first time.
    pub fn set_mode(&mut self, mode: Mode) {
        self.mode = mode;
        if mode == Mode::Room3D && self.room3d.is_none() {
            self.room3d = Some(crate::coderoom3d::CodeRoom3DTab::with_source(
                Arc::clone(&self.source),
                &self.workspace,
            ));
        }
    }
}

impl Facet for CoderoomTab {
    fn title(&self) -> &str {
        "Coderoom"
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        let mut mode = self.mode;
        ui.horizontal(|ui| {
            ui.selectable_value(&mut mode, Mode::Connections, "🔗 connections");
            ui.selectable_value(&mut mode, Mode::Room3D, "🏛 3D room");
        });
        if mode != self.mode {
            self.set_mode(mode);
        }
        if self.mode == Mode::Room3D {
            if let Some(r) = self.room3d.as_mut() {
                r.ui(ui);
            }
            return;
        }
        self.poll();
        if self.pending.is_some() {
            ui.ctx().request_repaint();
        }
        ui.horizontal(|ui| {
            ui.label(format!(
                "Code room of: {}",
                if self.workspace.is_empty() {
                    "(no workspace selected)"
                } else {
                    &self.workspace
                }
            ));
            if let Some(p) = &self.pending {
                ui.separator();
                ui.spinner();
                ui.label(egui::RichText::new(crate::room::asking_line(p.started)).small());
            }
        });
        ui.separator();
        match self.inner.as_mut() {
            // the pane leads with its own `via …`, room line and layer states
            Some(pane) => pane.ui(ui),
            // no measured graph: the four states and their evidence — no graph, no number
            None => {
                room::callout(ui, LAYER_GRAPH, &self.reading.graph);
                room::callout(ui, LAYER_VERDICTS, &self.reading.compat);
                room::callout(ui, LAYER_WHAT_BREAKS, &self.reading.what_breaks);
                for d in &self.reading.diffs {
                    room::callout(ui, LAYER_DIFF, d);
                }
            }
        }
    }

    fn state_json(&self) -> serde_json::Value {
        // The pane's own state when a graph was measured; else NO pane keys at all —
        // a reader of this JSON must not find a `crate_count: 0` that was never counted.
        let mut v = match &self.inner {
            Some(pane) => pane.state_json(),
            None => serde_json::json!({ "kind": "coderoom" }),
        };
        if let Some(obj) = v.as_object_mut() {
            obj.insert("workspace".into(), serde_json::json!(self.workspace));
            obj.insert(
                "mode".into(),
                serde_json::json!(if self.mode == Mode::Room3D {
                    "3d"
                } else {
                    "connections"
                }),
            );
            obj.insert(
                "room3d".into(),
                self.room3d
                    .as_ref()
                    .map(|r| r.state_json())
                    .unwrap_or(serde_json::Value::Null),
            );
            obj.insert("from_room".into(), serde_json::json!(self.inner.is_some()));
            obj.insert("pending".into(), serde_json::json!(self.pending.is_some()));
            obj.insert(
                "room".into(),
                serde_json::json!({
                    "graph": self.reading.graph.state_json(),
                    "compat": self.reading.compat.state_json(),
                    "what_breaks": self.reading.what_breaks.state_json(),
                    "diffs": self.reading.diffs.iter().map(RoomAnswer::state_json).collect::<Vec<_>>(),
                    // the reader's own totals, beside the pane's — the two must agree
                    "nodes": self.reading.graph.is_measured().then(|| self.reading.graph.body["room"]["nodes"].clone()),
                    "edges_total": self.reading.graph.is_measured().then(|| self.reading.graph.body["room"]["edges_total"].clone()),
                }),
            );
        }
        v
    }

    fn update_json(&mut self, msg_json: &str) {
        if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) {
            if let Some(m) = v.get("mode").and_then(|t| t.as_str()) {
                self.set_mode(if m == "3d" {
                    Mode::Room3D
                } else {
                    Mode::Connections
                });
                return;
            }
            // In the 3-D mode every other message but the selector is the room's.
            if self.mode == Mode::Room3D && v.get("set_workspace").is_none() {
                if let Some(r) = self.room3d.as_mut() {
                    r.update_json(msg_json);
                }
                return;
            }
            // The selector's broadcast: the tab reads the SELECTED workspace's room.
            if let Some(ws) = v.get("set_workspace").and_then(|t| t.as_str()) {
                // the 3-D room is made again, over the new workspace, when next shown
                self.room3d = None;
                if let Some((endpoint, token)) = &self.forge {
                    self.source = ForgeRoom::shared(endpoint, token, ws);
                    // an in-flight read of the previous workspace must not land as this one's
                    self.pending = None;
                }
                self.workspace = ws.to_string();
                self.bind();
                if self.mode == Mode::Room3D {
                    self.set_mode(Mode::Room3D);
                }
                return;
            }
            if v.get("reread").and_then(|t| t.as_bool()) == Some(true) {
                self.bind();
                self.wait();
                return;
            }
            if v.get("wait").and_then(|t| t.as_bool()) == Some(true) {
                self.wait();
                return;
            }
        }
        // Anything else (mode / refresh / target) forwards to the reused pane.
        if let Some(pane) = self.inner.as_mut() {
            pane.update_json(msg_json);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::room::testing::{refusal, room, CannedRoom};
    use serde_json::json;

    const COMPAT_NOTE: &str = "Coderoom.Compat: the world ring has no v2 producer (gap P5)";
    const BREAKS_NOTE: &str =
        "Coderoom.WhatBreaks: the registry versions of other crates are not ingested (gap P5)";

    /// A crate graph as the reader answers one: two local crates of one repo, one
    /// dependency, three joins.
    fn graph() -> serde_json::Value {
        json!({
            "state": "complete",
            "crates": [
                { "id": "app@1.0.0", "name": "app", "version": "1.0.0", "repo": "shop", "local": true, "nodes": 60, "symbols": 41, "fns": 30, "files": 4, "calls_inside": 22 },
                { "id": "core@0.3.0", "name": "core", "version": "0.3.0", "repo": "shop", "local": true, "nodes": 25, "symbols": 12, "fns": 9, "files": 2, "calls_inside": 5 },
                { "id": "serde@1.0.228", "name": "serde", "version": "1.0.228", "repo": null, "local": false, "nodes": 900, "symbols": 310, "fns": 200, "files": 0, "calls_inside": 0 },
            ],
            "edges": [
                { "from": "app@1.0.0", "to": "core@0.3.0", "depends_on": 1, "calls": 7, "call_sites": 19, "uses_type": 2 },
                { "from": "app@1.0.0", "to": "serde@1.0.228", "depends_on": 1, "calls": 3, "call_sites": 4, "uses_type": 0 },
                { "from": "core@0.3.0", "to": "serde@1.0.228", "depends_on": 1, "calls": 0, "call_sites": 0, "uses_type": 0 },
            ],
            "repos": [{ "repo": "shop", "seq": 2, "sha": "0123456789abcdef0123", "crates": ["app@1.0.0", "core@0.3.0"] }],
            "repo_edges": [],
            "evidence": { "missing": [], "notes": ["calls through closures are not recorded: a lower bound"] },
            "room": room("w", "shop", 2, 985, 2310),
        })
    }

    fn diff() -> serde_json::Value {
        json!({
            "state": "complete", "from": "shop@1", "to": "shop@2",
            "added_nodes": 3, "removed_nodes": 1, "changed_nodes": 2,
            "by_crate": {
                "crates_added": ["log@0.4.29"], "crates_removed": [],
                "crates": [
                    { "crate": "app@1.0.0", "symbols_added": 2, "symbols_removed": 1, "symbols_changed": 2, "nodes_added": 3, "nodes_removed": 1, "nodes_changed": 2 },
                    { "crate": "core@0.3.0", "symbols_added": 0, "symbols_removed": 0, "symbols_changed": 0, "nodes_added": 0, "nodes_removed": 0, "nodes_changed": 0 },
                ],
            },
            "evidence": { "missing": [], "notes": [] },
        })
    }

    fn fed() -> Arc<CannedRoom> {
        CannedRoom::new(&[
            (S_GRAPH, graph()),
            (S_DIFF, diff()),
            (S_COMPAT, refusal("not_wired", COMPAT_NOTE)),
            (S_WHAT_BREAKS, refusal("not_wired", BREAKS_NOTE)),
        ])
    }

    fn tab(src: &Arc<CannedRoom>) -> CoderoomTab {
        let mut t = CoderoomTab::with_source(src.clone(), "w");
        t.wait();
        t
    }

    /// The tab draws the READER's crate graph — its crates, its joins with their counts,
    /// the room it was read from — and the reader's own states for what has no producer.
    #[test]
    fn the_tab_draws_the_rooms_crate_graph_and_the_readers_states() {
        let src = fed();
        let s = tab(&src).state_json();
        assert_eq!(
            (s["from_room"].as_bool(), s["workspace"].as_str()),
            (Some(true), Some("w"))
        );
        // the graph: values read back, each one the reader's
        assert_eq!(
            (s["crate_count"].as_u64(), s["link_count"].as_u64()),
            (Some(3), Some(3))
        );
        assert_eq!(
            (
                s["depends_on_edges"].as_u64(),
                s["call_edges"].as_u64(),
                s["call_sites"].as_u64()
            ),
            (Some(3), Some(10), Some(23))
        );
        let app = s["crates"]
            .as_array()
            .unwrap()
            .iter()
            .find(|c| c["id"] == "app@1.0.0")
            .expect("app is drawn");
        assert_eq!(app["symbols"], 41);
        assert_eq!(
            s["links"][0],
            json!({ "from": "app@1.0.0", "to": "core@0.3.0", "depends_on": 1, "calls": 7, "call_sites": 19, "uses_type": 2 })
        );
        assert_eq!(
            (s["graph_nodes"].as_u64(), s["graph_edges"].as_u64()),
            (Some(3), Some(3)),
            "the reused viz got the scene"
        );
        // where it came from, and which room
        assert_eq!(s["source"], "canned room (test)");
        assert_eq!(
            s["identity"][0],
            "room: workspace w · shop@2 (0123456789) · 985 nodes · 2310 edges"
        );
        assert_eq!(
            s["identity"][1],
            "diff shop@1 → shop@2: 3 nodes added · 1 removed · 2 changed"
        );
        assert_eq!(
            (
                s["room"]["nodes"].as_u64(),
                s["room"]["edges_total"].as_u64()
            ),
            (Some(985), Some(2310))
        );
        // the diff: the reader's per-crate counts; a crate with no symbol change is not listed
        assert_eq!(s["diff_summary"], "1 added · 0 removed · 1 changed");
        assert_eq!(
            s["snapshot_diff"]["crates_changed"],
            json!([{ "crate_id": "app@1.0.0", "symbols_added": 2, "symbols_removed": 1, "symbols_changed": 2 }])
        );
        // no verdict: the two states with nornir's sentences, and no number
        assert_eq!(s["verdicts_measured"], false);
        assert!(
            s["breaking_edges"].is_null() && s["compatible_edges"].is_null(),
            "{s}"
        );
        assert_eq!(
            (s["edge_count"].as_u64(), s["impacted_count"].as_u64()),
            (Some(0), Some(0))
        );
        let layers = s["layers"].as_array().unwrap();
        assert_eq!(
            layers
                .iter()
                .map(|l| (l["layer"].as_str().unwrap(), l["state"].as_str().unwrap()))
                .collect::<Vec<_>>(),
            vec![
                ("crate graph", "complete"),
                ("compat verdicts", "not_wired"),
                ("what breaks", "not_wired"),
                ("snapshot diff", "complete"),
            ]
        );
        assert_eq!(
            (
                layers[1]["evidence"].as_str(),
                layers[2]["evidence"].as_str()
            ),
            (Some(COMPAT_NOTE), Some(BREAKS_NOTE))
        );
        // what was asked: the graph with the ring, the diff of the repo the graph named
        let asked = src.asked.lock().unwrap().clone();
        assert_eq!(
            asked[0],
            (S_GRAPH.to_string(), json!({ "universe": WITH_RING }))
        );
        assert!(
            asked.contains(&(S_DIFF.to_string(), json!({ "repo": "shop" }))),
            "{asked:?}"
        );
    }

    /// A room that refuses draws its state and NOTHING else: no pane keys, no zero.
    #[test]
    fn a_refused_graph_is_a_state_with_no_number() {
        for (state, note) in [
            (
                "not_consumed",
                "code room of `w`: no repo has a consumer offset yet",
            ),
            ("no_room", "no workspace selected"),
        ] {
            let src = CannedRoom::new(&[
                (S_GRAPH, refusal(state, note)),
                (S_COMPAT, refusal("not_wired", COMPAT_NOTE)),
                (S_WHAT_BREAKS, refusal("not_wired", BREAKS_NOTE)),
            ]);
            let s = tab(&src).state_json();
            assert_eq!(s["from_room"], false);
            assert_eq!(
                (
                    s["room"]["graph"]["state"].as_str(),
                    s["room"]["graph"]["evidence"].as_str()
                ),
                (Some(state), Some(note))
            );
            for key in [
                "crate_count",
                "link_count",
                "breaking_edges",
                "crates",
                "links",
                "summary",
            ] {
                assert!(
                    s.get(key).is_none(),
                    "`{key}` must not exist without a measured graph: {s}"
                );
            }
            assert!(
                s["room"]["nodes"].is_null() && s["room"]["diffs"].as_array().unwrap().is_empty()
            );
            // no repo was named, so no diff was asked
            assert!(!src.asked.lock().unwrap().iter().any(|a| a.0 == S_DIFF));
        }
    }

    /// One consumed commit: the reader compares with the empty state and says so; a
    /// diff that refuses is a named state on its layer and the mode is not offered.
    #[test]
    fn the_diff_layer_carries_the_readers_state() {
        let src = fed();
        src.set(
            S_DIFF,
            refusal("not_consumed", "repo `shop` has no consumer offset"),
        );
        let s = tab(&src).state_json();
        assert!(
            s["snapshot_diff"].is_null(),
            "no measured diff ⇒ no what-changed model"
        );
        assert_eq!(
            s["layers"][3],
            json!({ "layer": "snapshot diff", "state": "not_consumed", "measured": false, "evidence": "repo `shop` has no consumer offset" })
        );
        assert_eq!(
            s["identity"][1],
            "diff [not_consumed]: repo `shop` has no consumer offset"
        );
        assert_eq!(s["crate_count"], 3, "the graph is still the graph");
    }

    /// One tab, two modes: the connections (the crate graph) and the 3-D room, which
    /// is made — and asks its own surface — only when it is picked.
    #[test]
    fn the_3d_room_is_a_mode_of_this_tab_asked_only_when_picked() {
        let src = fed();
        let mut t = tab(&src);
        let s = t.state_json();
        assert_eq!(
            (s["mode"].as_str(), s["room3d"].is_null()),
            (Some("connections"), true),
            "{s}"
        );
        assert!(
            !src.asked
                .lock()
                .unwrap()
                .iter()
                .any(|(surface, _)| surface == crate::coderoom3d::S_CODE_MAP),
            "not asked before it is picked"
        );
        t.update_json(r#"{"mode":"3d"}"#);
        t.update_json(r#"{"wait":true}"#);
        let s = t.state_json();
        assert_eq!(s["mode"], "3d");
        assert_eq!(s["room3d"]["kind"], "code_map", "{s}");
        assert!(
            src.asked
                .lock()
                .unwrap()
                .iter()
                .any(|(surface, _)| surface == crate::coderoom3d::S_CODE_MAP),
            "picked: asked"
        );
        t.update_json(r#"{"mode":"connections"}"#);
        assert_eq!(
            t.state_json()["crate_count"],
            3,
            "the connections are still there"
        );
    }

    /// The selector's broadcast re-reads; an injected source keeps its source.
    #[test]
    fn a_reread_draws_what_the_room_says_now() {
        let src = fed();
        let mut t = tab(&src);
        assert_eq!(t.state_json()["crate_count"], 3);
        let mut g = graph();
        g["crates"].as_array_mut().unwrap().pop();
        g["edges"] = json!([]);
        src.set(S_GRAPH, g);
        t.update_json(r#"{"reread":true}"#);
        let s = t.state_json();
        assert_eq!(
            (s["crate_count"].as_u64(), s["link_count"].as_u64()),
            (Some(2), Some(0))
        );
    }
}