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

nordisk/dwarves

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");
    }
}