crates/nornir-warehouse-nav/src/lib.rs
//! **nornir-warehouse-nav** — the Warehouse Navigator, dwarves' LANDING view.
//!
//! A zoomable 2D navigator over the whole nordisk **warehouse**: constellation →
//! repo → crate → sub-deps, with **click-to-drill-down**, per-repo **grouping
//! hulls**, categorical **colour-by-repo** (the validated dataviz palette, see
//! [`palette`]), and **recency = glow** (an emissive halo / brighter ring, never a
//! hue — the design law).
//!
//! ## What it reuses (L5 — never a fork)
//! - **Model + layout:** the SHARED [`nornir_depgraph`] crate — the depgraph core
//! lifted out of nornir *precisely* so "nornir AND dwarves render the same suite
//! dependency view". [`nornir_depgraph::WorkspaceGraph`] holds the repos, their
//! produced crates, the cross-repo edges, and the blast-radius queries;
//! [`nornir_depgraph::layout::DepGraphLayout`] lays repos out on a column/row grid
//! (egui-free). No egui, no warehouse, no gRPC in that leaf → a clean one-way edge.
//! - **Renderer:** [`facett_graphpan::draw_graph`] — THE shared 2D pan/zoom/click
//! graph renderer. We feed it a laid-out [`facett_graphpan::GraphModel`] +
//! per-node [`facett_graphpan::Decorations`] (recency-glow rings) and read back
//! `clicked_node` to drive the drill-down. The repo **hulls** + glow **halos** are
//! a host-side overlay painted UNDER the reused chips (we hand `draw_graph` a
//! transparent background), so nothing in the shared renderer is forked.
//!
//! ## Levels (each click descends one semantic level; ⬆ / ⌂ climb back)
//! - **L0 Constellation** — every repo as a super-node; edges = cross-repo deps.
//! - **L1 Repo** — the clicked repo's crates inside an encircling **hull**, its
//! dependency / dependent repos around it (each still its own categorical hue).
//! - **L2 Crate** — the clicked crate, its repo's sibling crates (hull), the repos
//! that DEPEND on it (dependents), and the external crates its repo consumes.
//! (L3 module / L4 source need per-file anchors nornir does not export yet —
//! deferred; see `workspace_nordisk/dwarves_agents_problems.md`.)
//!
//! ## Offline by construction
//! The panel is built from a [`nornir_depgraph::WorkspaceGraph`]; a faithful
//! nordisk [`fixture_snapshot`] drives it with no server so it compiles + tests +
//! renders the landing page immediately. A native host may instead build the graph
//! live from the workspace members (`WorkspaceGraph::build_from_members`) — the same
//! model, no code change here.
#![recursion_limit = "256"]
pub mod palette;
use std::collections::{BTreeMap, BTreeSet};
use egui::{Color32, Pos2, Rect, Shape, Stroke};
use facett_core::Facet;
#[cfg(any(test, feature = "fixtures"))]
use facett_core::Panel;
use facett_graphpan::{
draw_graph, paint_scene_opts, Decorations, GraphEdge, GraphModel, GraphNode, GraphView,
LegibilityReport, NodeDecoration, PaintOpts, PickIndex, BOX_H, BOX_W,
};
use nornir_depgraph::funcgraph::CrateFuncGraph;
use nornir_depgraph::{CrossRepoEdge, DepKind, RepoFacts, WorkspaceGraph};
/// The recency→glow-alpha curve. Recency in `0.0` (old) .. `1.0` (fresh) → a soft
/// emissive alpha in `0.0 .. 0.55`. **Recency is glow, never hue** — this is the
/// whole recency channel. Pure ⇒ unit-testable.
pub fn glow_alpha(recency: f32) -> f32 {
(recency.clamp(0.0, 1.0)).powf(1.4) * 0.55
}
/// **The function-tier SOURCE seam.** A native host injects one so the WORLD GRAPH's
/// function tier is the fns and call edges its server holds.
///
/// Kept dependency-light: the seam hands back already-grouped per-repo
/// [`CrateFuncGraph`]s (the exact shape [`WarehouseNav::set_real_functions`] wants),
/// so this leaf crate never links a gRPC client — the fetch lives in the HOST
/// (dwarves-gui). A fetch that FAILS returns its state and evidence as the `Err`
/// sentence; the world then has NO function tier and says why. Nothing is drawn in
/// its place: there is no synthetic function, no filler node, no invented edge.
/// `Send + Sync` because the fetch runs on a WORKER thread — the host's implementation
/// is a gRPC round trip per table and must never happen on the frame that is drawing.
/// The `Arc` the navigator holds is what the worker clones.
pub trait RealFnSource: Send + Sync {
/// Fetch the real function tier, grouped `(repo, per-crate graphs)`. `Err` is an
/// honest typed failure string (unreachable / auth / not-served), never a silent
/// empty that would read as "no functions".
fn fetch(&self) -> Result<Vec<(String, Vec<CrateFuncGraph>)>, String>;
/// A human description of the source (endpoint + workspace, or "fixture").
fn describe(&self) -> String;
}
/// The repo/crate SKELETON as a host's source reads it — the repos, the crates each
/// produces, and the repo → repo dependency edges.
#[derive(Debug, Clone, Default)]
pub struct Skeleton {
/// The breadcrumb root: the workspace the skeleton is of.
pub root: String,
pub facts: BTreeMap<String, RepoFacts>,
pub edges: Vec<CrossRepoEdge>,
/// Where it was read — the line the HUD prints (`skeleton: …`).
pub source: String,
}
/// **The SKELETON source seam.** A host injects one so the repo/crate tiers are what
/// its server says they are. The navigator holds no skeleton of its own: until the
/// source answers it draws nothing but "asking", and when the source cannot answer it
/// draws the source's sentence — never a stand-in graph.
///
/// `Send + Sync`: the fetch runs on a WORKER thread, never on the frame that draws.
pub trait SkeletonSource: Send + Sync {
/// Fetch the skeleton. `Err` is the state and its evidence in one sentence
/// (`[not_consumed] nothing of this workspace was consumed`).
fn fetch(&self) -> Result<Skeleton, String>;
/// Where the source asks.
fn describe(&self) -> String;
}
/// Which drill level the navigator is showing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Level {
/// L0 — all repos as super-nodes.
Constellation,
/// L1 — one repo's crates (hull) + neighbour repos.
Repo(String),
/// L2 — one crate, its siblings (hull), dependents + the repo's external deps.
Crate { repo: String, krate: String },
}
impl Level {
fn key(&self) -> &'static str {
match self {
Level::Constellation => "constellation",
Level::Repo(_) => "repo",
Level::Crate { .. } => "crate",
}
}
}
/// What a scene node stands for (drives drill-down + colour role).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum NodeKind {
Repo,
Crate,
Extern,
}
impl NodeKind {
fn as_str(self) -> &'static str {
match self {
NodeKind::Repo => "repo",
NodeKind::Crate => "crate",
NodeKind::Extern => "extern",
}
}
}
/// One laid-out scene node (world-space), before it becomes a graphpan chip.
#[derive(Clone)]
struct SceneNode {
id: String,
label: String,
kind: NodeKind,
/// The owning repo (for colour-by-repo + hull grouping). Empty for `Extern`.
repo: String,
pos: Pos2,
fill: Color32,
recency: f32,
bug: bool,
}
/// A per-repo grouping **hull** — the encircling boundary the design asks for.
#[derive(Clone)]
struct Hull {
repo: String,
member_ids: Vec<String>,
/// The convex-hull polygon in WORLD space (expanded outward from the centroid).
world_pts: Vec<Pos2>,
color: Color32,
}
/// The materialised WORLD GRAPH preview (repos→crates→functions), lowered to a
/// graphpan model, plus the function-tier accounting `state_json` reports. Built by [`WarehouseNav::build_world`], consumed
/// by both `ui()` and `state_json()` so they agree.
struct WorldBuild {
model: GraphModel,
n: usize,
edges: usize,
lane: &'static str,
repo_nodes: usize,
crate_nodes: usize,
fn_nodes: usize,
/// Function nodes drawn — each one a fn the source named (its id + name).
real_fn_nodes: usize,
/// Crates that have a function tier in this preview.
real_fn_crates: usize,
/// Crates the source named NO function of (they are drawn as a crate node only).
crates_without_fns: usize,
/// Intra-crate call edges rendered (caller→callee, resolved by id).
real_call_edges: usize,
/// Repo → repo dependency edges rendered (the skeleton's own).
repo_dep_edges: usize,
/// True when the bounded preview truncated a real crate's functions to fit.
truncated: bool,
/// Per-node tier (`0` repo, `1` crate, `2` function) — lets `ui()` map a clicked
/// `w{i}` chip to a drill action (repo→focus, function→select) the way the code room
/// maps a clicked symbol.
node_level: Vec<u8>,
/// Per-node owning repo NAME (for the repo-focus drill).
node_repo: Vec<String>,
/// Per-node REAL function name (`Some` only for warehouse-indexed function nodes) —
/// the identity the selected-function readout surfaces.
node_fn: Vec<Option<String>>,
/// **The cached 3D GALAXY layout** (`[-1,1]³` position per node) when the world is in 3D
/// mode; `None` in flat 2D mode. This is the layout the live spin PROJECTS each frame —
/// [`WarehouseNav::ui`] re-runs the cheap orthographic camera over these positions at the
/// current azimuth, so the galaxy rotates without ever rebuilding the layout (the
/// [`world_build_calls`] delta stays 0 across rotation frames). The `model` node positions
/// hold the yaw-0 baseline projection so `state_json` + 2D consumers still read a spread.
pos3: Option<Vec<[f32; 3]>>,
}
/// One crate's allocation into the bounded world preview.
struct CrateAlloc {
/// Function nodes this crate contributes.
functions: usize,
/// `(id, name)` of the fns the source named, taken in node order (empty when it
/// named none: the crate is then a crate node with no function tier).
real: Vec<(String, String)>,
}
/// The fully laid-out scene for the current level — pure data (no egui painter), so
/// both `ui()` and `state_json()` build from the SAME source.
struct Scene {
nodes: Vec<SceneNode>,
/// `(from_id, to_id, dashed)` — dashed = a `Dev`-kind (non-publish-gating) edge.
edges: Vec<(String, String, bool)>,
hulls: Vec<Hull>,
}
// ── layout constants (world units, before zoom) ───────────────────────────────
const COL_W: f32 = 250.0;
const ROW_H: f32 = 92.0;
const CLUSTER_R: f32 = 150.0; // radius of a repo's crate ring at L1/L2
const NEIGHBOUR_X: f32 = 470.0; // how far dependency/dependent repos sit from centre
const HULL_MARGIN: f32 = 46.0; // how far the hull is pushed out past its members
// ── 3D world (galaxy) constants ────────────────────────────────────────────────
/// World-space scale the normalised (`[-1,1]`) world-graph layout is blown up to before it
/// becomes graphpan chips — shared by the flat 2D spread and the 3D galaxy projection so both
/// fill the same canvas.
const WORLD_SPREAD: f32 = 1600.0;
/// The fixed camera **pitch** (tilt, radians) for the 3D world — a gentle downward look so the
/// nested repo/crate/function shells read as depth instead of a flat disc.
const WORLD_3D_PITCH: f32 = 0.42;
/// Auto-rotate speed (radians/second) ≈ 15°/s — a slow, calm showpiece spin.
const WORLD_ROTATE_RAD_PER_S: f32 = 0.26;
/// The layout result [`WarehouseNav::build_world`] unifies across its flat-2D / 3D-galaxy arms:
/// `(2D world positions, lane key, optional CACHED 3D galaxy positions the live spin projects)`.
type WorldLayout = (Vec<[f32; 2]>, &'static str, Option<Vec<[f32; 3]>>);
/// The Warehouse Navigator panel. A [`Facet`] (hence a [`Panel`](facett_core::Panel)) a host mounts as
/// its landing tab.
pub struct WarehouseNav {
graph: WorkspaceGraph,
/// Stable repo → categorical slot (index into [`palette`]). Assigned once from
/// the sorted repo set, so a repo keeps its hue across every level (identity,
/// not rank).
repo_slot: BTreeMap<String, usize>,
/// repo/crate name → recency in `0.0..1.0` (drives the glow, not the hue).
recency: BTreeMap<String, f32>,
/// repo/crate names flagged as a **known glitch** (bug / failing test) — painted
/// with the reserved alert hue + a ⛔ badge.
bugs: BTreeSet<String>,
/// Where the repo/crate SKELETON came from — honest provenance, shown in the HUD
/// header and `state_json.skeleton_source`. `from_fixture` names the compiled-in
/// snapshot; a live host that built the graph from real member checkouts
/// (`WorkspaceGraph::build_from_members`) names that instead via
/// [`Self::with_skeleton_source`]. The function tier is a SEPARATE axis (see
/// `real_functions`) — this is only the repo/crate tier.
skeleton_source: String,
/// The breadcrumb root — the workspace the skeleton is of.
root: String,
/// The host's skeleton source ([`SkeletonSource`]); `None` ⇒ the graph given to
/// [`Self::new`] is the skeleton.
skeleton_feed: Option<std::sync::Arc<dyn SkeletonSource>>,
/// Whether the feed was consulted (guards the one-shot fetch; `reread` clears it).
skeleton_scanned: bool,
#[allow(clippy::type_complexity)]
skeleton_pending: Option<(
std::sync::mpsc::Receiver<Result<Skeleton, String>>,
std::time::Instant,
)>,
/// The feed's sentence when it could not give a skeleton (state + evidence).
skeleton_error: Option<String>,
level: Level,
view: GraphView,
/// Last-seen surface mode (updated each `ui()`); `state_json` uses it so the
/// emitted hues match what is painted. Defaults to dark.
dark: bool,
/// **WORLD GRAPH** toggle. When ON the navigator leaves the repo-grid drill-down
/// and shows the levels-of-detail world graph down to **functions**, laid out by
/// the shared scale-aware engine (`facett_graphview::world`) and rendered through
/// facett's instanced GPU cloud. See [`WarehouseNav::world_target`].
world: bool,
/// The scale the WORLD GRAPH is asked to lay out (nodes). The layout lane is chosen
/// by size: ≤ [`WORLD_BH_MAX`] → Barnes–Hut O(n log n) force layout; above it → the
/// O(n) hierarchical placement that reaches 10M. The live egui panel draws only a
/// bounded [`WORLD_PREVIEW_CAP`] preview; the reported plan is the full target.
world_target: usize,
/// **The function tier**, keyed by repo → its per-crate function graphs
/// ([`nornir_depgraph::funcgraph`]) as the host's source named them (via
/// [`WarehouseNav::set_real_functions`] / a [`RealFnSource`]). A crate that has
/// functions here is drawn with them (id + name) and their intra-crate call
/// edges; a crate absent here is a crate node with no function tier.
real_functions: BTreeMap<String, Vec<CrateFuncGraph>>,
/// **Optional LIVE source for the real function tier** (see [`RealFnSource`]).
/// `None` ⇒ the offline path (fixture / an injected `with_real_functions`); `Some`
/// ⇒ a native host wired the thin warehouse fetch. It is pulled ONCE, lazily, the
/// first time the WORLD GRAPH is built — never per frame, and never on the
/// constellation landing (which draws no functions), so opening dwarves stays snappy.
real_source: Option<std::sync::Arc<dyn RealFnSource>>,
/// Whether the live source has already been consulted (guards the one-shot fetch).
/// Set the moment the fetch is STARTED — starting is consulting, and the guard must
/// hold from that instant or a second frame would launch a second round trip.
real_scanned: bool,
/// The one-shot live fetch in flight (worker receiver + when it started). The frame
/// polls it; the scripted seam blocks on it. `ensure_real_functions` used to run the
/// fetch inline from `ui()`, so the frame that entered WORLD mode froze for the
/// whole read — once, which is precisely why nobody profiled it.
#[allow(clippy::type_complexity)]
real_pending: Option<(
std::sync::mpsc::Receiver<Result<Vec<(String, Vec<CrateFuncGraph>)>, String>>,
std::time::Instant,
)>,
/// The source's sentence when it could not give a function tier (state +
/// evidence). The world then has none, and says this.
real_error: Option<String>,
/// **World-graph layout cache — the per-frame-relayout fix.** `build_world` runs a full
/// Barnes–Hut converge (80 force iterations) + a model rebuild over the bounded preview
/// (which fills up to `WORLD_PREVIEW_CAP` nodes). It used to run on EVERY egui repaint —
/// from both `ui()` and `state_json()` — so a "20-node" world (previewed as ~4 000 nodes)
/// re-converged its force layout every frame ("segt för 20 noder"). The converged,
/// positioned graph is cached here and only rebuilt when an input that actually affects it
/// changes (theme, preview budget, or the real-function data version); a repaint re-reads
/// the cache. Interior mutability so the `&self` `state_json` can keep it warm too.
world_cache: std::cell::RefCell<Option<CachedWorld>>,
/// Monotonic version of the real-function tier — bumped by `set_real_functions` (the only
/// post-construction input to `build_world` besides `world_target`/`dark`), so the layout
/// cache invalidates when the underlying data changes. Also bumped when the world DRILL
/// focus changes (see `world_focus`), since that changes which nodes the world contains.
world_data_ver: u64,
/// **WORLD-GRAPH DRILL focus.** `None` = the whole warehouse (every repo's galaxy);
/// `Some(repo)` = drilled into one repo (its crates + functions fill the view). A click
/// on a repo node sets it; ⬆/⌂ clear it — the same descend/climb the constellation
/// drill-down uses, now on the world graph.
world_focus: Option<String>,
/// The world node index of the last-CLICKED function (a `w{i}`), so the state surface
/// can report that function's identity + its callers/callees from the real call edges
/// (the funcgraph seam) — the function→callers/callees step of the drill.
world_selected: Option<usize>,
/// When set, the next world frame **fits the camera to the laid-out cloud's bounds**
/// (the honest ⊙ fit) instead of the naive zoom-1 recentre — so the spread galaxy fills
/// the viewport as separated dots rather than overflowing into an overlapping blob. Set
/// on world toggle / drill / target change; cleared once the fit is applied so the user
/// can then pan/zoom freely.
world_fit_pending: bool,
/// The pending fit is the viewer's ⊙ fit: the camera FLIES there on the shared smooth
/// path ([`facett_graphpan::fly_to_bounds`]) instead of jumping. A first show, a drill
/// or a target change still frames at once.
world_fit_fly: bool,
/// The repo/crate (non-world) graph is framed on its next frame — fitted and
/// centred in the visible canvas: its first show, ⊙ fit, ⌂ / ⬆, and a 🎯 Focus drill.
/// A 📌 Stay drill keeps the camera (a click never moves it).
level_fit_pending: bool,
/// The pending level fit FLIES (⊙ fit, a Focus drill) instead of jumping.
level_fit_fly: bool,
/// Where the last frame drew the graph (node centres on screen, the canvas, its
/// visible part, the chip half-size) — what a robot reads to check "centred, fully
/// visible" and "the clicked node stayed put".
last_shot: std::cell::RefCell<Option<Shot>>,
/// **3D WORLD toggle.** When ON the world graph is laid out as a nested-spherical GALAXY
/// (repos on a near-cubic grid, each repo's crates on a shell around it, each crate's
/// functions on a small sub-sphere — the 3D analogue of the flat `layout_world` spread)
/// and flattened to the 2D dot cloud through an orthographic CAMERA. OFF = the flat 2D
/// spread. Part of the cache key ([`CachedWorld`]) so flipping it rebuilds the layout
/// exactly once; the camera azimuth is NOT in the key (rotation is projection-only).
world_3d: bool,
/// **3D ROTATING checkbox.** When ON (and [`Self::world_3d`]) the camera azimuth advances
/// over time so the galaxy spins slowly around its vertical axis — a live showpiece. Only
/// the PROJECTION angle changes per frame; the `galaxy_3d` LAYOUT stays cached (0 rebuilds
/// across rotation frames — the frame-budget guard proves it).
world_rotate: bool,
/// The live camera **azimuth** (yaw, radians) about the galaxy's vertical axis. Advanced
/// each frame while [`Self::world_rotate`] is on and fed into the per-frame projection —
/// never into the layout. Reported by `state_json` as `rotate_angle` so a robot can assert
/// it changed while `world_build_calls()` stayed flat.
world_yaw: f32,
/// **UNIVERSE mode toggle** — the whole code universe as a pure GPU star-field, with **NO
/// LOD/budget cap**. When ON (and [`Self::world`]) the world graph drops the
/// [`WORLD_PREVIEW_CAP`] preview limit and lays out the **FULL** node count the
/// `world_target` asks for (12k → 100k → 1M), rendered through the **shared instanced GPU
/// cloud** (`facett_graphpan::paint_scene_opts` with `gpu_cloud` — the SAME renderer
/// `facett_graphview::world_still` rasterises headless at 100k/1M) as coloured points with
/// **labels/text OFF**. The bounded "world" mode (capped, CPU dot-cloud) is untouched —
/// Universe is purely additive. Part of the cache key ([`CachedWorld`]) so flipping it
/// rebuilds the (O(n), cached) galaxy layout exactly once; a repaint re-reads the cache
/// (`world_build_calls()` delta stays 0 — the frame-budget guard proves it).
universe: bool,
/// What the last Universe frame's GPU-cloud lane did — `gpu_cloud == true` iff the instanced
/// renderer painted the star-field this process (a host installed it). Reported by
/// `state_json` as `universe_gpu_cloud` so a robot asserts the lane without reading pixels;
/// `false` is the honest CPU fallback (no renderer / no `gpu` feature). Interior mutability
/// so the `&self` view path can record it.
universe_legibility: std::cell::RefCell<LegibilityReport>,
/// Screen-space pick index for the Universe GPU canvas (its click-to-drill goes through the
/// spatial index, not an O(n) scan — the same discipline `facett_graphpan` uses).
universe_pick: std::cell::RefCell<PickIndex>,
}
/// A converged, positioned world-graph preview cached across egui repaints, keyed by the inputs
/// that actually change its result. See [`WarehouseNav::world_cache`].
struct CachedWorld {
dark: bool,
budget: usize,
ver: u64,
/// Whether this build is the 3D galaxy layout (vs the flat 2D spread) — part of the cache
/// key so flipping the 3D toggle rebuilds once. The camera AZIMUTH is deliberately NOT a
/// key: rotation re-projects the cached `pos3`, it never rebuilds.
world_3d: bool,
/// Whether this build is the uncapped UNIVERSE layout (full node count, O(n) galaxy
/// placement) vs the bounded force/hierarchical preview — part of the cache key so
/// flipping the Universe toggle rebuilds exactly once, never per repaint.
universe: bool,
build: WorldBuild,
}
/// Process-wide count of full world builds (Barnes–Hut converge + model rebuild) — instrumentation
/// for the per-frame-relayout regression guard: the layout must run O(1) times per data/zoom change,
/// NOT once per repaint. A robot test snapshots this, renders many frames, and asserts the delta.
static WORLD_BUILD_CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Total number of times the expensive world build ran process-wide (see [`WORLD_BUILD_CALLS`]).
/// Public so the black-box robot suite (a separate test crate) can assert the layout is cached.
#[must_use]
pub fn world_build_calls() -> u64 {
WORLD_BUILD_CALLS.load(std::sync::atomic::Ordering::Relaxed)
}
/// The force-layout budget crossover (mirrors [`facett_graphview::LayoutBudget::default`]):
/// at/below this the WORLD GRAPH runs a real Barnes–Hut force converge; above it the O(n)
/// hierarchical placement lane (the 10M path).
pub const WORLD_BH_MAX: usize = 1_000_000;
/// How many world nodes the LIVE egui panel actually turns into chips. The full
/// `world_target` (up to 10M) is laid out by the shared engine + rendered on the GPU
/// cloud; the landing panel previews a bounded slice so the CPU chip renderer stays snappy.
pub const WORLD_PREVIEW_CAP: usize = 4_000;
/// **UNIVERSE mode default node count.** Flipping the Universe toggle with the sentinel
/// 10M `world_target` still in place would try to materialise 10M `GraphNode`s at once; the
/// toggle instead seeds this interactive default (the owner's beloved ~100k star-field). The
/// user is free to raise `world_target` afterwards (100k → 1M) — Universe applies NO cap of
/// its own; this is only the initial target, not a ceiling.
pub const UNIVERSE_DEFAULT_TARGET: usize = 100_000;
impl WarehouseNav {
/// Build the navigator over an assembled [`WorkspaceGraph`], with a recency map
/// (repo/crate → 0..1) and a set of glitch anchors. Repo hues are assigned from
/// the sorted repo set so they are stable + deterministic.
pub fn new(
graph: WorkspaceGraph,
recency: BTreeMap<String, f32>,
bugs: BTreeSet<String>,
) -> Self {
let mut names: Vec<String> = graph.facts.keys().cloned().collect();
// Include any repo that only appears as an edge endpoint (defensive).
for e in &graph.edges {
if !names.contains(&e.from) {
names.push(e.from.clone());
}
if !names.contains(&e.to) {
names.push(e.to.clone());
}
}
names.sort();
names.dedup();
let repo_slot = names.into_iter().enumerate().map(|(i, n)| (n, i)).collect();
Self {
graph,
repo_slot,
recency,
bugs,
skeleton_source: "host-assembled".to_string(),
root: "workspace".to_string(),
skeleton_feed: None,
skeleton_scanned: false,
skeleton_pending: None,
skeleton_error: None,
level: Level::Constellation,
view: GraphView::default(),
dark: true,
world: false,
world_target: 10_000_000,
real_functions: BTreeMap::new(),
real_source: None,
real_scanned: false,
real_pending: None,
real_error: None,
world_cache: std::cell::RefCell::new(None),
world_data_ver: 0,
world_focus: None,
world_selected: None,
world_fit_pending: false,
world_fit_fly: false,
level_fit_pending: true,
level_fit_fly: false,
last_shot: std::cell::RefCell::new(None),
world_3d: false,
world_rotate: false,
world_yaw: 0.0,
universe: false,
universe_legibility: std::cell::RefCell::new(LegibilityReport::default()),
universe_pick: std::cell::RefCell::new(PickIndex::default()),
}
}
/// Install the function tier for `repo` — its per-crate function graphs. The WORLD
/// GRAPH then draws those crates' functions (id + name + call edges). A crate with
/// no entry here is a crate node alone.
pub fn set_real_functions(&mut self, repo: &str, graphs: Vec<CrateFuncGraph>) {
self.real_functions.insert(repo.to_string(), graphs);
self.world_data_ver = self.world_data_ver.wrapping_add(1);
}
/// Builder form of [`set_real_functions`](Self::set_real_functions).
#[must_use]
pub fn with_real_functions(mut self, repo: &str, graphs: Vec<CrateFuncGraph>) -> Self {
self.set_real_functions(repo, graphs);
self
}
/// Inject a LIVE [`RealFnSource`] — the WORLD GRAPH then pulls its function tier
/// from the warehouse (once, lazily, on first world build). Builder form, so a host
/// writes `WarehouseNav::from_fixture().with_real_source(src)`: the fixture supplies
/// the repo/crate skeleton, the source supplies the REAL functions on top of it.
#[must_use]
pub fn with_real_source(mut self, source: Box<dyn RealFnSource>) -> Self {
// Taken as a `Box` (no caller's spelling changed) and held as an `Arc`, which is
// what the fetch worker clones.
self.real_source = Some(source.into());
self
}
/// Pull the real function tier from the live source EXACTLY ONCE (guarded). A no-op
/// when there is no source (the offline path) or it was already consulted. On
/// success each repo's graphs are installed (bumping the layout-cache version so the
/// world rebuilds with them); on failure the source's sentence is recorded and
/// the world has no function tier — never a fabricated symbol from an outage.
///
/// **Off the egui thread.** MEASURED before this was (2026-08-25, this box, a mock
/// `Warehouse` holding each of the two scans 400 ms on the far side of the real
/// tonic transport): the frame that entered WORLD mode held for **1.049 s**. The
/// once-guard is what hid it — a single freeze on pressing `🌐 world` reads as "the
/// app is slow to start", not as a stall with a cause.
fn ensure_real_functions(&mut self) {
if self.real_scanned {
return;
}
let Some(source) = self.real_source.as_ref().map(std::sync::Arc::clone) else {
return; // the offline path — no source, nothing to consult
};
// Guard from the instant the fetch STARTS, or the next frame launches a second.
self.real_scanned = true;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::Builder::new()
.name("warehouse-nav-fn-tier".into())
.spawn(move || {
let _ = tx.send(source.fetch());
})
.expect("spawn warehouse-nav function-tier worker");
self.real_pending = Some((rx, std::time::Instant::now()));
}
/// Non-blocking: land the in-flight fetch if the worker finished. Called once/frame.
fn poll_real_functions(&mut self) {
let Some((rx, _)) = &self.real_pending else {
return;
};
match rx.try_recv() {
Ok(res) => {
self.real_pending = None;
self.land_real_functions(res);
}
Err(std::sync::mpsc::TryRecvError::Empty) => {}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.real_pending = None;
// A dead worker is a NAMED outage. Silence here would read as "the
// source named no functions", which is a different claim.
self.real_error = Some(
"[read_failed] the function-tier worker died before it answered".to_string(),
);
}
}
}
/// Blocking: wait for the in-flight fetch. The headless seam (`update_json`
/// `{"world":true}` / `{"universe":true}`) uses this, so a scripted caller that
/// enters world mode and then reads `state_json` sees the REAL tier — the guarantee
/// that path has always given. The window never blocks on it.
fn wait_real_functions(&mut self) {
let Some((rx, _)) = self.real_pending.take() else {
return;
};
let res = rx.recv().unwrap_or_else(|_| {
Err("[read_failed] the function-tier worker died before it answered".to_string())
});
self.land_real_functions(res);
}
/// Whether the one-shot live fetch is in flight.
pub fn real_fetch_pending(&self) -> bool {
self.real_pending.is_some()
}
/// Fold a landed fetch into the tier. On failure the tier is EMPTY and the
/// source's sentence is kept — never a fabricated symbol from an outage.
fn land_real_functions(&mut self, res: Result<Vec<(String, Vec<CrateFuncGraph>)>, String>) {
match res {
Ok(per_repo) => {
let mut any = false;
for (repo, graphs) in per_repo {
if !graphs.is_empty() {
any = true;
}
self.real_functions.insert(repo, graphs);
}
if any {
self.world_data_ver = self.world_data_ver.wrapping_add(1);
}
self.real_error = None;
}
Err(e) => {
// what an earlier answer gave is no longer what the source says
if !self.real_functions.is_empty() {
self.real_functions.clear();
self.world_data_ver = self.world_data_ver.wrapping_add(1);
}
self.real_error = Some(e);
}
}
}
/// **A navigator with NO skeleton of its own**, for the workspace `root`: it draws
/// what its [`SkeletonSource`] answers ([`Self::with_skeleton_feed`]) and, until
/// then or when the source cannot answer, the state — never a stand-in graph.
pub fn empty(root: impl Into<String>) -> Self {
let mut me = Self::new(
WorkspaceGraph::from_query_parts(BTreeMap::new(), Vec::new()),
BTreeMap::new(),
BTreeSet::new(),
);
me.root = root.into();
me.skeleton_source = "not read yet".to_string();
me
}
/// Inject the host's [`SkeletonSource`]. It is consulted once, off the UI thread,
/// the first time the pane is drawn (or driven), and again on `{"reread":true}`.
#[must_use]
pub fn with_skeleton_feed(mut self, source: Box<dyn SkeletonSource>) -> Self {
self.skeleton_feed = Some(source.into());
self
}
fn ensure_skeleton(&mut self) {
if self.skeleton_scanned {
return;
}
let Some(source) = self.skeleton_feed.as_ref().map(std::sync::Arc::clone) else {
return;
};
self.skeleton_scanned = true;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::Builder::new()
.name("warehouse-nav-skeleton".into())
.spawn(move || {
let _ = tx.send(source.fetch());
})
.expect("spawn warehouse-nav skeleton worker");
self.skeleton_pending = Some((rx, std::time::Instant::now()));
}
fn poll_skeleton(&mut self) {
let Some((rx, _)) = &self.skeleton_pending else {
return;
};
match rx.try_recv() {
Ok(res) => {
self.skeleton_pending = None;
self.land_skeleton(res);
}
Err(std::sync::mpsc::TryRecvError::Empty) => {}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.skeleton_pending = None;
self.land_skeleton(Err(
"[read_failed] the skeleton worker died before it answered".to_string(),
));
}
}
}
fn wait_skeleton(&mut self) {
let Some((rx, _)) = self.skeleton_pending.take() else {
return;
};
let res = rx.recv().unwrap_or_else(|_| {
Err("[read_failed] the skeleton worker died before it answered".to_string())
});
self.land_skeleton(res);
}
/// Whether the skeleton fetch is in flight.
pub fn skeleton_fetch_pending(&self) -> bool {
self.skeleton_pending.is_some()
}
/// Whether the graph on screen is an ANSWER: a skeleton the source gave (or one the
/// host built the navigator with). `false` while a fed navigator has not asked yet,
/// is still asking, or was refused — then there is no graph and no count.
pub fn skeleton_read(&self) -> bool {
self.skeleton_feed.is_none()
|| (self.skeleton_scanned
&& self.skeleton_pending.is_none()
&& self.skeleton_error.is_none())
}
/// Fold a landed skeleton in. A source that could not answer leaves NO graph: what
/// was drawn from an earlier answer is dropped, because it is no longer what the
/// source says.
fn land_skeleton(&mut self, res: Result<Skeleton, String>) {
let (graph, source, error) = match res {
Ok(sk) => {
self.root = sk.root;
(
WorkspaceGraph::from_query_parts(sk.facts, sk.edges),
sk.source,
None,
)
}
Err(e) => (
WorkspaceGraph::from_query_parts(BTreeMap::new(), Vec::new()),
"no skeleton".to_string(),
Some(e),
),
};
let mut names: Vec<String> = graph.facts.keys().cloned().collect();
for e in &graph.edges {
names.push(e.from.clone());
names.push(e.to.clone());
}
names.sort();
names.dedup();
self.repo_slot = names.into_iter().enumerate().map(|(i, n)| (n, i)).collect();
self.graph = graph;
self.skeleton_source = source;
self.skeleton_error = error;
self.level = Level::Constellation;
self.world_focus = None;
self.world_selected = None;
self.world_data_ver = self.world_data_ver.wrapping_add(1);
self.view = GraphView::default();
self.level_fit_pending = true;
self.level_fit_fly = false;
}
/// The real function graph for one crate in `repo`, if the warehouse indexed it.
fn real_crate(&self, repo: &str, krate: &str) -> Option<&CrateFuncGraph> {
self.real_functions
.get(repo)
.and_then(|gs| gs.iter().find(|g| g.crate_name == krate && !g.is_empty()))
}
/// The navigator over the tests' [`fixture_snapshot`] (feature `fixtures`; no
/// shipped build enables it).
#[cfg(any(test, feature = "fixtures"))]
pub fn from_fixture() -> Self {
let (facts, edges, recency, bugs) = fixture_snapshot();
let graph = WorkspaceGraph::from_query_parts(facts, edges);
let mut me = Self::new(graph, recency, bugs)
.with_skeleton_source("fixture (compiled-in nordisk snapshot)");
me.root = "nordisk".to_string();
me
}
/// The fixture navigator with a GENERATED function tier of `total` fns spread
/// evenly over the fixture's crates, each crate's fns chained by call edges —
/// what the scale / layout / frame-budget tests stand a world on (feature
/// `fixtures`). The names say what they are (`fixture_fn_<i>`).
#[cfg(any(test, feature = "fixtures"))]
pub fn from_fixture_with_functions(total: usize) -> Self {
use nornir_depgraph::funcgraph::{FnEdge, FnNode};
let mut me = Self::from_fixture();
let crates: Vec<(String, String)> = me
.graph
.facts
.iter()
.flat_map(|(r, f)| f.produces.iter().map(move |c| (r.clone(), c.clone())))
.collect();
let per = total.div_ceil(crates.len().max(1));
let mut by_repo: BTreeMap<String, Vec<CrateFuncGraph>> = BTreeMap::new();
let mut made = 0usize;
for (repo, krate) in crates {
let n = per.min(total - made);
made += n;
let id = |i: usize| format!("{repo}::{krate}::fixture_fn_{i}");
by_repo
.entry(repo.clone())
.or_default()
.push(CrateFuncGraph {
crate_name: krate.clone(),
nodes: (0..n)
.map(|i| FnNode {
id: id(i),
name: format!("fixture_fn_{i}"),
crate_name: krate.clone(),
})
.collect(),
edges: (1..n)
.map(|i| FnEdge {
from: id(i - 1),
to: id(i),
})
.collect(),
});
}
for (repo, graphs) in by_repo {
me.set_real_functions(&repo, graphs);
}
me
}
/// Name the SKELETON's provenance — surfaced in the HUD header and
/// `state_json.skeleton_source`. [`Self::from_fixture`] names the compiled-in
/// snapshot; a live host that built the [`WorkspaceGraph`] from real member
/// checkouts (`build_from_members`) names the suite root + member count here, so
/// a viewer (or robot) can tell a static skeleton from a live one without guessing.
#[must_use]
pub fn with_skeleton_source(mut self, source: impl Into<String>) -> Self {
self.skeleton_source = source.into();
self
}
fn slot(&self, repo: &str) -> usize {
self.repo_slot.get(repo).copied().unwrap_or(0)
}
fn repo_names(&self) -> Vec<String> {
let mut v: Vec<String> = self.repo_slot.keys().cloned().collect();
v.sort();
v
}
fn recency_of(&self, name: &str) -> f32 {
self.recency.get(name).copied().unwrap_or(0.0)
}
// ── drill-down state machine ───────────────────────────────────────────────
/// Descend one level toward the clicked node id (`repo:<name>` / `crate:<name>`;
/// `extern:<name>` is a leaf → no-op). Neutral to which level we're on, so the
/// `{"drill":…}` robot seam and a real chip click share one path.
pub fn drill(&mut self, id: &str) {
if let Some(repo) = id.strip_prefix("repo:") {
// Descending into a repo (from anywhere) → its L1 view.
if self.repo_slot.contains_key(repo) {
self.set_level(Level::Repo(repo.to_string()));
}
} else if let Some(krate) = id.strip_prefix("crate:") {
// A crate chip only exists inside a repo view → descend to its L2.
let repo = match &self.level {
Level::Repo(r) => Some(r.clone()),
Level::Crate { repo, .. } => Some(repo.clone()),
Level::Constellation => self.owning_repo(krate),
};
if let Some(repo) = repo {
self.set_level(Level::Crate {
repo,
krate: krate.to_string(),
});
}
}
// `extern:*` and unknown ids: intentionally inert.
}
/// TEST / host accessor: the current WORLD preview model (the laid-out, COLOURED dot
/// cloud the panel renders) — a clone of the cached build's model. Refreshes the cache
/// so it reflects the active `world_target`/focus/theme.
#[must_use]
pub fn world_preview_model(&self) -> GraphModel {
let wb = self.world_build_cached(self.dark, self.world_budget());
wb.model.clone()
}
/// **Where the graph is on screen** after the last frame: `(box, seen)` — the box of
/// every node (chip or dot) and the visible part of the canvas.
#[must_use]
pub fn screen_box(&self) -> Option<(Rect, Rect)> {
let shot = self.last_shot.borrow();
let shot = shot.as_ref()?;
let mut b: Option<Rect> = None;
for (_, c) in &shot.centres {
let r = Rect::from_center_size(*c, shot.half * 2.0);
b = Some(b.map_or(r, |b| b.union(r)));
}
Some((b?, shot.seen))
}
/// Where node `id` was drawn on the last frame.
#[must_use]
pub fn node_on_screen(&self, id: &str) -> Option<Pos2> {
self.last_shot.borrow().as_ref()?.centres.iter().find(|(i, _)| i == id).map(|(_, p)| *p)
}
/// TEST / host accessor: a snapshot of the pan/zoom/selection transform (after any
/// fit) — so a screenshot test can project the model with the exact camera the panel used.
#[must_use]
pub fn world_view_snapshot(&self) -> GraphView {
self.view.clone()
}
/// Climb one level (⬆): in WORLD mode, a selected function clears first, then a repo
/// focus (back to the whole warehouse); otherwise Crate → its Repo → Constellation.
pub fn up(&mut self) {
if self.world {
if self.world_selected.take().is_some() {
return;
}
if self.world_focus.take().is_some() {
self.world_data_ver = self.world_data_ver.wrapping_add(1);
self.world_fit_pending = true;
return;
}
}
let next = match &self.level {
Level::Crate { repo, .. } => Level::Repo(repo.clone()),
Level::Repo(_) => Level::Constellation,
Level::Constellation => Level::Constellation,
};
self.set_level(next);
self.frame_level();
}
/// Jump to the L0 constellation (⌂ home). Also clears any WORLD drill focus/selection.
pub fn home(&mut self) {
self.clear_world_drill();
self.set_level(Level::Constellation);
self.frame_level();
}
/// Clear the WORLD drill focus + function selection (invalidating the layout cache when
/// the focus actually changes, since it changes which nodes the world contains).
fn clear_world_drill(&mut self) {
if self.world_focus.take().is_some() {
self.world_data_ver = self.world_data_ver.wrapping_add(1);
self.world_fit_pending = true;
}
self.world_selected = None;
}
/// **WORLD drill on a clicked `w{i}` chip.** A repo node → focus that repo (expand its
/// crates + functions); a function node → select it (surface its callers/callees from
/// the real call edges). A crate node with no active focus → focus its repo. Mirrors the
/// code room's descend-on-click. Returns whether anything changed (for the caller's fit).
fn world_drill(&mut self, node_index: usize, level: u8, repo: &str) -> bool {
match level {
0 => {
// repo → focus (only descends when not already focused there)
if self.world_focus.as_deref() != Some(repo) {
self.world_focus = Some(repo.to_string());
self.world_selected = None;
self.world_data_ver = self.world_data_ver.wrapping_add(1);
self.world_fit_pending = true;
return true;
}
false
}
1 => {
// crate → focus its repo so its functions fill the view
if self.world_focus.as_deref() != Some(repo) {
self.world_focus = Some(repo.to_string());
self.world_selected = None;
self.world_data_ver = self.world_data_ver.wrapping_add(1);
self.world_fit_pending = true;
return true;
}
false
}
_ => {
// function → select (callers/callees surfaced in state_json)
let changed = self.world_selected != Some(node_index);
self.world_selected = Some(node_index);
changed
}
}
}
fn set_level(&mut self, level: Level) {
if self.level != level {
self.level = level;
// A fresh level clears the selection; the camera is the caller's call: ⌂ / ⬆
// and a 🎯 Focus drill fly to frame it, a 📌 Stay drill keeps it (it used to
// reset to zoom 1 with world (0, 0) at the canvas centre — the "click moves
// it to the right" the owner saw).
self.view.selected = None;
}
}
/// Frame the current level on the next frame, flying there.
fn frame_level(&mut self) {
self.level_fit_pending = true;
self.level_fit_fly = true;
}
fn owning_repo(&self, krate: &str) -> Option<String> {
self.graph
.facts
.iter()
.find(|(_, f)| f.produces.contains(krate))
.map(|(name, _)| name.clone())
}
fn crumbs(&self) -> Vec<String> {
match &self.level {
Level::Constellation => vec![self.root.clone()],
Level::Repo(r) => vec![self.root.clone(), r.clone()],
Level::Crate { repo, krate } => {
vec![self.root.clone(), repo.clone(), krate.clone()]
}
}
}
// ── scene construction (pure) ──────────────────────────────────────────────
fn build_scene(&self, dark: bool) -> Scene {
match &self.level {
Level::Constellation => self.scene_constellation(dark),
Level::Repo(r) => self.scene_repo(r, dark),
Level::Crate { repo, krate } => self.scene_crate(repo, krate, dark),
}
}
fn scene_constellation(&self, dark: bool) -> Scene {
let repos = self.repo_names();
let layout = nornir_depgraph::layout::DepGraphLayout::build(
&repos,
&self.graph.edges,
false,
&BTreeSet::new(),
);
// Column/row grid → world positions.
let mut raw: Vec<(String, Pos2)> = layout
.nodes
.iter()
.map(|n| {
(
n.repo.clone(),
Pos2::new(n.col as f32 * COL_W, n.row as f32 * ROW_H),
)
})
.collect();
// Any repo the layout dropped (shouldn't happen) gets appended in a spare row.
for (i, r) in repos.iter().enumerate() {
if !raw.iter().any(|(n, _)| n == r) {
raw.push((r.clone(), Pos2::new(i as f32 * COL_W, -ROW_H)));
}
}
center(&mut raw);
let nodes: Vec<SceneNode> = raw
.into_iter()
.map(|(repo, pos)| {
let slot = self.slot(&repo);
SceneNode {
id: format!("repo:{repo}"),
label: repo.clone(),
kind: NodeKind::Repo,
fill: palette::repo_color(slot, dark),
recency: self.recency_of(&repo),
bug: self.bugs.contains(&repo),
repo,
pos,
}
})
.collect();
let edges = self
.graph
.edges
.iter()
.filter(|e| self.repo_slot.contains_key(&e.from) && self.repo_slot.contains_key(&e.to))
.map(|e| {
(
format!("repo:{}", e.from),
format!("repo:{}", e.to),
e.kind == DepKind::Dev,
)
})
.collect();
// No hulls at L0 — repos ARE the atoms; hulls appear when a repo opens.
Scene {
nodes,
edges,
hulls: Vec::new(),
}
}
fn scene_repo(&self, repo: &str, dark: bool) -> Scene {
let mut nodes: Vec<SceneNode> = Vec::new();
let mut edges: Vec<(String, String, bool)> = Vec::new();
let slot = self.slot(repo);
// The focused repo anchor at centre.
let anchor_id = format!("repo:{repo}");
nodes.push(SceneNode {
id: anchor_id.clone(),
label: format!("◆ {repo}"),
kind: NodeKind::Repo,
repo: repo.to_string(),
pos: Pos2::ZERO,
fill: palette::repo_color(slot, dark),
recency: self.recency_of(repo),
bug: self.bugs.contains(repo),
});
// The repo's crates, ringed around the anchor — grouped by a hull.
let crates: Vec<String> = self
.graph
.facts
.get(repo)
.map(|f| f.produces.iter().cloned().collect())
.unwrap_or_default();
let mut member_ids = vec![anchor_id.clone()];
let mut hull_pts = vec![Pos2::ZERO];
for (i, c) in crates.iter().enumerate() {
let pos = ring_pos(i, crates.len(), CLUSTER_R);
let id = format!("crate:{c}");
member_ids.push(id.clone());
hull_pts.push(pos);
nodes.push(SceneNode {
id,
label: c.clone(),
kind: NodeKind::Crate,
repo: repo.to_string(),
pos,
fill: palette::crate_color(slot, i, dark),
recency: self.recency_of(c),
bug: self.bugs.contains(c),
});
}
// Neighbour repos: dependencies (repo → dep) to the right, dependents (dep →
// repo) to the left. Each keeps its own categorical hue.
let deps: Vec<&CrossRepoEdge> = self.graph.dependencies_of(repo);
let dependents: Vec<&CrossRepoEdge> = self.graph.dependents_of(repo);
for (i, e) in deps.iter().enumerate() {
let pos = column_pos(NEIGHBOUR_X, i, deps.len());
let id = format!("repo:{}", e.to);
if !nodes.iter().any(|n| n.id == id) {
nodes.push(self.neighbour_node(&e.to, pos, dark));
}
edges.push((anchor_id.clone(), id, e.kind == DepKind::Dev));
}
for (i, e) in dependents.iter().enumerate() {
let pos = column_pos(-NEIGHBOUR_X, i, dependents.len());
let id = format!("repo:{}", e.from);
if !nodes.iter().any(|n| n.id == id) {
nodes.push(self.neighbour_node(&e.from, pos, dark));
}
edges.push((id, anchor_id.clone(), e.kind == DepKind::Dev));
}
let hull = self.make_hull(repo, member_ids, hull_pts, dark);
Scene {
nodes,
edges,
hulls: vec![hull],
}
}
fn scene_crate(&self, repo: &str, krate: &str, dark: bool) -> Scene {
let mut nodes: Vec<SceneNode> = Vec::new();
let mut edges: Vec<(String, String, bool)> = Vec::new();
let slot = self.slot(repo);
let focus_id = format!("crate:{krate}");
// The focus crate at centre.
nodes.push(SceneNode {
id: focus_id.clone(),
label: format!("◆ {krate}"),
kind: NodeKind::Crate,
repo: repo.to_string(),
pos: Pos2::ZERO,
fill: palette::crate_color(slot, 0, dark),
recency: self.recency_of(krate),
bug: self.bugs.contains(krate),
});
// Sibling crates of the same repo, ringed + hulled with the focus crate.
let siblings: Vec<String> = self
.graph
.facts
.get(repo)
.map(|f| {
f.produces
.iter()
.filter(|c| c.as_str() != krate)
.cloned()
.collect()
})
.unwrap_or_default();
let mut member_ids = vec![focus_id.clone()];
let mut hull_pts = vec![Pos2::ZERO];
for (i, c) in siblings.iter().enumerate() {
let pos = ring_pos(i, siblings.len().max(1), CLUSTER_R);
let id = format!("crate:{c}");
member_ids.push(id.clone());
hull_pts.push(pos);
nodes.push(SceneNode {
id,
label: c.clone(),
kind: NodeKind::Crate,
repo: repo.to_string(),
pos,
fill: palette::crate_color(slot, i + 1, dark),
recency: self.recency_of(c),
bug: self.bugs.contains(c),
});
}
// Dependent repos (who consumes THIS crate) to the left, edge dependent→crate.
let users = self.graph.external_dep_users(krate);
for (i, u) in users.iter().enumerate() {
let pos = column_pos(-NEIGHBOUR_X, i, users.len());
let id = format!("repo:{u}");
if !nodes.iter().any(|n| n.id == id) {
nodes.push(self.neighbour_node(u, pos, dark));
}
edges.push((id, focus_id.clone(), false));
}
// The repo's external crate deps (its sub-deps, repo-granular) to the right.
// Crate-exact sub-deps need per-file manifest anchors nornir does not export
// yet — deferred; these repo-level externals are the honest available slice.
let externs: Vec<String> = self.graph.external_deps(repo).into_iter().take(8).collect();
let muted = if dark {
Color32::from_gray(90)
} else {
Color32::from_gray(175)
};
for (i, x) in externs.iter().enumerate() {
let pos = column_pos(NEIGHBOUR_X, i, externs.len());
let id = format!("extern:{x}");
nodes.push(SceneNode {
id: id.clone(),
label: x.clone(),
kind: NodeKind::Extern,
repo: String::new(),
pos,
fill: muted,
recency: 0.0,
bug: false,
});
edges.push((focus_id.clone(), id, false));
}
let hull = self.make_hull(repo, member_ids, hull_pts, dark);
Scene {
nodes,
edges,
hulls: vec![hull],
}
}
fn neighbour_node(&self, repo: &str, pos: Pos2, dark: bool) -> SceneNode {
let slot = self.slot(repo);
SceneNode {
id: format!("repo:{repo}"),
label: repo.to_string(),
kind: NodeKind::Repo,
repo: repo.to_string(),
pos,
fill: palette::repo_color(slot, dark),
recency: self.recency_of(repo),
bug: self.bugs.contains(repo),
}
}
fn make_hull(&self, repo: &str, member_ids: Vec<String>, pts: Vec<Pos2>, dark: bool) -> Hull {
Hull {
color: palette::repo_color(self.slot(repo), dark),
world_pts: expanded_hull(&pts, HULL_MARGIN),
member_ids,
repo: repo.to_string(),
}
}
// ── graphpan model + decorations from a scene ─────────────────────────────
fn to_graph_model(&self, scene: &Scene, dark: bool) -> (GraphModel, Decorations) {
let edge_col = if dark {
Color32::from_gray(95)
} else {
Color32::from_gray(165)
};
let text = if dark {
Color32::from_gray(230)
} else {
Color32::from_gray(30)
};
let nodes = scene
.nodes
.iter()
.map(|n| {
let fill = if n.bug { palette::BUG_ALERT } else { n.fill };
GraphNode {
id: n.id.clone(),
label: n.label.clone(),
fill,
stroke: text.gamma_multiply(0.5),
pos: n.pos,
}
})
.collect();
let edges = scene
.edges
.iter()
.map(|(from, to, dashed)| GraphEdge {
from: from.clone(),
to: to.clone(),
color: edge_col,
dashed: *dashed,
label: None,
})
.collect();
// Decorations: a recency-glow RING (brighter for fresher) + a ⛔ badge on a
// known glitch. The filled halo is painted in the host overlay; the ring is
// the shared renderer's own channel (reuse), so recency shows even when a
// node is off the lit path.
let mut decos = Decorations::default();
for n in &scene.nodes {
let mut d = NodeDecoration::default();
if n.bug {
d.badge = Some("⛔".into());
d.badge_color = Some(palette::BUG_ALERT);
d.ring = Some(palette::BUG_ALERT);
} else if n.recency > 0.05 {
// Brighten the repo hue toward white by recency for the ring.
let a = 0.35 + 0.65 * n.recency;
d.ring = Some(lighten(n.fill, 0.30 * n.recency).gamma_multiply(a));
}
if d.ring.is_some() || d.badge.is_some() {
decos.nodes.insert(n.id.clone(), d);
}
}
(GraphModel { nodes, edges }, decos)
}
/// Paint the host overlay (bg wash + repo hulls + recency glow halos) UNDER the
/// reused chips. Uses the exact projection `draw_graph` will use for the SAME
/// rect, so hull/halo and chip line up.
fn paint_overlay(&self, ui: &egui::Ui, rect: Rect, scene: &Scene, dark: bool) {
let painter = ui.painter_at(rect);
let bg = if dark {
Color32::from_gray(18)
} else {
Color32::from_gray(250)
};
painter.rect_filled(rect, 6.0, bg);
let origin = rect.center() + self.view.pan;
let project = |p: Pos2| self.view.project(origin, p);
let zoom = self.view.zoom;
// Repo hulls first (deepest layer): a soft fill + a stronger outline.
for hull in &scene.hulls {
let pts: Vec<Pos2> = hull.world_pts.iter().map(|p| project(*p)).collect();
if pts.len() >= 3 {
let fill = hull.color.gamma_multiply(if dark { 0.10 } else { 0.08 });
let stroke = Stroke::new(2.0_f32, hull.color.gamma_multiply(0.85));
painter.add(Shape::convex_polygon(pts.clone(), fill, stroke));
}
}
// Recency glow halos over the hull fill, under the chips.
for n in &scene.nodes {
let a = glow_alpha(n.recency);
if a <= 0.01 {
continue;
}
let c = project(n.pos);
let base = (BOX_W.max(BOX_H)) * 0.75 * zoom;
// Two concentric soft discs = a cheap bloom.
let col = lighten(n.fill, 0.35);
painter.circle_filled(c, base * 1.15, col.gamma_multiply(a * 0.45));
painter.circle_filled(c, base * 0.72, col.gamma_multiply(a));
}
}
// ── WORLD GRAPH (LOD repos→crates→functions) ───────────────────────────────
/// The node budget the LIVE panel builds this frame. Bounded "world" mode clamps to
/// [`WORLD_PREVIEW_CAP`] (the CPU dot-cloud stays snappy); **UNIVERSE mode removes the cap
/// entirely** and builds the full `world_target` (the GPU instanced cloud renders it). The
/// single source of truth so `ui()` and `state_json()` request the SAME build and never
/// thrash the layout cache.
fn world_budget(&self) -> usize {
if self.universe {
self.world_target
} else {
self.world_target.min(WORLD_PREVIEW_CAP)
}
}
/// The layout lane the shared engine WOULD run at the full `world_target` — cheap
/// (no allocation): Barnes–Hut below the force budget, hierarchical placement above.
fn planned_lane(&self) -> &'static str {
if self.world_target <= WORLD_BH_MAX {
"barnes_hut"
} else {
"hier_grid"
}
}
/// Build the BOUNDED WORLD GRAPH preview (≤ [`WORLD_PREVIEW_CAP`] nodes): the
/// skeleton's repos and crates, and under each crate the fns its source named
/// ([`Self::real_functions`]: id + name + intra-crate call edges). A crate the
/// source named no fn of is a crate node with nothing under it — there is no
/// synthetic function. `node_budget` is a CAP on what is drawn, never a quota to
/// fill.
///
/// Layout reuses the SHARED scale-aware engine ([`facett_graphview::layout_world`]);
/// identity is overlaid by replaying the exact node order
/// [`facett_graphview::WorldGraph::from_specs`] produces, and the engine's edge set
/// is REPLACED with containment, the call edges and the skeleton's repo → repo
/// dependency edges — every edge drawn is one the sources gave.
fn build_world(&self, dark: bool, node_budget: usize) -> WorldBuild {
WORLD_BUILD_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
// DRILL: when focused on a repo, the world contains just that repo's galaxy (its
// crates + functions) — the "click a repo → expand its crates" descent. Otherwise
// the whole warehouse. A focus on an unknown repo falls back to the whole set.
let repos: Vec<String> = match &self.world_focus {
Some(f) if self.repo_names().iter().any(|r| r == f) => vec![f.clone()],
_ => self.repo_names(),
};
// Repo → the crates the skeleton says it produces (a repo that produces none
// is a repo node alone).
let repo_crates: Vec<(String, Vec<String>)> = repos
.iter()
.map(|r| {
(
r.clone(),
self.graph
.facts
.get(r)
.map(|f| f.produces.iter().cloned().collect())
.unwrap_or_default(),
)
})
.collect();
let total_crates: usize = repo_crates.iter().map(|(_, cs)| cs.len()).sum();
let overhead = repos.len() + total_crates; // repo + crate nodes
let mut budget_left = node_budget.saturating_sub(overhead);
// Allocate per (repo, crate): the fns the source named, greedily, truncating
// (and saying so) to fit the bounded preview.
let mut alloc: Vec<(String, Vec<(String, CrateAlloc)>)> =
Vec::with_capacity(repo_crates.len());
let mut truncated = false;
for (r, cs) in &repo_crates {
let mut this: Vec<(String, CrateAlloc)> = Vec::with_capacity(cs.len());
for c in cs {
if let Some(g) = self.real_crate(r, c) {
let want = g.nodes.len();
let take = want.min(budget_left);
if take < want {
truncated = true;
}
let real: Vec<(String, String)> = g
.nodes
.iter()
.take(take)
.map(|n| (n.id.clone(), n.name.clone()))
.collect();
budget_left -= take;
this.push((
c.clone(),
CrateAlloc {
functions: take,
real,
},
));
} else {
this.push((
c.clone(),
CrateAlloc {
functions: 0,
real: Vec::new(),
},
));
}
}
alloc.push((r.clone(), this));
}
// Function COUNTS drive the shared engine's node layout; identity is overlaid
// below by replaying the SAME order.
let specs: Vec<facett_graphview::RepoSpec> = alloc
.iter()
.map(|(r, cs)| facett_graphview::RepoSpec {
name: r.clone(),
crates: cs
.iter()
.map(|(name, a)| facett_graphview::CrateSpec {
name: name.clone(),
functions: a.functions as u32,
})
.collect(),
})
.collect();
let real_fn_nodes: usize = alloc
.iter()
.flat_map(|(_, cs)| cs.iter())
.map(|(_, a)| a.real.len())
.sum();
// Materialise the node set only: the engine's own edges are replaced below
// by the edges the sources gave.
let mut w = facett_graphview::WorldGraph::from_specs(
&specs,
facett_graphview::Lod::Functions,
false,
false,
);
// Replay from_specs' node order (repo node → per crate: crate node, then its
// function nodes) to overlay real identity + build the real edge set.
let mut meta: Vec<Option<(String, String)>> = vec![None; w.n];
let mut id_to_idx: std::collections::HashMap<String, u32> =
std::collections::HashMap::new();
let mut wedges: Vec<(u32, u32)> = Vec::new();
let mut repo_node_idx: Vec<u32> = Vec::with_capacity(alloc.len());
let mut real_fn_crates = 0usize;
let mut crates_without_fns = 0usize;
{
let mut idx: u32 = 0;
for (_, cs) in &alloc {
let repo_idx = idx;
repo_node_idx.push(repo_idx);
idx += 1; // repo node
for (_, a) in cs {
let crate_idx = idx;
idx += 1; // crate node
wedges.push((repo_idx, crate_idx)); // containment repo→crate
if a.real.is_empty() {
crates_without_fns += 1;
} else {
real_fn_crates += 1;
}
for (id, name) in &a.real {
meta[idx as usize] = Some((id.clone(), name.clone()));
id_to_idx.insert(id.clone(), idx);
wedges.push((crate_idx, idx)); // containment crate→fn
idx += 1;
}
}
}
debug_assert_eq!(idx as usize, w.n, "replay must match from_specs node count");
}
// REAL intra-crate call edges (both endpoints among the taken nodes).
let mut real_call_edges = 0usize;
for (r, cs) in &alloc {
for (cname, a) in cs {
if a.real.is_empty() {
continue;
}
if let Some(g) = self.real_crate(r, cname) {
for e in &g.edges {
if let (Some(&f), Some(&t)) = (id_to_idx.get(&e.from), id_to_idx.get(&e.to))
{
wedges.push((f, t));
real_call_edges += 1;
}
}
}
}
}
// Repo → repo: the skeleton's OWN dependency edges (both repos in this world).
let repo_at: std::collections::HashMap<&str, u32> = alloc
.iter()
.zip(&repo_node_idx)
.map(|((r, _), i)| (r.as_str(), *i))
.collect();
let mut repo_dep_edges = 0usize;
for e in &self.graph.edges {
if let (Some(&f), Some(&t)) = (repo_at.get(e.from.as_str()), repo_at.get(e.to.as_str()))
{
if f != t {
wedges.push((f, t));
repo_dep_edges += 1;
}
}
}
// Drive the layout. 3D mode lays the nodes out as a nested-spherical GALAXY (O(n),
// no force pass) and flattens them through the orthographic camera at yaw = 0 (the
// baseline the cached model + `state_json` read); the live `ui()` re-projects the
// cached `pos3` at the current azimuth each frame so the galaxy spins WITHOUT a
// relayout. Flat mode uses the shared scale-aware force/hierarchical spread.
w.edges = wedges;
let (pos, lane_key, pos3): WorldLayout = if self.world_3d {
let p3 = galaxy_3d(&w);
let flat: Vec<[f32; 2]> = p3
.iter()
.map(|&p| project_azimuth(p, 0.0, WORLD_3D_PITCH))
.collect();
(flat, "galaxy_3d", Some(p3))
} else if self.universe {
(facett_graphview::world::hier_placement(&w), "galaxy", None)
} else {
let (pos, lane) =
facett_graphview::layout_world(&w, &facett_graphview::LayoutBudget::default());
(pos, lane.key(), None)
};
// Lower to a graphpan model. Every tier is COLOURED by its repo/crate hue — the
// grey-blob fix: a repo wears its categorical hue, a crate a shade of it, and a
// FUNCTION a lighter shade of ITS crate, labelled with its name. Crate ordinals
// come from the shared `world::crate_ordinals` so sibling crates separate in hue.
const SPREAD: f32 = WORLD_SPREAD;
let ords = facett_graphview::world::crate_ordinals(&w);
let (mut repo_nodes, mut crate_nodes, mut fn_nodes) = (0usize, 0usize, 0usize);
let nodes: Vec<GraphNode> = (0..w.n)
.map(|i| {
let repo = w.repo_of[i] as usize;
let slot = self.slot(&w.repos[repo]);
let ord = ords[i] as usize;
let (label, fill) = match w.level[i] {
facett_graphview::NodeLevel::Repo => {
repo_nodes += 1;
(w.repos[repo].clone(), palette::repo_color(slot, dark))
}
facett_graphview::NodeLevel::Crate => {
crate_nodes += 1;
(String::new(), palette::crate_color(slot, ord, dark))
}
facett_graphview::NodeLevel::Function => {
fn_nodes += 1;
match &meta[i] {
Some((_, name)) => (
truncate_label(name),
palette::function_color(slot, ord, dark, true),
),
None => (
String::new(),
palette::function_color(slot, ord, dark, false),
),
}
}
};
GraphNode {
id: format!("w{i}"),
label,
fill,
stroke: Color32::TRANSPARENT,
pos: Pos2::new(pos[i][0] * SPREAD, pos[i][1] * SPREAD),
}
})
.collect();
let edge_col = if dark {
Color32::from_gray(70)
} else {
Color32::from_gray(185)
};
let edges: Vec<GraphEdge> = w
.edges
.iter()
.map(|&(a, b)| GraphEdge {
from: format!("w{a}"),
to: format!("w{b}"),
color: edge_col,
dashed: false,
label: None,
})
.collect();
// Per-node drill metadata (tier / owning repo / real function name) so a click on
// a `w{i}` chip maps to a drill action, and a selected function surfaces its identity.
let node_level: Vec<u8> = (0..w.n)
.map(|i| match w.level[i] {
facett_graphview::NodeLevel::Repo => 0,
facett_graphview::NodeLevel::Crate => 1,
facett_graphview::NodeLevel::Function => 2,
})
.collect();
let node_repo: Vec<String> = (0..w.n)
.map(|i| w.repos[w.repo_of[i] as usize].clone())
.collect();
let node_fn: Vec<Option<String>> = (0..w.n)
.map(|i| meta[i].as_ref().map(|(_, name)| name.clone()))
.collect();
WorldBuild {
n: w.n,
edges: w.edges.len(),
lane: lane_key,
repo_nodes,
crate_nodes,
fn_nodes,
real_fn_nodes,
real_fn_crates,
crates_without_fns,
real_call_edges,
repo_dep_edges,
truncated,
node_level,
node_repo,
node_fn,
pos3,
model: GraphModel { nodes, edges },
}
}
/// Recompute the cached world build **only** when an input that changes its result changed
/// (theme, preview budget, or the real-function data version). The per-frame-relayout fix:
/// the Barnes–Hut converge in [`Self::build_world`] runs once per data/zoom change instead of
/// once per egui repaint. `&self` (interior mutability) so `state_json` can keep it warm.
fn refresh_world_cache(&self, dark: bool, budget: usize) {
let stale = match &*self.world_cache.borrow() {
Some(c) => {
c.dark != dark
|| c.budget != budget
|| c.ver != self.world_data_ver
|| c.world_3d != self.world_3d
|| c.universe != self.universe
}
None => true,
};
if stale {
let build = self.build_world(dark, budget);
*self.world_cache.borrow_mut() = Some(CachedWorld {
dark,
budget,
ver: self.world_data_ver,
world_3d: self.world_3d,
universe: self.universe,
build,
});
}
}
/// The cached, converged world build for `(dark, budget)`, rebuilt through
/// [`Self::refresh_world_cache`] only on a real input change. Used by `state_json`; `ui`
/// borrows the `world_cache` field directly so it can also mutably borrow `self.view`.
fn world_build_cached(&self, dark: bool, budget: usize) -> std::cell::Ref<'_, WorldBuild> {
self.refresh_world_cache(dark, budget);
std::cell::Ref::map(self.world_cache.borrow(), |c| {
&c.as_ref()
.expect("refresh_world_cache populated the cache")
.build
})
}
}
impl Facet for WarehouseNav {
fn title(&self) -> &str {
"Warehouse"
}
fn ui(&mut self, ui: &mut egui::Ui) {
// Land the one-shot live fetch if the worker finished, and keep asking for
// frames while it runs so the "reading" line stays live and the world swaps to
// the real tier the moment it lands.
self.poll_real_functions();
// The skeleton is the source's: asked once, off this thread, on first draw.
self.ensure_skeleton();
self.poll_skeleton();
if self.real_pending.is_some() || self.skeleton_pending.is_some() {
ui.ctx().request_repaint();
}
self.dark = ui.visuals().dark_mode;
let dark = self.dark;
// ── breadcrumb + climb controls ──────────────────────────────────────
let mut go_home = false;
let mut go_up = false;
ui.horizontal(|ui| {
if ui.button("⌂ constellation").on_hover_text("back to the whole warehouse (L0)").clicked() {
go_home = true;
}
if !matches!(self.level, Level::Constellation)
&& ui.button("⬆ up").on_hover_text("climb one level").clicked()
{
go_up = true;
}
if ui.button("⊙ fit").on_hover_text("reset pan + zoom").clicked() {
if self.world {
// In world mode (the universe star-field included), FLY to the cloud's
// bounds on the shared smooth path, like every other view's ⊙ fit.
self.world_fit_pending = true;
self.world_fit_fly = true;
} else {
// fly to the level's graph, centred in what the viewer sees
self.frame_level();
}
}
// 📌 Stay / 🎯 Focus — the viewer's one choice for every graph pane
// (facett_core::fly); the canvas below reads it on a click.
let _ = facett_core::fly::NavMode::toggle(ui, facett_core::fly::NavMode::remembered(ui.ctx()));
ui.separator();
// WORLD GRAPH toggle: flip between the repo-grid drill-down and the LOD
// world graph (down to functions) on the shared scale-aware engine.
let world_label = if self.world { "🌐 world: ON" } else { "🌐 world: off" };
if ui
.button(world_label)
.on_hover_text("toggle the WORLD GRAPH — repos→crates→functions, GPU-cloud scale")
.clicked()
{
self.world = !self.world;
self.view.fit();
self.world_fit_pending = self.world;
}
// 3D galaxy + auto-rotate controls — only meaningful while the world graph is shown.
if self.world {
let prev_3d = self.world_3d;
ui.checkbox(&mut self.world_3d, "3D").on_hover_text(
"lay the world out as a nested-spherical galaxy + orthographic camera (off = flat 2D spread)",
);
if self.world_3d != prev_3d {
// Flipping the layout dimension rebuilds the cached layout (it is a cache
// key) → refit the camera to the new bounds; leaving 3D stops any spin.
self.world_fit_pending = true;
if !self.world_3d {
self.world_rotate = false;
}
}
ui.add_enabled_ui(self.world_3d, |ui| {
ui.checkbox(&mut self.world_rotate, "3D rotating").on_hover_text(
"continuously spin the galaxy around its vertical axis — a live showpiece (projection-only, no relayout)",
);
});
// UNIVERSE toggle — only meaningful while the world graph is shown. When ON the
// preview cap is removed and the FULL galaxy renders as a pure GPU star-field
// (labels off). Flipping it rebuilds the (cached) layout once → re-fit the camera.
let prev = self.universe;
let uni_label = if self.universe { "✦ universe: ON" } else { "✦ universe: off" };
if ui
.button(uni_label)
.on_hover_text(
"UNIVERSE — the ENTIRE code universe as one GPU star-field: NO node cap \
(12k → 100k → 1M), no labels, coloured by repo/crate (needs the `gpu` feature \
+ an installed cloud renderer; falls back to the CPU dot-cloud otherwise)",
)
.clicked()
{
self.universe = !self.universe;
// Seed an interactive default the first time we enter Universe from the
// 10M sentinel target (materialising 10M nodes at once is not the intent).
if self.universe && self.world_target > UNIVERSE_DEFAULT_TARGET {
self.world_target = UNIVERSE_DEFAULT_TARGET;
}
self.view.fit();
self.world_fit_pending = true;
}
let _ = prev;
}
ui.separator();
let crumbs = self.crumbs();
ui.label(crumbs.join(" › "));
ui.separator();
ui.weak("click a node to drill in · scroll to zoom · drag to pan");
ui.separator();
// Where the skeleton was read (the function tier has its own line in the
// world header below).
ui.weak(format!("skeleton: {}", self.skeleton_source));
});
// A skeleton in flight / a source that could not answer: said on the surface,
// and nothing is drawn in the skeleton's place.
if let Some((_, started)) = &self.skeleton_pending {
ui.horizontal(|ui| {
ui.spinner();
ui.weak(format!(
"asking for the skeleton — {:.1}s (off the UI thread)",
started.elapsed().as_secs_f32()
));
});
}
if let Some(why) = &self.skeleton_error {
ui.colored_label(
Color32::from_rgb(0xc6, 0x28, 0x28),
format!("NO SKELETON — {why}"),
);
}
if go_home {
self.home();
}
if go_up {
self.up();
}
ui.separator();
// ── WORLD GRAPH mode: the LOD (repos→crates→functions) surface ──────────
if self.world {
// Pull the REAL function tier from the live warehouse the first time the world
// is shown (once, guarded) — the constellation landing never paid this cost.
self.ensure_real_functions();
// Converge + build the world ONCE per data/zoom change (cached); a repaint just
// re-reads it. Previously `build_world` (a full 80-iteration Barnes–Hut converge
// over the ≤WORLD_PREVIEW_CAP preview) ran on EVERY frame — ~48 ms for a 20-node
// world, ~140 ms for the default 4 000-node preview — "segt för 20 noder".
let budget = self.world_budget();
let world_target = self.world_target;
let planned = self.planned_lane();
// AUTO-ROTATE: advance the camera azimuth over wall-clock time and keep repainting.
// This mutates ONLY the projection angle — the galaxy_3d layout stays cached, so no
// frame here rebuilds it (the frame-budget guard asserts the 0-rebuild invariant).
// `stable_dt` (clamped) gives a smooth, framerate-independent ~15°/s spin.
if self.world_3d && self.world_rotate {
let dt = ui.input(|i| i.stable_dt).min(0.1);
self.world_yaw = (self.world_yaw + WORLD_ROTATE_RAD_PER_S * dt)
.rem_euclid(std::f32::consts::TAU);
// 30 Hz is plenty for a 15°/s spin; an uncapped `request_repaint()` ran the
// whole edge tessellation at the display's vsync (MEASURED 2026-08-22 on
// oden, idle dwarves-ui-gpu: 90 % of a core, perf top = memmove/malloc/
// epaint tessellator/cubic-bezier flatten under facett_graphpan::draw_edge).
// Speed is unchanged — it rides `stable_dt`, not the frame count.
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(33));
}
self.refresh_world_cache(dark, budget);
// Borrow the cached build (the `world_cache` field) and `self.view` (a disjoint
// field) at once — draw straight from the cache, no per-frame layout.
let cache = self.world_cache.borrow();
let wb = &cache
.as_ref()
.expect("refresh_world_cache populated the cache")
.build;
let (n, e, lane) = (wb.n, wb.edges, wb.lane);
// 3D: re-project the CACHED galaxy layout through the orthographic camera at the
// current azimuth. This is the ONLY per-frame work the spin costs — a cheap map over
// the node positions, NOT a relayout (the layout `pos3` is reused from the cache).
// Flat mode draws the cached model directly (borrowed, zero copy).
let display: std::borrow::Cow<'_, GraphModel> = match wb.pos3.as_ref() {
Some(pos3) => {
let yaw = self.world_yaw;
let mut m = wb.model.clone();
for (nd, &p) in m.nodes.iter_mut().zip(pos3.iter()) {
let q = project_azimuth(p, yaw, WORLD_3D_PITCH);
nd.pos = Pos2::new(q[0] * WORLD_SPREAD, q[1] * WORLD_SPREAD);
}
std::borrow::Cow::Owned(m)
}
None => std::borrow::Cow::Borrowed(&wb.model),
};
ui.horizontal(|ui| {
ui.strong("🌐 WORLD GRAPH");
ui.weak(format!(
"cap {world_target} nodes · lane at that cap: {planned} · drawing {n} nodes / {e} edges via {lane}"
));
// The function tier: what the source named, or why there is none.
if let Some((_, started)) = &self.real_pending {
ui.weak(format!(
"· asking for the function tier — {:.1}s (off the UI thread; no function is drawn until it lands)",
started.elapsed().as_secs_f32()
));
} else if let Some(err) = &self.real_error {
ui.colored_label(Color32::from_rgb(0xc6, 0x28, 0x28), format!("· NO FUNCTION TIER — {err}"));
} else if self.real_source.is_none() && wb.real_fn_nodes == 0 {
ui.weak("· no function tier (no source wired)");
} else {
ui.weak(format!(
"· functions: {} fns / {} call edges across {} crates{}{} — from {}",
wb.real_fn_nodes,
wb.real_call_edges,
wb.real_fn_crates,
if wb.crates_without_fns > 0 { format!(" · {} crate(s) the source named no fn of", wb.crates_without_fns) } else { String::new() },
if wb.truncated { " · cut to the preview cap" } else { "" },
// WHERE the tier came from, on the surface: the question "real or
// hard-coded?" is answered by the line that draws the numbers.
self.real_source.as_ref().map(|s| s.describe()).unwrap_or_else(|| "the host (no source wired)".to_string()),
));
}
});
let text = if dark {
Color32::from_gray(235)
} else {
Color32::from_gray(25)
};
let text_dim = if dark {
Color32::from_gray(120)
} else {
Color32::from_gray(150)
};
let sel_ring = if dark {
Color32::from_rgb(0x39, 0x87, 0xe5)
} else {
Color32::from_rgb(0x2a, 0x78, 0xd6)
};
let bg = if dark {
Color32::from_gray(14)
} else {
Color32::from_gray(250)
};
// POINT-CLOUD mode: the world is thousands of nodes — render them as small
// constant-size DOTS (not 184-px chips that pile into a grey blob), sized down
// as the cloud grows so they stay separated. This is THE fix for "1000 noder är
// gråa och läggs över varandra".
let dot_r = (9000.0 / wb.n.max(1) as f32).sqrt().clamp(1.8, 6.0);
self.view.dot_px = Some(dot_r);
// FIT the camera to the laid-out cloud's bounds on first show / after a drill —
// the honest ⊙ fit, so the whole spread galaxy is visible + separated instead of
// overflowing the viewport (the naive zoom-1 recentre left it a central blob).
if self.world_fit_pending {
let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for nd in &display.nodes {
x0 = x0.min(nd.pos.x);
y0 = y0.min(nd.pos.y);
x1 = x1.max(nd.pos.x);
y1 = y1.max(nd.pos.y);
}
if x1 >= x0 {
let vp = facett_core::nav::canvas_size(ui);
if self.world_fit_fly {
// the canvas below is this ui's next allocation: its id is the
// flight's key (`camera_step` advances it there)
let canvas = ui.next_auto_id();
let now = ui.input(|i| i.time);
facett_graphpan::fly_to_bounds(ui.ctx(), canvas, &self.view, Pos2::new(x0, y0), Pos2::new(x1, y1), vp, 24.0, now);
} else {
self.view
.fit_bounds(Pos2::new(x0, y0), Pos2::new(x1, y1), vp, 24.0);
}
}
self.world_fit_pending = false;
self.world_fit_fly = false;
}
// Ring the selected function so the drill selection reads on the canvas.
let mut decos = Decorations::default();
if let Some(sel) = self.world_selected {
decos.nodes.insert(
format!("w{sel}"),
NodeDecoration {
ring: Some(sel_ring),
badge: None,
badge_color: None,
},
);
}
// UNIVERSE mode routes the FULL cloud through the shared instanced GPU renderer
// (labels OFF — a pure star-field); bounded modes keep the CPU dot-cloud (draw_graph).
// Both take the projected `display` model and return a GraphResponse (one drill path).
let canvas_rect = Rect::from_min_size(ui.cursor().min, facett_core::nav::canvas_size(ui));
let resp = if self.universe {
let (r, report) = draw_universe_cloud(
ui,
&display,
&decos,
&mut self.view,
&mut self.universe_pick.borrow_mut(),
bg,
text,
sel_ring,
text_dim,
);
*self.universe_legibility.borrow_mut() = report;
r
} else {
draw_graph(
ui,
&display,
&decos,
&mut self.view,
bg,
text,
sel_ring,
text_dim,
)
};
*self.last_shot.borrow_mut() = Some(Shot::of(&display, &self.view, canvas_rect, ui));
drop(display); // release the (possibly borrowed-from-cache) projected model
// Capture the clicked chip's drill target WHILE the cache is borrowed, then drop
// the borrow so the drill can take `&mut self`.
let clicked = resp.clicked_node.as_ref().and_then(|id| {
id.strip_prefix('w')
.and_then(|s| s.parse::<usize>().ok())
.filter(|&i| i < wb.node_level.len())
.map(|i| (i, wb.node_level[i], wb.node_repo[i].clone()))
});
drop(cache);
if let Some((i, lvl, repo)) = clicked {
let refocused = self.world_drill(i, lvl, &repo);
// 📌 Stay: a click never moves the camera (the drill's own fit is
// dropped); 🎯 Focus: fly to frame what the click opened.
let focus = facett_core::fly::NavMode::remembered(ui.ctx()) == facett_core::fly::NavMode::Focus;
self.world_fit_pending = refocused && focus;
self.world_fit_fly = self.world_fit_pending;
}
return;
}
// ── the canvas: overlay (hulls + glow) UNDER the reused graphpan chips ──
let scene = self.build_scene(dark);
let (model, decos) = self.to_graph_model(&scene, dark);
// Predict the rect `draw_graph` will allocate (the shared canvas size: what is
// left, never past the visible clip) so the overlay lines up with the projection.
let rect = Rect::from_min_size(ui.cursor().min, facett_core::nav::canvas_size(ui));
// The level's graph framed: first show, ⊙ fit, ⌂ / ⬆, a 🎯 Focus drill —
// fitted and CENTRED in the visible canvas (flying when asked to).
if self.level_fit_pending {
if let Some((min, max)) = model_bounds(&model, false) {
if self.level_fit_fly {
let now = ui.input(|i| i.time);
facett_graphpan::fly_to_bounds(ui.ctx(), ui.next_auto_id(), &self.view, min, max, rect.size(), 24.0, now);
} else {
self.view.fit_bounds(min, max, rect.size(), 24.0);
}
}
self.level_fit_pending = false;
self.level_fit_fly = false;
}
self.paint_overlay(ui, rect, &scene, dark);
let text = if dark {
Color32::from_gray(235)
} else {
Color32::from_gray(25)
};
let text_dim = if dark {
Color32::from_gray(120)
} else {
Color32::from_gray(150)
};
let sel_ring = if dark {
Color32::from_rgb(0x39, 0x87, 0xe5)
} else {
Color32::from_rgb(0x2a, 0x78, 0xd6)
};
// Transparent bg so our overlay (painted first) shows through; the shared
// renderer paints edges + chips + selection + reads the click on top.
let resp = draw_graph(
ui,
&model,
&decos,
&mut self.view,
Color32::TRANSPARENT,
text,
sel_ring,
text_dim,
);
*self.last_shot.borrow_mut() = Some(Shot::of(&model, &self.view, rect, ui));
if let Some(id) = resp.clicked_node {
// A click that DESCENDS (repo→ its L1, crate→ its L2) drills; a click on
// the already-focused anchor just selects (the renderer handled that).
// 📌 Stay: the camera stays; 🎯 Focus: fly to frame the level it opens.
let focus = facett_core::fly::NavMode::remembered(ui.ctx()) == facett_core::fly::NavMode::Focus;
let descends = match (&self.level, id.as_str()) {
(Level::Repo(r), _) => id != format!("repo:{r}"),
(Level::Crate { krate, .. }, _) => id != format!("crate:{krate}"),
_ => true,
};
if descends {
self.drill(&id);
if focus {
self.frame_level();
}
}
}
}
fn state_json(&self) -> serde_json::Value {
let dark = self.dark;
// ── WORLD GRAPH state (headless-verifiable): the LOD surface + its scale plan ──
if self.world {
let wb = self.world_build_cached(dark, self.world_budget());
// COLOUR proof (grey-blob red-when-broken): the count of DISTINCT node fills in
// the lowered model. Grey blobs ⇒ ~1; a coloured repo/crate/function map ⇒ many.
let mut fills = std::collections::BTreeSet::new();
for nd in &wb.model.nodes {
fills.insert((nd.fill.r(), nd.fill.g(), nd.fill.b()));
}
// SPREAD proof (stacking red-when-broken): bbox span + the fraction of nodes that
// share a coarse grid cell with another (the "on top of each other" proxy).
let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for nd in &wb.model.nodes {
x0 = x0.min(nd.pos.x);
y0 = y0.min(nd.pos.y);
x1 = x1.max(nd.pos.x);
y1 = y1.max(nd.pos.y);
}
let span = (x1 - x0).max(y1 - y0).max(1e-3);
let g = 200.0f32;
let mut cells = std::collections::HashMap::<(i32, i32), u32>::new();
for nd in &wb.model.nodes {
let k = (
((nd.pos.x - x0) / span * g) as i32,
((nd.pos.y - y0) / span * g) as i32,
);
*cells.entry(k).or_default() += 1;
}
let stacked = wb
.model
.nodes
.iter()
.filter(|nd| {
let k = (
((nd.pos.x - x0) / span * g) as i32,
((nd.pos.y - y0) / span * g) as i32,
);
cells[&k] > 1
})
.count();
let stacked_frac = stacked as f32 / wb.model.nodes.len().max(1) as f32;
// DRILL: the selected function's identity + its callers/callees from the REAL
// call edges (the function→callers/callees step). Datatype/variable tiers are
// NOT in the warehouse — surfaced honestly as unavailable, never fabricated.
let selected = self.world_selected.and_then(|i| {
let sid = format!("w{i}");
let callees = wb.model.edges.iter().filter(|e| e.from == sid).count();
let callers = wb.model.edges.iter().filter(|e| e.to == sid).count();
wb.node_fn.get(i).cloned().flatten().map(|name| {
serde_json::json!({
"index": i,
"name": name,
"callers": callers,
"callees": callees,
"real": true,
"variables": serde_json::Value::Null, // not in the warehouse
"datatypes": serde_json::Value::Null, // not in the warehouse
})
})
});
// Built into a binding rather than returned straight out of `json!`: this
// object is one key short of serde_json's macro recursion limit, so the
// in-flight oracle is inserted below instead of blowing the expansion.
let mut world_state = serde_json::json!({
"kind": "warehouse-nav",
"level": "world",
"world": true,
"lod": "functions",
"world_target": self.world_target,
"planned_lane": self.planned_lane(),
"bh_max": WORLD_BH_MAX,
// Repo/crate-tier provenance (a SEPARATE axis from the function tier).
"skeleton_read": self.skeleton_read(),
"skeleton_source": self.skeleton_source,
"skeleton_error": self.skeleton_error,
"root": self.root,
"preview_cap": WORLD_PREVIEW_CAP,
// ── UNIVERSE mode: the ENTIRE universe as a GPU star-field, NO budget cap ──
// `universe` is on; `cap_applied` is false (the WORLD_PREVIEW_CAP does not clamp
// the build); `universe_nodes` is the FULL node count rendered (== preview_nodes,
// which now equals the whole galaxy, not a bounded slice); `labels` is "off"
// (pure star-field); `universe_gpu_cloud` reports whether the instanced GPU lane
// actually painted (true = a renderer is installed; false = honest CPU fallback).
"universe": self.universe,
"cap_applied": !self.universe,
"universe_nodes": if self.universe { wb.n } else { 0 },
"labels": if self.universe { "off" } else { "dots" },
"universe_gpu_cloud": self.universe_legibility.borrow().gpu_cloud,
"preview_nodes": wb.n,
"preview_edges": wb.edges,
"preview_lane": wb.lane,
"preview_repo_nodes": wb.repo_nodes,
"preview_crate_nodes": wb.crate_nodes,
"preview_fn_nodes": wb.fn_nodes,
// The function tier: every node and edge is one the source named.
"real_fn_nodes": wb.real_fn_nodes,
"real_fn_crates": wb.real_fn_crates,
"crates_without_fns": wb.crates_without_fns,
"real_call_edges": wb.real_call_edges,
"repo_dep_edges": wb.repo_dep_edges,
"functions_truncated": wb.truncated,
"preview_model_nodes": wb.model.nodes.len(),
// COLOUR + SPREAD proofs (robot-verifiable).
"distinct_fills": fills.len(),
"spread_span": span,
"stacked_frac": stacked_frac,
// DRILL state.
"world_focus": self.world_focus,
"selected": selected,
// 3D galaxy + auto-rotate state. `rotate_angle` is the live camera azimuth
// (radians); a robot asserts it CHANGED across repaints while the layout stayed
// cached (`world_build_calls()` flat). `preview_lane` reads "galaxy_3d" in 3D.
"world_3d": self.world_3d,
"world_rotate": self.world_rotate,
"rotate_angle": self.world_yaw,
// The LIVE real-function source wiring (honest, robot-verifiable): whether
// a source is installed + consulted, its description, and any fetch error.
// A source that cannot answer leaves NO function tier and is named here.
"real_source": self.real_source.is_some(),
"real_source_desc": self.real_source.as_ref().map(|s| s.describe()),
"real_scanned": self.real_scanned,
"real_error": self.real_error,
"dark": dark,
});
if let Some(o) = world_state.as_object_mut() {
// The in-flight oracle: "still reading" is not "read nothing", and not
// "the warehouse indexed no functions".
o.insert(
"real_pending".into(),
serde_json::json!(self.real_pending.is_some()),
);
// A tier that was not read has NO counts: `null`, never a `0` nobody
// counted. The skeleton tier and the function tier each say so.
let fns_read = self.real_source.is_none()
|| (self.real_scanned
&& self.real_pending.is_none()
&& self.real_error.is_none());
o.insert("functions_read".into(), serde_json::json!(fns_read));
if !self.skeleton_read() {
for k in [
"universe_nodes",
"preview_nodes",
"preview_edges",
"preview_repo_nodes",
"preview_crate_nodes",
"preview_model_nodes",
"repo_dep_edges",
"distinct_fills",
"spread_span",
"stacked_frac",
] {
o.insert(k.into(), serde_json::Value::Null);
}
}
if !fns_read || !self.skeleton_read() {
for k in [
"preview_fn_nodes",
"real_fn_nodes",
"real_fn_crates",
"crates_without_fns",
"real_call_edges",
] {
o.insert(k.into(), serde_json::Value::Null);
}
}
}
return world_state;
}
let scene = self.build_scene(dark);
let (repo_count, crate_count) = scene.nodes.iter().fold((0, 0), |(r, c), n| match n.kind {
NodeKind::Repo => (r + 1, c),
NodeKind::Crate => (r, c + 1),
NodeKind::Extern => (r, c),
});
let nodes: Vec<_> = scene
.nodes
.iter()
.map(|n| {
serde_json::json!({
"id": n.id,
"label": n.label,
"kind": n.kind.as_str(),
"repo": n.repo,
"color": palette::hex(if n.bug { palette::BUG_ALERT } else { n.fill }),
"recency": n.recency,
"glow": glow_alpha(n.recency),
"bug": n.bug,
})
})
.collect();
let edges: Vec<_> = scene
.edges
.iter()
.map(|(f, t, d)| serde_json::json!({ "from": f, "to": t, "dashed": d }))
.collect();
let hulls: Vec<_> = scene
.hulls
.iter()
.map(|h| {
serde_json::json!({
"repo": h.repo,
"members": h.member_ids,
"color": palette::hex(h.color),
})
})
.collect();
// The stable colour-by-repo mapping (repo → hex), so a robot asserts the
// categorical palette without pixels.
let palette_map: serde_json::Map<String, serde_json::Value> = self
.repo_slot
.iter()
.map(|(r, &s)| {
(
r.clone(),
serde_json::json!(palette::hex(palette::repo_color(s, dark))),
)
})
.collect();
let (focus_repo, focus_crate) = match &self.level {
Level::Constellation => (serde_json::Value::Null, serde_json::Value::Null),
Level::Repo(r) => (serde_json::json!(r), serde_json::Value::Null),
Level::Crate { repo, krate } => (serde_json::json!(repo), serde_json::json!(krate)),
};
// A skeleton that was not read (not asked yet, in flight, or refused) has NO
// counts: `null`, never a `0` nobody counted. `skeleton_read` says which.
let read = self.skeleton_read();
let count = |n: usize| {
if read {
serde_json::json!(n)
} else {
serde_json::Value::Null
}
};
serde_json::json!({
"kind": "warehouse-nav",
"level": self.level.key(),
"skeleton_read": read,
"skeleton_source": self.skeleton_source,
// the source's sentence when it gave no skeleton (then every count below is
// of an empty graph: read THIS first)
"skeleton_error": self.skeleton_error,
"skeleton_pending": self.skeleton_pending.is_some(),
"root": self.root,
"focus_repo": focus_repo,
"focus_crate": focus_crate,
"breadcrumb": self.crumbs(),
"repo_count": count(repo_count),
"crate_count": count(crate_count),
"node_count": count(scene.nodes.len()),
"edge_count": count(scene.edges.len()),
"hull_count": count(scene.hulls.len()),
"dark": dark,
"nodes": nodes,
"edges": edges,
"hulls": hulls,
"palette": palette_map,
"bug_hue": palette::hex(palette::BUG_ALERT),
})
}
fn update_json(&mut self, msg_json: &str) {
// The headless control surface (drives drill-down from a robot test, the SAME
// path a chip click takes):
// {"drill":"repo:<name>"|"crate:<name>"} → descend
// {"up":true} → climb one level
// {"home":true} / {"reset":true} → back to L0 constellation
if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) {
// The headless twin of the first draw: ask the sources and wait for them.
// {"read":true} → the skeleton, once (what the first frame starts)
// {"reread":true} → ask both sources again
if v.get("reread").and_then(|d| d.as_bool()) == Some(true) {
self.skeleton_scanned = false;
self.real_scanned = false;
self.ensure_skeleton();
self.wait_skeleton();
if self.world {
self.ensure_real_functions();
self.wait_real_functions();
}
return;
}
if v.get("read").and_then(|d| d.as_bool()) == Some(true) {
self.ensure_skeleton();
self.wait_skeleton();
return;
}
// WORLD GRAPH controls: {"world":true|false} toggles the LOD surface;
// {"world_target":N} sets the cap the engine lays out (drives the lane).
if let Some(on) = v.get("world").and_then(|d| d.as_bool()) {
self.world = on;
if on {
self.ensure_skeleton();
self.wait_skeleton();
}
// Entering world mode asks for the function tier once (the headless
// twin of the first world `ui()` — so a robot that drives world:true then
// reads state_json sees what the source answered).
if on {
self.ensure_real_functions();
self.wait_real_functions();
}
self.view.fit();
self.world_fit_pending = on;
return;
}
if let Some(t) = v.get("world_target").and_then(|d| d.as_u64()) {
self.world_target = (t as usize).max(1);
self.world_fit_pending = self.world;
return;
}
// 3D controls (the headless twins of the "3D" / "3D rotating" checkboxes):
// {"world_3d":true|false} → nested-spherical galaxy vs flat 2D spread
// {"world_rotate":true|false} → auto-spin the galaxy about its vertical axis
if let Some(on) = v.get("world_3d").and_then(|d| d.as_bool()) {
self.world_3d = on;
self.world_fit_pending = self.world;
if !on {
self.world_rotate = false;
}
return;
}
if let Some(on) = v.get("world_rotate").and_then(|d| d.as_bool()) {
self.world_rotate = on;
return;
}
// UNIVERSE seam (the headless twin of the ✦ toggle): {"universe":true|false}.
// ON removes the WORLD_PREVIEW_CAP so the FULL galaxy is built + rendered on the
// GPU star-field. Unlike the button it does NOT reseed `world_target` — a robot
// sets the exact node count it wants to prove (e.g. {"world_target":100000}).
if let Some(on) = v.get("universe").and_then(|d| d.as_bool()) {
self.universe = on;
if on {
self.world = true;
self.ensure_skeleton();
self.wait_skeleton();
self.ensure_real_functions();
self.wait_real_functions();
}
self.view.fit();
self.world_fit_pending = self.world;
return;
}
// WORLD DRILL seam (the headless twin of a chip click): {"world_drill": <index>}
// descends on the node at that preview index (repo→focus, function→select) — the
// SAME path `ui()` takes for a clicked `w{i}` chip.
if let Some(idx) = v.get("world_drill").and_then(|d| d.as_u64()) {
let i = idx as usize;
self.refresh_world_cache(self.dark, self.world_budget());
let info = {
let c = self.world_cache.borrow();
c.as_ref().and_then(|c| {
let b = &c.build;
(i < b.node_level.len()).then(|| (b.node_level[i], b.node_repo[i].clone()))
})
};
if let Some((lvl, repo)) = info {
self.world_drill(i, lvl, &repo);
}
return;
}
if let Some(id) = v.get("drill").and_then(|d| d.as_str()) {
self.drill(id);
return;
}
if v.get("up").and_then(|d| d.as_bool()) == Some(true) {
self.up();
return;
}
if v.get("home").and_then(|d| d.as_bool()) == Some(true)
|| v.get("reset").and_then(|d| d.as_bool()) == Some(true)
{
self.home();
}
}
}
}
/// The navigator over the tests' fixture as a `Box<dyn Panel>` (feature `fixtures`).
#[cfg(any(test, feature = "fixtures"))]
pub fn panel() -> Box<dyn Panel> {
Box::new(WarehouseNav::from_fixture())
}
// ── 3D galaxy layout + camera projection (pure) ────────────────────────────────
//
// IMPORTED from the ungated `facett_graphview::camera3d` — the shared home this
// module's own header promised to defer to "once both land". Both landed: the
// CPU ports of `galaxy_3d` + `fib_sphere` + `project_azimuth` that used to live
// here (carried only because the math sat inside facett's `gpu`-gated
// `world_still`) are deleted (LAW 5).
use facett_graphview::camera3d::galaxy_3d;
/// **Camera-only orthographic projection** of a `[-1,1]³` galaxy point — the shared
/// `facett_graphview::camera3d::project_azimuth`, flattened to the `[x, y]` this
/// pane consumes (it has no depth cue; the funnel's 3D DAG uses the third
/// component).
fn project_azimuth(p: [f32; 3], yaw: f32, pitch: f32) -> [f32; 2] {
let [x, y, _depth] = facett_graphview::camera3d::project_azimuth(p, yaw, pitch);
[x, y]
}
// ── the UNIVERSE GPU star-field lane ───────────────────────────────────────────
//
// A drop-in for `facett_graphpan::draw_graph` that routes the node/edge cloud through the
// SHARED instanced GPU renderer (`paint_scene_opts` with `gpu_cloud` — the same cloud
// `facett_graphview::world_still` rasterises headless for the 100k/1M still) instead of
// tessellating every disc on the egui UI thread, and returns the `LegibilityReport` whose
// `gpu_cloud` flag names the lane that drew. Interaction (pan/drag + scroll-zoom +
// click-to-drill through the spatial pick index) is identical to `draw_graph`.
//
// This is written against `facett_graphpan`'s STABLE public surface (`paint_scene_opts` +
// `PickIndex`) so the dwarves crate builds against facett `main`; the facett
// `feat/world-universe` branch adds `draw_graph_opts` as the one-call home this collapses
// into once that lands.
#[allow(clippy::too_many_arguments)]
fn draw_universe_cloud(
ui: &mut egui::Ui,
model: &GraphModel,
decorations: &Decorations,
view: &mut GraphView,
pick: &mut PickIndex,
bg: Color32,
text: Color32,
selection_ring: Color32,
text_dim: Color32,
) -> (facett_graphpan::GraphResponse, LegibilityReport) {
if view.zoom <= 0.0 {
view.zoom = 1.0;
}
// the shared canvas size: what is left, never laid out past the visible clip (a
// canvas wider than the screen centred the galaxy off to the right, and made a GPU
// offscreen past the device limit)
let (resp, painter) = ui.allocate_painter(facett_core::nav::canvas_size(ui), egui::Sense::click_and_drag());
painter.rect_filled(resp.rect, 4.0, bg);
// a flight under way (🎯 Focus) advances; a drag or a scroll takes over from it
facett_graphpan::camera_step(ui, view, &resp);
if resp.dragged() {
view.pan += resp.drag_delta();
}
if resp.hovered() {
let scroll = ui.input(|i| i.smooth_scroll_delta.y);
if scroll != 0.0 {
view.zoom = (view.zoom * (1.0 + scroll * 0.001)).clamp(0.25, 4.0);
}
}
let origin = resp.rect.center() + view.pan;
// Click-to-drill through the spatial index (a neighbourhood query, not an O(n) scan —
// essential at 100k/1M). Same select/clear semantics as `draw_graph`.
let mut out = facett_graphpan::GraphResponse::default();
if let Some(click) = resp
.clicked()
.then(|| resp.interact_pointer_pos())
.flatten()
{
pick.ensure(model, origin, view.zoom);
match pick.pick(click, view.zoom) {
Some(i) => {
let id = model.nodes[i].id.clone();
view.selected = Some(id.clone());
out.clicked_node = Some(id);
}
None => {
view.selected = None;
out.clicked_empty = true;
}
}
}
// UNIVERSE = pure star-field: GPU instanced cloud ON, label/edge thinning OFF (no text at
// all — labels would be noise at 100k+). The GPU lane is fail-safe: with no installed
// renderer `paint_scene_opts` transparently runs the CPU dot painter and reports
// `gpu_cloud == false`, so a GPU-less box still renders every node.
let opts = PaintOpts::default().with_gpu(true);
let report = paint_scene_opts(
&painter,
model,
decorations,
view,
origin,
text,
selection_ring,
text_dim,
opts,
Some((ui, resp.rect)),
);
// 🎯 Focus: fly the clicked star to the centre; 📌 Stay: the camera never moves
facett_graphpan::focus_click(ui, view, model, &resp, out.clicked_node.as_deref());
(out, report)
}
// ── geometry helpers ──────────────────────────────────────────────────────────
/// Where a frame drew the graph (see [`WarehouseNav::screen_box`]).
#[derive(Clone, Debug)]
struct Shot {
centres: Vec<(String, Pos2)>,
half: egui::Vec2,
seen: Rect,
}
impl Shot {
fn of(model: &GraphModel, view: &GraphView, canvas: Rect, ui: &egui::Ui) -> Self {
let origin = canvas.center() + view.pan;
let half = match view.dot_px {
Some(r) => egui::Vec2::splat(r),
None => egui::Vec2::new(BOX_W, BOX_H) * 0.5 * view.zoom,
};
let seen = canvas.intersect(ui.clip_rect());
Self {
centres: model.nodes.iter().map(|n| (n.id.clone(), view.project(origin, n.pos))).collect(),
half,
seen: if seen.width() > 1.0 && seen.height() > 1.0 { seen } else { canvas },
}
}
}
/// The world box of a model's chips (centres ± half a chip at zoom 1, or the centres
/// alone for a dot cloud).
fn model_bounds(model: &GraphModel, dots: bool) -> Option<(Pos2, Pos2)> {
let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
let (hw, hh) = if dots { (0.0, 0.0) } else { (BOX_W * 0.5, BOX_H * 0.5) };
for n in &model.nodes {
x0 = x0.min(n.pos.x - hw);
y0 = y0.min(n.pos.y - hh);
x1 = x1.max(n.pos.x + hw);
y1 = y1.max(n.pos.y + hh);
}
(x1 >= x0).then(|| (Pos2::new(x0, y0), Pos2::new(x1, y1)))
}
/// Re-centre a set of `(name, pos)` on world (0,0) (graphpan centres the graph on
/// origin, so the mean must sit there).
fn center(raw: &mut [(String, Pos2)]) {
if raw.is_empty() {
return;
}
let (mut sx, mut sy) = (0.0, 0.0);
for (_, p) in raw.iter() {
sx += p.x;
sy += p.y;
}
let (cx, cy) = (sx / raw.len() as f32, sy / raw.len() as f32);
for (_, p) in raw.iter_mut() {
p.x -= cx;
p.y -= cy;
}
}
/// Position `i` of `n` on a ring of radius `r` centred on origin (crate clusters).
fn ring_pos(i: usize, n: usize, r: f32) -> Pos2 {
if n <= 1 {
return Pos2::new(0.0, -r);
}
let a = (i as f32 / n as f32) * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2;
Pos2::new(a.cos() * r, a.sin() * r)
}
/// Position `i` of `n` stacked vertically at world-x `x` (neighbour repo columns).
fn column_pos(x: f32, i: usize, n: usize) -> Pos2 {
let y = (i as f32 - (n.saturating_sub(1) as f32) / 2.0) * ROW_H;
Pos2::new(x, y)
}
/// Convex hull (monotone chain) of `pts`, each vertex pushed `margin` outward from
/// the centroid so the boundary encircles the chips rather than cutting through
/// them. Fewer than 3 unique points → a small box around the mean.
fn expanded_hull(pts: &[Pos2], margin: f32) -> Vec<Pos2> {
if pts.is_empty() {
return Vec::new();
}
let cx = pts.iter().map(|p| p.x).sum::<f32>() / pts.len() as f32;
let cy = pts.iter().map(|p| p.y).sum::<f32>() / pts.len() as f32;
let centroid = Pos2::new(cx, cy);
let mut hull = convex_hull(pts);
if hull.len() < 3 {
// Degenerate (1–2 members): a padded box around the centroid.
let m = margin + BOX_W * 0.6;
return vec![
Pos2::new(cx - m, cy - m * 0.5),
Pos2::new(cx + m, cy - m * 0.5),
Pos2::new(cx + m, cy + m * 0.5),
Pos2::new(cx - m, cy + m * 0.5),
];
}
for p in hull.iter_mut() {
let d = *p - centroid;
let len = d.length().max(1.0);
// Push out past the chip half-extent + the margin.
let out = len + margin + BOX_W * 0.4;
*p = centroid + d / len * out;
}
hull
}
/// Andrew's monotone-chain convex hull. Returns the hull vertices CCW.
fn convex_hull(input: &[Pos2]) -> Vec<Pos2> {
let mut pts: Vec<Pos2> = input.to_vec();
pts.sort_by(|a, b| {
a.x.partial_cmp(&b.x)
.unwrap()
.then(a.y.partial_cmp(&b.y).unwrap())
});
pts.dedup_by(|a, b| (a.x - b.x).abs() < 0.01 && (a.y - b.y).abs() < 0.01);
let n = pts.len();
if n < 3 {
return pts;
}
let cross = |o: Pos2, a: Pos2, b: Pos2| (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
let mut lower: Vec<Pos2> = Vec::new();
for &p in &pts {
while lower.len() >= 2 && cross(lower[lower.len() - 2], lower[lower.len() - 1], p) <= 0.0 {
lower.pop();
}
lower.push(p);
}
let mut upper: Vec<Pos2> = Vec::new();
for &p in pts.iter().rev() {
while upper.len() >= 2 && cross(upper[upper.len() - 2], upper[upper.len() - 1], p) <= 0.0 {
upper.pop();
}
upper.push(p);
}
lower.pop();
upper.pop();
lower.extend(upper);
lower
}
/// Trim a real function name for a chip label (keep it short; ellipsis the tail).
fn truncate_label(s: &str) -> String {
const MAX: usize = 24;
if s.chars().count() <= MAX {
s.to_string()
} else {
let mut out: String = s.chars().take(MAX - 1).collect();
out.push('…');
out
}
}
/// Mix a colour toward white by `k` (0..1) — the emissive brighten for glow/rings.
fn lighten(c: Color32, k: f32) -> Color32 {
let m = |v: u8| -> u8 { (v as f32 + (255.0 - v as f32) * k.clamp(0.0, 1.0)).round() as u8 };
Color32::from_rgb(m(c.r()), m(c.g()), m(c.b()))
}
// ── the faithful nordisk fixture ──────────────────────────────────────────────
/// A faithful offline snapshot of the nordisk warehouse: the real repos, a real
/// slice of each repo's produced crates, and real cross-repo edges — so the landing
/// tests render with no server. Feature `fixtures` only: no shipped build links it —
/// the shipped host feeds the skeleton from its server ([`SkeletonSource`]) and draws
/// the source's state when it has no answer.
///
/// Returns `(facts, edges, recency, bugs)`.
#[cfg(any(test, feature = "fixtures"))]
#[allow(clippy::type_complexity)]
pub fn fixture_snapshot() -> (
BTreeMap<String, RepoFacts>,
Vec<CrossRepoEdge>,
BTreeMap<String, f32>,
BTreeSet<String>,
) {
fn facts(name: &str, produces: &[&str]) -> (String, RepoFacts) {
let mut f = RepoFacts {
name: name.to_string(),
..Default::default()
};
f.produces = produces.iter().map(|s| s.to_string()).collect();
(name.to_string(), f)
}
fn set(items: &[&str]) -> BTreeSet<String> {
items.iter().map(|s| s.to_string()).collect()
}
let facts: BTreeMap<String, RepoFacts> = [
facts(
"edda",
&[
"jera",
"nornir-git",
"nornir-service",
"nornir-testmatrix",
"nornir-release",
"nornir-claude",
"nornir-depgraph",
"nornir-robotui",
"autonom",
"saga",
],
),
facts(
"nornir",
&[
"nornir",
"nornir-build-thing",
"nornir-viz-web",
"nornir-jobs",
],
),
facts(
"dwarves",
&[
"dwarves",
"dwarves-embed",
"nornir-funnel-embed",
"nornir-warehouse-embed",
],
),
facts(
"facett",
&[
"facett-core",
"facett-graphview",
"facett-graphpan",
"facett-plandag",
"facett-brokkrview",
"facett-jobview",
],
),
facts("holger", &["holger", "holger-server-lib"]),
facts("funnel-core", &["funnel-core"]),
facts("modgunn", &["modgunn"]),
facts("skade", &["skade"]),
facts("znippy", &["znippy-common", "znippy-compress"]),
facts("znippy-zoomies", &["stree", "gatling"]),
facts("Skidbladnir", &["nornir-airgap"]),
facts("korp", &["korp"]),
]
.into_iter()
.collect();
let edges = vec![
CrossRepoEdge::normal(
"nornir",
"edda",
set(&["jera", "nornir-git", "nornir-release"]),
),
CrossRepoEdge::normal("nornir", "modgunn", set(&["modgunn"])),
CrossRepoEdge::normal("nornir", "funnel-core", set(&["funnel-core"])),
CrossRepoEdge::normal("nornir", "facett", set(&["facett-core"])),
CrossRepoEdge::normal("nornir", "Skidbladnir", set(&["nornir-airgap"])),
CrossRepoEdge::normal(
"dwarves",
"edda",
set(&["nornir-service", "nornir-testmatrix", "nornir-depgraph"]),
),
CrossRepoEdge::normal(
"dwarves",
"facett",
set(&["facett-core", "facett-graphpan", "facett-brokkrview"]),
),
CrossRepoEdge::normal("dwarves", "nornir", set(&["nornir-build-thing"])),
CrossRepoEdge::normal("holger", "edda", set(&["nornir-service"])),
CrossRepoEdge::normal("holger", "znippy", set(&["znippy-common"])),
CrossRepoEdge {
from: "facett".into(),
to: "edda".into(),
via: set(&["nornir-testmatrix"]),
kind: DepKind::Dev,
},
CrossRepoEdge::normal("skade", "znippy-zoomies", set(&["stree"])),
CrossRepoEdge::normal("korp", "facett", set(&["facett-core"])),
CrossRepoEdge::normal("korp", "edda", set(&["jera"])),
];
// Recency (0 old .. 1 fresh) — newest repos glow brightest. Recency is GLOW, not
// hue. A few fresh crates too.
let recency: BTreeMap<String, f32> = [
("dwarves", 1.0),
("facett", 0.95),
("edda", 0.9),
("nornir", 0.85),
("nornir-warehouse-embed", 0.95),
("facett-graphpan", 0.9),
("nornir-depgraph", 0.85),
("holger", 0.5),
("korp", 0.4),
("skade", 0.3),
("funnel-core", 0.3),
("znippy", 0.25),
("modgunn", 0.2),
("znippy-zoomies", 0.2),
("Skidbladnir", 0.15),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
// One demo glitch anchor (a reported problem) — exercises the reserved alert hue.
let bugs: BTreeSet<String> = set(&["saga"]);
(facts, edges, recency, bugs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glow_curve_is_recency_only_and_bounded() {
assert_eq!(glow_alpha(0.0), 0.0);
assert!(glow_alpha(1.0) > glow_alpha(0.5), "fresher glows brighter");
assert!(glow_alpha(2.0) <= 0.56, "clamped");
}
/// Skeleton PROVENANCE reads honestly: the fixture path names itself "fixture …",
/// a host override (the live `build_from_members` path) replaces it, and both are
/// visible on the robot surface. RED-when-broken: if `state_json` dropped the
/// field, or `from_fixture` stopped naming itself, a live host could ship a stale
/// fixture skeleton that reads as live.
#[test]
fn skeleton_source_is_fixture_by_default_and_host_overridable() {
let nav = WarehouseNav::from_fixture();
let s = Facet::state_json(&nav);
assert!(
s["skeleton_source"]
.as_str()
.unwrap()
.starts_with("fixture"),
"the fixture names itself: {}",
s["skeleton_source"]
);
let (facts, edges, recency, bugs) = fixture_snapshot();
let nav = WarehouseNav::new(
WorkspaceGraph::from_query_parts(facts, edges),
recency,
bugs,
)
.with_skeleton_source("live: 18 member repos @ /home/x/git");
let s = Facet::state_json(&nav);
assert_eq!(s["skeleton_source"], "live: 18 member repos @ /home/x/git");
}
/// The LANDING page (L0) renders the whole constellation: one node per repo,
/// the cross-repo edges, colour-by-repo, and NO hull yet. RED-when-broken.
#[test]
fn landing_is_the_constellation() {
let nav = WarehouseNav::from_fixture();
let s = Facet::state_json(&nav);
assert_eq!(s["kind"], "warehouse-nav");
assert_eq!(s["level"], "constellation");
assert_eq!(s["repo_count"], 12, "one super-node per nordisk repo");
assert_eq!(s["crate_count"], 0, "no crates at L0");
assert_eq!(s["hull_count"], 0, "hulls appear only when a repo opens");
assert!(
s["edge_count"].as_u64().unwrap() >= 10,
"cross-repo edges present"
);
// Colour-by-repo: every repo has a distinct-per-slot hex in the palette map.
let pal = s["palette"].as_object().unwrap();
assert_eq!(pal.len(), 12);
assert_eq!(pal["dwarves"].as_str().unwrap().len(), 7, "#rrggbb");
assert_ne!(pal["nornir"], pal["edda"], "different repos, different hue");
}
/// CLICK-TO-DRILL-DOWN + REPO HULL: descend constellation → repo. The repo's
/// crates appear inside exactly one grouping hull, coloured in the repo's family.
#[test]
fn drill_into_repo_shows_crates_in_a_hull() {
let mut nav = WarehouseNav::from_fixture();
// Drive the SAME path a chip click takes.
nav.drill("repo:facett");
let s = Facet::state_json(&nav);
assert_eq!(s["level"], "repo");
assert_eq!(s["focus_repo"], "facett");
assert_eq!(
s["hull_count"], 1,
"the repo is drawn as one encircling hull"
);
// facett produces 6 crates in the fixture → 6 crate chips + the anchor.
assert_eq!(s["crate_count"], 6);
// The hull groups the anchor + its crates.
let hull = &s["hulls"][0];
assert_eq!(hull["repo"], "facett");
assert!(hull["members"].as_array().unwrap().len() >= 7);
// Neighbour repos are present too (dwarves/korp/nornir depend on facett).
assert!(
s["repo_count"].as_u64().unwrap() >= 2,
"focus anchor + neighbour repos"
);
}
/// A second drill step: repo → crate (L2), then climb back up twice to L0.
#[test]
fn drill_repo_then_crate_then_climb_back() {
let mut nav = WarehouseNav::from_fixture();
nav.drill("repo:dwarves");
assert_eq!(Facet::state_json(&nav)["level"], "repo");
nav.drill("crate:dwarves-embed");
let s = Facet::state_json(&nav);
assert_eq!(s["level"], "crate");
assert_eq!(s["focus_repo"], "dwarves");
assert_eq!(s["focus_crate"], "dwarves-embed");
assert_eq!(
s["hull_count"], 1,
"the repo's crates are still hulled at L2"
);
// Climb: crate → repo → constellation.
nav.up();
assert_eq!(Facet::state_json(&nav)["level"], "repo");
nav.up();
assert_eq!(Facet::state_json(&nav)["level"], "constellation");
}
/// The reserved BUG hue is applied to a glitch node and never to a repo slot.
#[test]
fn bug_node_wears_the_reserved_alert_hue() {
let mut nav = WarehouseNav::from_fixture();
nav.drill("repo:edda"); // `saga` is a fixture glitch anchor
let s = Facet::state_json(&nav);
let saga = s["nodes"]
.as_array()
.unwrap()
.iter()
.find(|n| n["id"] == "crate:saga")
.expect("saga crate present");
assert_eq!(saga["bug"], true);
assert_eq!(
saga["color"], s["bug_hue"],
"a glitch wears the reserved alert hue"
);
}
/// WORLD GRAPH toggle (the robot seam): flipping world mode on switches the surface
/// to the LOD (repos→crates→functions) graph. With no function tier fed, the world
/// is the skeleton's repos and crates and NOTHING else: no filler node appears.
#[test]
fn world_toggle_switches_to_the_lod_surface_and_draws_no_filler() {
let mut nav = WarehouseNav::from_fixture();
// Off by default → the normal constellation.
assert_eq!(Facet::state_json(&nav)["level"], "constellation");
// Ask for the full 10M cap, then toggle world on.
Facet::update_json(&mut nav, r#"{"world_target":10000000}"#);
Facet::update_json(&mut nav, r#"{"world":true}"#);
let s = Facet::state_json(&nav);
assert_eq!(s["level"], "world");
assert_eq!(s["lod"], "functions");
assert_eq!(s["world_target"], 10_000_000u64);
// 10M is past the force budget → the lane at that cap is the O(n) placement.
assert_eq!(s["planned_lane"], "hier_grid");
// The cap is a cap, not a quota: 12 repos + their 32 crates, and not one node more.
assert_eq!(
(
s["preview_repo_nodes"].as_u64(),
s["preview_fn_nodes"].as_u64()
),
(Some(12), Some(0))
);
assert_eq!(
s["preview_nodes"].as_u64().unwrap(),
12 + s["preview_crate_nodes"].as_u64().unwrap()
);
assert_eq!(s["preview_model_nodes"], s["preview_nodes"]);
assert_eq!(
(s["real_fn_nodes"].as_u64(), s["real_fn_crates"].as_u64()),
(Some(0), Some(0))
);
assert_eq!(s["crates_without_fns"], s["preview_crate_nodes"]);
assert!(
s.get("synthetic_functions").is_none() && s.get("synthetic_fn_nodes").is_none(),
"there is no synthetic tier to report: {s}"
);
// Every repo → repo edge drawn is one of the skeleton's 14 dependency edges.
assert_eq!(s["repo_dep_edges"], 14);
assert_eq!(
s["preview_edges"].as_u64().unwrap(),
s["preview_crate_nodes"].as_u64().unwrap() + 14,
"containment + the skeleton's dependency edges, nothing invented"
);
// Toggle back off → the drill-down surface returns.
Facet::update_json(&mut nav, r#"{"world":false}"#);
assert_eq!(Facet::state_json(&nav)["level"], "constellation");
}
/// A small `world_target` (≤ the force budget) picks the Barnes–Hut force lane.
#[test]
fn world_small_target_uses_the_barnes_hut_lane() {
let mut nav = WarehouseNav::from_fixture();
Facet::update_json(&mut nav, r#"{"world_target":100000}"#);
Facet::update_json(&mut nav, r#"{"world":true}"#);
let s = Facet::state_json(&nav);
assert_eq!(
s["planned_lane"], "barnes_hut",
"≤ bh_max → a real force layout"
);
// The bounded preview itself is small enough to lay out with Barnes–Hut too.
assert_eq!(s["preview_lane"], "barnes_hut");
}
fn two_fns() -> CrateFuncGraph {
use nornir_depgraph::funcgraph::{FnEdge, FnNode};
CrateFuncGraph {
crate_name: "nornir-depgraph".into(),
nodes: vec![
FnNode {
id: "edda::nornir-depgraph::crate::build".into(),
name: "build".into(),
crate_name: "nornir-depgraph".into(),
},
FnNode {
id: "edda::nornir-depgraph::crate::layout".into(),
name: "layout".into(),
crate_name: "nornir-depgraph".into(),
},
],
edges: vec![FnEdge {
from: "edda::nornir-depgraph::crate::build".into(),
to: "edda::nornir-depgraph::crate::layout".into(),
}],
}
}
/// **The function tier is exactly what was fed.** One crate's function graph in ⇒
/// exactly its two fns and its one call edge in the world, by name; every other
/// crate has no function under it.
#[test]
fn the_world_draws_exactly_the_functions_it_was_given() {
// `edda` produces `nornir-depgraph` in the fixture.
let mut nav = WarehouseNav::from_fixture().with_real_functions("edda", vec![two_fns()]);
Facet::update_json(&mut nav, r#"{"world_target":100000}"#);
Facet::update_json(&mut nav, r#"{"world":true}"#);
let s = Facet::state_json(&nav);
assert_eq!(
(s["real_fn_nodes"].as_u64(), s["preview_fn_nodes"].as_u64()),
(Some(2), Some(2)),
"the two fns, and no third"
);
assert_eq!(s["real_call_edges"], 1, "the call edge resolved");
assert_eq!(
s["real_fn_crates"], 1,
"exactly the one crate has a function tier"
);
assert_eq!(
s["crates_without_fns"].as_u64().unwrap(),
s["preview_crate_nodes"].as_u64().unwrap() - 1
);
// The fn NAMES are the chip labels in the lowered model, and no chip is unnamed.
let wb = nav.build_world(true, WORLD_PREVIEW_CAP);
let fn_labels: Vec<&str> = wb
.model
.nodes
.iter()
.zip(&wb.node_level)
.filter(|(_, l)| **l == 2)
.map(|(n, _)| n.label.as_str())
.collect();
assert_eq!(fn_labels, vec!["build", "layout"]);
}
/// A crate graph with ZERO functions adds nothing — not a tier, not a filler node.
#[test]
fn an_empty_crate_graph_adds_no_function() {
let empty = CrateFuncGraph {
crate_name: "nornir-depgraph".into(),
nodes: vec![],
edges: vec![],
};
let mut nav = WarehouseNav::from_fixture().with_real_functions("edda", vec![empty]);
Facet::update_json(&mut nav, r#"{"world":true}"#);
let s = Facet::state_json(&nav);
assert_eq!(
(
s["real_fn_nodes"].as_u64(),
s["preview_fn_nodes"].as_u64(),
s["real_fn_crates"].as_u64()
),
(Some(0), Some(0), Some(0))
);
}
/// **A live [`RealFnSource`] populates the tier on first world build.** The
/// injected source is consulted exactly once (lazily, when world mode opens).
#[test]
fn injected_source_populates_the_function_tier_on_first_world_build() {
struct MockSource;
impl RealFnSource for MockSource {
fn fetch(&self) -> Result<Vec<(String, Vec<CrateFuncGraph>)>, String> {
Ok(vec![("edda".to_string(), vec![two_fns()])])
}
fn describe(&self) -> String {
"mock source".into()
}
}
let mut nav = WarehouseNav::from_fixture().with_real_source(Box::new(MockSource));
// Before world mode: the source has NOT been consulted (landing pays no cost).
assert!(
!nav.real_scanned,
"source untouched until the world is shown"
);
Facet::update_json(&mut nav, r#"{"world_target":100000}"#);
Facet::update_json(&mut nav, r#"{"world":true}"#);
let s = Facet::state_json(&nav);
assert_eq!(s["real_source"], true, "a source is wired");
assert_eq!(s["real_scanned"], true, "consulted on first world build");
assert_eq!(
s["real_error"],
serde_json::Value::Null,
"no error on a good fetch"
);
assert_eq!(
(s["real_fn_nodes"].as_u64(), s["real_call_edges"].as_u64()),
(Some(2), Some(1))
);
assert_eq!(s["real_source_desc"], "mock source");
// Re-reading state_json many times does NOT re-fetch (the one-shot guard).
for _ in 0..5 {
let _ = Facet::state_json(&nav);
}
Facet::update_json(&mut nav, r#"{"world":false}"#);
Facet::update_json(&mut nav, r#"{"world":true}"#);
assert!(nav.real_scanned);
}
/// A source that cannot answer leaves NO function tier and its sentence on the
/// surface — and a tier an earlier answer gave is dropped with it.
#[test]
fn a_source_that_cannot_answer_leaves_no_function_tier() {
struct Refusing;
impl RealFnSource for Refusing {
fn fetch(&self) -> Result<Vec<(String, Vec<CrateFuncGraph>)>, String> {
Err("[not_consumed] code room of `w`: no repo has a consumer offset yet".into())
}
fn describe(&self) -> String {
"refusing source".into()
}
}
let mut nav = WarehouseNav::from_fixture()
.with_real_functions("edda", vec![two_fns()])
.with_real_source(Box::new(Refusing));
Facet::update_json(&mut nav, r#"{"world":true}"#);
let s = Facet::state_json(&nav);
// the earlier tier is gone and nothing stands in: the tier was NOT read, so it has
// no count at all (null) — while the skeleton, which was read, keeps its own
assert!(
s["real_fn_nodes"].is_null()
&& s["preview_fn_nodes"].is_null()
&& s["real_call_edges"].is_null(),
"{s}"
);
assert_eq!(
(
s["functions_read"].as_bool(),
s["skeleton_read"].as_bool(),
s["preview_repo_nodes"].as_u64()
),
(Some(false), Some(true), Some(12)),
"{s}"
);
assert_eq!(
s["real_error"],
"[not_consumed] code room of `w`: no repo has a consumer offset yet"
);
}
/// **The skeleton is the source's.** A navigator made with no skeleton draws none
/// until its source answers; then it draws exactly the repos, crates and edges the
/// source gave, under the source's workspace as the breadcrumb root.
#[test]
fn the_skeleton_is_what_the_source_answers() {
struct Two;
impl SkeletonSource for Two {
fn fetch(&self) -> Result<Skeleton, String> {
let facts = |name: &str, produces: &[&str]| {
(
name.to_string(),
RepoFacts {
name: name.to_string(),
produces: produces.iter().map(|c| c.to_string()).collect(),
..Default::default()
},
)
};
Ok(Skeleton {
root: "shop-ws".into(),
facts: [
facts("shop", &["shop", "shop-core"]),
facts("web", &["web"]),
]
.into(),
edges: vec![CrossRepoEdge::normal(
"web",
"shop",
["shop".to_string()].into(),
)],
source: "Viz.CrateGraph @ test · workspace shop-ws · shop@2, web@1".into(),
})
}
fn describe(&self) -> String {
"two-repo source".into()
}
}
let mut nav = WarehouseNav::empty("shop-ws").with_skeleton_feed(Box::new(Two));
let before = Facet::state_json(&nav);
assert_eq!(before["skeleton_source"], "not read yet");
assert!(
before["repo_count"].is_null()
&& before["node_count"].is_null()
&& before["skeleton_read"] == false,
"no count before a read: {before}"
);
Facet::update_json(&mut nav, r#"{"read":true}"#);
let s = Facet::state_json(&nav);
assert_eq!(
(s["repo_count"].as_u64(), s["edge_count"].as_u64()),
(Some(2), Some(1))
);
assert_eq!(s["breadcrumb"], serde_json::json!(["shop-ws"]));
assert_eq!(
s["skeleton_source"],
"Viz.CrateGraph @ test · workspace shop-ws · shop@2, web@1"
);
assert!(s["skeleton_error"].is_null());
Facet::update_json(&mut nav, r#"{"drill":"repo:shop"}"#);
let s = Facet::state_json(&nav);
assert_eq!(
(s["focus_repo"].as_str(), s["crate_count"].as_u64()),
(Some("shop"), Some(2)),
"{s}"
);
}
/// A skeleton source that cannot answer leaves NO graph and its sentence — and a
/// graph an earlier answer gave is dropped.
#[test]
fn a_skeleton_source_that_cannot_answer_draws_no_graph() {
struct Flaky(std::sync::atomic::AtomicBool);
impl SkeletonSource for Flaky {
fn fetch(&self) -> Result<Skeleton, String> {
if self.0.swap(false, std::sync::atomic::Ordering::SeqCst) {
let f = RepoFacts {
name: "shop".into(),
produces: ["shop".to_string()].into(),
..Default::default()
};
return Ok(Skeleton {
root: "w".into(),
facts: [("shop".to_string(), f)].into(),
edges: vec![],
source: "first answer".into(),
});
}
Err("[no_room] no workspace selected".into())
}
fn describe(&self) -> String {
"flaky".into()
}
}
let mut nav = WarehouseNav::empty("w").with_skeleton_feed(Box::new(Flaky(true.into())));
Facet::update_json(&mut nav, r#"{"read":true}"#);
assert_eq!(Facet::state_json(&nav)["repo_count"], 1);
Facet::update_json(&mut nav, r#"{"reread":true}"#);
let s = Facet::state_json(&nav);
assert!(
s["repo_count"].is_null() && s["node_count"].is_null() && s["edge_count"].is_null(),
"the earlier graph is gone and nothing is counted: {s}"
);
assert_eq!(s["skeleton_read"], false);
assert_eq!(s["nodes"], serde_json::json!([]));
assert_eq!(s["skeleton_error"], "[no_room] no workspace selected");
}
/// The generated test tier is what it says: `total` fns, named as fixtures.
#[test]
fn the_generated_test_tier_has_the_asked_size_and_fixture_names() {
let mut nav = WarehouseNav::from_fixture_with_functions(500);
Facet::update_json(&mut nav, r#"{"world_target":100000}"#);
Facet::update_json(&mut nav, r#"{"world":true}"#);
let s = Facet::state_json(&nav);
assert_eq!(s["real_fn_nodes"], 500);
let wb = nav.build_world(true, WORLD_PREVIEW_CAP);
assert!(wb
.node_fn
.iter()
.flatten()
.all(|n| n.starts_with("fixture_fn_")));
}
/// The `update_json` seam drives the whole drill-down headless (the robot path).
#[test]
fn update_json_seam_drives_drilldown() {
let mut nav = WarehouseNav::from_fixture();
Facet::update_json(&mut nav, r#"{"drill":"repo:nornir"}"#);
assert_eq!(Facet::state_json(&nav)["focus_repo"], "nornir");
Facet::update_json(&mut nav, r#"{"up":true}"#);
assert_eq!(Facet::state_json(&nav)["level"], "constellation");
Facet::update_json(&mut nav, r#"{"drill":"repo:holger"}"#);
Facet::update_json(&mut nav, r#"{"home":true}"#);
assert_eq!(Facet::state_json(&nav)["level"], "constellation");
}
}