dwarves-gui/src/warehouse_nav.rs
//! **The 🌐 Warehouse Navigator's host half — both tiers are the server's.**
//!
//! The navigator (`nornir-warehouse-nav`) is dwarves' landing view: a
//! workspace → repo → crate → **function** graph. That leaf crate is dependency-light
//! (no gRPC) and holds NO data of its own: this module feeds it, through its two
//! seams, from nornir's code room (`CodeRoom.Answer` on the forge, [`crate::room`]):
//!
//! | tier | seam | asked of | mapped onto |
//! |---|---|---|---|
//! | repos, their crates, repo → repo dependency edges | [`SkeletonSource`] | `Viz.CrateGraph` | `nornir_depgraph::RepoFacts` / `CrossRepoEdge` |
//! | fns per crate + the call edges among them | [`RealFnSource`] | `Viz.FunctionTier` | `nornir_depgraph::funcgraph::CrateFuncGraph` |
//!
//! Field for field: the room's rows are the navigator's rows. Nothing is derived here —
//! no manifest is parsed, no checkout is read, no edge is resolved.
//!
//! ## A source that cannot answer draws its state
//! Each seam's `Err` is the answer's state and evidence in one sentence
//! (`[not_consumed] code room of …`). The navigator then draws NO skeleton / NO
//! function tier and prints that sentence. There is no fixture skeleton and no
//! synthetic function behind either seam.
//!
//! ## What this replaced
//! The skeleton used to be built from the member checkouts on the machine the UI runs
//! on (`WorkspaceGraph::build_from_members` over `$DWARVES_SUITE_ROOT` / the working
//! directory's ancestors), with the glow read off `.git` mtimes — and, when no such
//! directory was found, from a compiled-in nordisk snapshot. The function tier was
//! filler nodes. A UI that reads its own disk shows the laptop's tree, not the
//! server's; both are gone. `$DWARVES_SUITE_ROOT` / `$DWARVES_SKELETON` are no longer
//! read. The recency glow has no server feed yet and is not drawn.
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use facett_core::Facet;
use nornir_depgraph::funcgraph::{CrateFuncGraph, FnEdge, FnNode};
use nornir_depgraph::{CrossRepoEdge, RepoFacts};
use nornir_warehouse_nav::{RealFnSource, Skeleton, SkeletonSource, WarehouseNav};
use crate::room::{ForgeRoom, RoomAnswer, RoomSource};
/// The skeleton surface.
pub const S_SKELETON: &str = "Viz.CrateGraph";
/// The function-tier surface.
pub const S_FUNCTIONS: &str = "Viz.FunctionTier";
/// Fns per crate the navigator asks the function tier for. The tier is a SUMMARY
/// by default (each crate's counts, no fn — the whole room's fns were 37 MB for
/// `nordisk`); the world preview draws at most a few thousand nodes, so a short
/// preview of each crate is all it can use. A crate's every fn is the reader's
/// drill-in (`crate` + `page`).
pub const FN_PREVIEW_PER_CRATE: u64 = 16;
/// The sentence a seam hands the navigator for an answer that is not a measurement:
/// the state by name, then the reader's evidence.
fn refusal_line(a: &RoomAnswer) -> String {
format!("[{}] {}", a.state, a.evidence_text())
}
/// The `name` of a `name@version` crate id.
fn crate_name(id: &str) -> &str {
id.rsplit_once('@').map_or(id, |(n, _)| n)
}
/// **A `Viz.CrateGraph` answer as the navigator's skeleton** — the reader's `repos`
/// (each with the crates it defines and the crates those depend on) and `repo_edges`
/// (the ones that carry a dependency). `workspace` is the breadcrumb root when the
/// answer names none.
pub fn skeleton_of(a: &RoomAnswer, workspace: &str) -> Result<Skeleton, String> {
if !a.is_measured() {
return Err(refusal_line(a));
}
let names = |v: &serde_json::Value| -> BTreeSet<String> {
v.as_array()
.map(|x| {
x.iter()
.filter_map(|c| c.as_str())
.map(|c| crate_name(c).to_string())
.collect()
})
.unwrap_or_default()
};
let mut facts: BTreeMap<String, RepoFacts> = BTreeMap::new();
for r in a.rows("repos") {
let Some(repo) = r["repo"].as_str() else {
continue;
};
facts.insert(
repo.to_string(),
RepoFacts {
name: repo.to_string(),
produces: names(&r["crates"]),
consumes: names(&r["consumes"]),
..Default::default()
},
);
}
let edges = a
.rows("repo_edges")
.iter()
.filter(|e| e["depends_on"].as_u64().unwrap_or(0) > 0)
.filter_map(|e| {
Some(CrossRepoEdge::normal(
e["from"].as_str()?,
e["to"].as_str()?,
names(&e["via"]),
))
})
.collect();
Ok(Skeleton {
root: a.body["room"]["workspace"]
.as_str()
.unwrap_or(workspace)
.to_string(),
facts,
edges,
source: format!(
"{S_SKELETON} [{}] · {} · via {}",
a.state,
a.room_line(),
a.via
),
})
}
/// **A `Viz.FunctionTier` answer as the navigator's function tier** — per repo, per
/// crate, the reader's fns (its readable path is the id, its name the label) and the
/// call edges among them (indexes into that crate's fns, resolved to ids here).
pub fn functions_of(a: &RoomAnswer) -> Result<Vec<(String, Vec<CrateFuncGraph>)>, String> {
if !a.is_measured() {
return Err(refusal_line(a));
}
let mut out = Vec::new();
for r in a.rows("repos") {
let Some(repo) = r["repo"].as_str() else {
continue;
};
let mut graphs = Vec::new();
for c in r["crates"].as_array().map(Vec::as_slice).unwrap_or(&[]) {
let Some(krate) = c["crate"].as_str() else {
continue;
};
let nodes: Vec<FnNode> = c["fns"]
.as_array()
.map(Vec::as_slice)
.unwrap_or(&[])
.iter()
.map(|f| FnNode {
id: f["id"].as_str().unwrap_or("?").to_string(),
name: f["name"].as_str().unwrap_or("?").to_string(),
crate_name: krate.to_string(),
})
.collect();
let edges = c["calls"]
.as_array()
.map(Vec::as_slice)
.unwrap_or(&[])
.iter()
.filter_map(|e| {
let (f, t) = (e[0].as_u64()? as usize, e[1].as_u64()? as usize);
Some(FnEdge {
from: nodes.get(f)?.id.clone(),
to: nodes.get(t)?.id.clone(),
})
})
.collect();
graphs.push(CrateFuncGraph {
crate_name: krate.to_string(),
nodes,
edges,
});
}
out.push((repo.to_string(), graphs));
}
Ok(out)
}
/// **The shipped source of both tiers**: nornir's code room, through the forge. It
/// keeps the last answer of each surface so the pane can show its state, evidence and
/// the room it was read from.
pub struct RoomNavSource {
room: Arc<dyn RoomSource>,
workspace: String,
last: Arc<std::sync::Mutex<BTreeMap<&'static str, RoomAnswer>>>,
}
impl RoomNavSource {
pub fn new(room: Arc<dyn RoomSource>, workspace: impl Into<String>) -> Self {
Self {
room,
workspace: workspace.into(),
last: Arc::default(),
}
}
fn ask(&self, surface: &'static str, params: serde_json::Value) -> RoomAnswer {
let a = self.room.ask(surface, ¶ms);
self.last
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(surface, a.clone());
a
}
/// The same source for the other seam (they share the kept answers).
fn twin(&self) -> Self {
Self {
room: Arc::clone(&self.room),
workspace: self.workspace.clone(),
last: Arc::clone(&self.last),
}
}
}
impl SkeletonSource for RoomNavSource {
fn fetch(&self) -> Result<Skeleton, String> {
// the workspace's own crates: the ring's crates are not repos of this workspace
skeleton_of(
&self.ask(S_SKELETON, serde_json::json!({ "universe": false })),
&self.workspace,
)
}
fn describe(&self) -> String {
format!("{S_SKELETON} via {}", self.room.describe())
}
}
impl RealFnSource for RoomNavSource {
fn fetch(&self) -> Result<Vec<(String, Vec<CrateFuncGraph>)>, String> {
functions_of(&self.ask(
S_FUNCTIONS,
serde_json::json!({ "per_crate": FN_PREVIEW_PER_CRATE }),
))
}
fn describe(&self) -> String {
format!("{S_FUNCTIONS} via {}", self.room.describe())
}
}
/// The Warehouse tab: the navigator, bound to the SELECTED workspace's room. The
/// selector's broadcast re-binds it (a new navigator over the new workspace's sources
/// — nothing of the previous workspace stays on screen).
pub struct NavTab {
nav: WarehouseNav,
/// The answers the sources last got, by surface (for `state_json`).
last: Arc<std::sync::Mutex<BTreeMap<&'static str, RoomAnswer>>>,
/// The forge endpoint + bearer a re-bind asks with (`None` for an injected source).
forge: Option<(String, String)>,
workspace: String,
}
impl NavTab {
/// The shipped tab: the room of `workspace`, asked through the forge at `endpoint`.
pub fn new(endpoint: &str, token: &str, workspace: &str) -> Self {
let mut me = Self::with_source(ForgeRoom::shared(endpoint, token, workspace), workspace);
me.forge = Some((endpoint.to_string(), token.to_string()));
me
}
/// The tab over an injected room source (tests).
pub fn with_source(room: Arc<dyn RoomSource>, workspace: &str) -> Self {
let source = RoomNavSource::new(room, workspace);
let last = Arc::clone(&source.last);
let nav = WarehouseNav::empty(workspace)
.with_skeleton_feed(Box::new(source.twin()))
.with_real_source(Box::new(source));
Self {
nav,
last,
forge: None,
workspace: workspace.to_string(),
}
}
}
impl Facet for NavTab {
fn title(&self) -> &str {
Facet::title(&self.nav)
}
fn ui(&mut self, ui: &mut egui::Ui) {
// WHERE each tier came from, above the graph: the answer's state as a chip, its
// evidence, the room it was read from (workspace · repo@seq (commit) · nodes ·
// edges) and the door it was asked through. Not asked yet ⇒ no line.
{
let last = self.last.lock().unwrap_or_else(|e| e.into_inner());
for (what, surface) in [
("skeleton (repos · crates)", S_SKELETON),
("function tier", S_FUNCTIONS),
] {
if let Some(a) = last.get(surface) {
crate::room::callout(ui, what, a);
}
}
}
Facet::ui(&mut self.nav, ui);
}
fn state_json(&self) -> serde_json::Value {
let mut v = Facet::state_json(&self.nav);
if let Some(o) = v.as_object_mut() {
let last = self.last.lock().unwrap_or_else(|e| e.into_inner());
let of = |s: &str| {
last.get(s)
.map(RoomAnswer::state_json)
.unwrap_or(serde_json::Value::Null)
};
o.insert("workspace".into(), serde_json::json!(self.workspace));
// what each tier rests on: the answer's state, evidence and room (null = not asked yet)
// …and the reader's own totals beside what the pane drew (the world preview
// is capped; these are not): null when that tier is not a measurement.
let total = |s: &str, key: &str| {
last.get(s)
.filter(|a| a.is_measured())
.and_then(|a| a.body[key].as_u64())
};
let listed = |s: &str, key: &str| {
last.get(s)
.filter(|a| a.is_measured())
.map(|a| a.rows(key).len())
};
o.insert(
"room".into(),
serde_json::json!({
"skeleton": of(S_SKELETON),
"functions": of(S_FUNCTIONS),
"repos": listed(S_SKELETON, "repos"),
"crates": listed(S_SKELETON, "crates"),
"fns_total": total(S_FUNCTIONS, "total_fns"),
"calls_total": total(S_FUNCTIONS, "total_calls"),
}),
);
}
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(ws) = v.get("set_workspace").and_then(|t| t.as_str()) {
if ws != self.workspace {
if let Some((endpoint, token)) = self.forge.clone() {
*self = Self::new(&endpoint, &token, ws);
}
}
return;
}
}
Facet::update_json(&mut self.nav, msg_json);
}
}
/// **The shipped Warehouse Navigator landing pane**, bound to the selected workspace.
/// Safe to mount when the server is down: it then draws the state of each tier.
#[must_use]
pub fn warehouse_nav_pane(
endpoint: &str,
token: &str,
workspace: &str,
) -> Box<dyn facett_core::Panel> {
Box::new(NavTab::new(endpoint, token, workspace))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::room::testing::{refusal, room, CannedRoom};
use serde_json::json;
/// A crate graph as the reader answers one: `shop` (two crates) and `web`, web
/// depending on shop, shop on the dependency `serde`.
fn graph() -> serde_json::Value {
json!({
"state": "complete",
"crates": [],
"edges": [],
"repos": [
{ "repo": "shop", "seq": 2, "sha": "0123456789abcdef", "crates": ["shop@0.1.0", "shop-core@0.1.0"], "consumes": ["serde", "shop-core"] },
{ "repo": "web", "seq": 1, "sha": "fedcba9876543210", "crates": ["web@0.2.0"], "consumes": ["shop"] },
],
"repo_edges": [
{ "from": "web", "to": "shop", "depends_on": 1, "calls": 4, "via": ["shop"] },
// calls only, no dependency edge: not a skeleton edge
{ "from": "shop", "to": "web", "depends_on": 0, "calls": 1, "via": [] },
],
"evidence": { "missing": [], "notes": [] },
"room": room("shop-ws", "shop", 2, 31, 60),
})
}
fn tier() -> serde_json::Value {
json!({
"state": "complete",
"repos": [{
"repo": "shop",
"crates": [
{ "crate": "shop", "version": "0.1.0", "total_fns": 3, "total_calls": 2,
"fns": [{ "id": "shop::checkout", "name": "checkout" }, { "id": "shop::pay::charge", "name": "charge" }, { "id": "shop::pay::audit", "name": "audit" }],
"calls": [[0, 1, 3], [1, 2, 2]] },
{ "crate": "shop-core", "version": "0.1.0", "total_fns": 0, "total_calls": 0, "fns": [], "calls": [] },
],
}],
"total_fns": 3, "total_calls": 2,
"evidence": { "missing": [], "notes": ["a lower bound"] },
"room": room("shop-ws", "shop", 2, 31, 60),
})
}
fn tab(src: &Arc<CannedRoom>) -> NavTab {
let mut t = NavTab::with_source(src.clone(), "shop-ws");
t.update_json(r#"{"read":true}"#);
t
}
/// The skeleton drawn is the reader's: its repos, the crates it says each defines,
/// and only the repo edges that carry a dependency.
#[test]
fn the_skeleton_is_the_rooms_crate_graph() {
let src = CannedRoom::new(&[(S_SKELETON, graph()), (S_FUNCTIONS, tier())]);
let mut t = tab(&src);
let s = t.state_json();
assert_eq!(
(s["repo_count"].as_u64(), s["edge_count"].as_u64()),
(Some(2), Some(1)),
"{s}"
);
assert_eq!(
s["edges"],
json!([{ "from": "repo:web", "to": "repo:shop", "dashed": false }])
);
assert_eq!(s["breadcrumb"], json!(["shop-ws"]));
assert!(s["skeleton_error"].is_null());
assert_eq!(s["skeleton_source"], "Viz.CrateGraph [complete] · workspace shop-ws · shop@2 (0123456789) · 31 nodes · 60 edges · via canned room (test)");
assert_eq!(s["room"]["skeleton"]["state"], "complete");
assert!(
s["room"]["functions"].is_null(),
"the function tier is not asked on the landing"
);
// the workspace's own crates only — the ring was not asked for
assert_eq!(
src.asked.lock().unwrap()[0],
(S_SKELETON.to_string(), json!({ "universe": false }))
);
// drill: the crates of `shop` are the two the reader named
t.update_json(r#"{"drill":"repo:shop"}"#);
let s = t.state_json();
let crates: Vec<&str> = s["nodes"]
.as_array()
.unwrap()
.iter()
.filter(|n| n["kind"] == "crate")
.filter_map(|n| n["label"].as_str())
.collect();
assert_eq!(crates, vec!["shop", "shop-core"]);
}
/// The function tier drawn is the reader's: its fns by name, its call edges, and
/// nothing under a crate it named no fn of.
#[test]
fn the_function_tier_is_the_rooms_function_tier() {
let src = CannedRoom::new(&[(S_SKELETON, graph()), (S_FUNCTIONS, tier())]);
let mut t = tab(&src);
t.update_json(r#"{"world_target":100000}"#);
t.update_json(r#"{"world":true}"#);
let s = t.state_json();
assert_eq!(
(
s["preview_repo_nodes"].as_u64(),
s["preview_crate_nodes"].as_u64(),
s["preview_fn_nodes"].as_u64()
),
(Some(2), Some(3), Some(3))
);
assert_eq!(
(
s["real_fn_nodes"].as_u64(),
s["real_call_edges"].as_u64(),
s["real_fn_crates"].as_u64(),
s["crates_without_fns"].as_u64()
),
(Some(3), Some(2), Some(1), Some(2))
);
assert_eq!(s["repo_dep_edges"], 1);
assert!(s["real_error"].is_null());
assert_eq!(s["room"]["functions"]["state"], "complete");
assert_eq!(
s["room"]["functions"]["room"],
"workspace shop-ws · shop@2 (0123456789) · 31 nodes · 60 edges"
);
// the tier is asked as a summary with a short preview per crate — never the
// whole room's fns (that answer was 37 MB for `nordisk`)
assert!(
src.asked.lock().unwrap().contains(&(
S_FUNCTIONS.to_string(),
json!({ "per_crate": FN_PREVIEW_PER_CRATE })
)),
"{:?}",
src.asked.lock().unwrap()
);
// 2 repos + 3 crates + 3 fns, and edges: 3 repo→crate + 3 crate→fn + 2 calls + 1 dependency
assert_eq!(
(s["preview_nodes"].as_u64(), s["preview_edges"].as_u64()),
(Some(8), Some(9))
);
}
/// A room that refuses: each tier draws its state and sentence, and no node.
#[test]
fn a_refusing_room_draws_its_state_and_no_node() {
let note = "code room of `shop-ws`: no repo has a consumer offset yet — nothing of this workspace was consumed";
let src = CannedRoom::new(&[
(S_SKELETON, refusal("not_consumed", note)),
(S_FUNCTIONS, refusal("not_consumed", note)),
]);
let mut t = tab(&src);
let s = t.state_json();
assert!(
s["repo_count"].is_null()
&& s["node_count"].is_null()
&& s["edge_count"].is_null()
&& s["skeleton_read"] == false,
"no count for a graph nobody read: {s}"
);
assert_eq!(s["nodes"], serde_json::json!([]));
assert_eq!(s["skeleton_error"], format!("[not_consumed] {note}"));
assert_eq!(s["room"]["skeleton"]["state"], "not_consumed");
t.update_json(r#"{"world":true}"#);
let s = t.state_json();
assert!(
s["preview_nodes"].is_null()
&& s["real_fn_nodes"].is_null()
&& s["real_call_edges"].is_null()
&& s["functions_read"] == false,
"{s}"
);
assert_eq!(s["real_error"], format!("[not_consumed] {note}"));
}
/// The mapping drops nothing and invents nothing: a call edge whose index the
/// crate's fn list does not hold is not drawn.
#[test]
fn a_call_edge_outside_the_fn_list_is_dropped_not_guessed() {
let mut body = tier();
body["repos"][0]["crates"][0]["calls"] = json!([[0, 1, 1], [0, 9, 1]]);
let a = RoomAnswer::from_body(S_FUNCTIONS, "test", body);
let tiers = functions_of(&a).unwrap();
assert_eq!(
tiers[0].1[0].edges,
vec![FnEdge {
from: "shop::checkout".into(),
to: "shop::pay::charge".into()
}]
);
assert_eq!(
skeleton_of(
&RoomAnswer::from_body(
S_SKELETON,
"test",
refusal("no_room", "no workspace selected")
),
"w"
)
.unwrap_err(),
"[no_room] no workspace selected"
);
}
}