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

nordisk/dwarves

dwarves-gui/src/coverage.rs

//! **The 📐 Coverage tab — the coverage index.**
//!
//! dwarves' answer to *"is this actually finished?"*, and the UI surface of
//! [`dwarves::coverage`]. L3 says a feature is not done without a test AND a heavy
//! bench; this pane is where you see how far from that the constellation is.
//!
//! The pane owns **no coverage logic**. Every gap, every rank, every host verdict
//! comes from the `dwarves::coverage` engine, which `dwarves coverage` drives
//! through the identical [`dwarves::coverage::build`] call — so the tab and the
//! command line can never disagree about what a gap is. This file is the read seam
//! plus the render.
//!
//! ## Three views, kept structurally apart
//! * **Repos** — per repo: functions no test can reach, hot paths with no bench.
//! * **Bench integrity** — one table per HOST. `loki · 12 cores` and `oden · 32
//!   cores` are not comparable, so they are never in the same table, and a
//!   `<host>-preview` row is badged unpublishable. This separation is structural,
//!   not stylistic: it is the failure this constellation keeps having to retract.
//! * **Cannot-go-red** — tests that have never once failed over changing code, and,
//!   above them, the plain statement of how much history that verdict rests on.
//!
//! ## What the pane must never do
//! **Render an empty index as a clean bill of health.** Every view leads with the
//! engine's `honest_headline()`, which names the MISSING FEEDS by table. A repo the
//! warehouse never scanned shows `NOT SCANNED`, never `0 gaps`. A weak-test verdict
//! with no history shows the evidence note, never an empty "all good" list.
//!
//! ## Read seam
//! Two halves, both through the forge, both off the UI thread:
//!
//! * the CODE GRAPH — which fns exist, which a test reaches, the call sites arriving,
//!   what each test stands on — is ONE question to nornir's code room
//!   (`Knowledge.TestReach`, [`crate::room`]). The walk is the room's; nothing here or
//!   in the engine walks an edge. Its answer's state leads the pane: a room that did
//!   not compute draws its state and its sentence, and no repo row.
//! * heat, bench runs / results and test outcomes are warehouse tables over the thin
//!   `Warehouse.Scan` seam, as before.
//!
//! The roster (the report's fixed denominator) is the workspace's member list as the
//! server registers it — a member the room has not consumed shows NOT SCANNED.
//!
//! ## No sample
//! With no reachable server the pane says UNAVAILABLE with the reason. It draws no
//! fixture: the demo index this file used to carry lives behind the `fixtures`
//! feature, which only the tests enable.

use std::collections::{BTreeMap, BTreeSet};

use dwarves::coverage::{build, rows::Table, CoverageIndex, GraphFeed, GRAPH_SURFACE, TABLES};
use facett_core::Facet;

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

/// Row cap per table on the thin read. The server honours a non-zero limit.
const SCAN_LIMIT: u32 = 800_000;

/// Seconds between warehouse-generation probes. A populate takes minutes; probing its
/// snapshot id every few seconds is prompt without being a poll loop.
const GENERATION_PROBE_S: f64 = 4.0;

/// One read of everything the index folds.
#[derive(Debug, Clone)]
pub struct Loaded {
    /// The code room's answer to [`GRAPH_SURFACE`] — by state.
    pub graph: RoomAnswer,
    /// The warehouse tables ([`TABLES`]).
    pub tables: BTreeMap<String, Table>,
    /// The report's fixed denominator: the workspace's members as the server
    /// registers them.
    pub roster: Vec<String>,
}

/// Where the pane's index comes from. Injected so a test drives the pane over a
/// deterministic reading with no server.
///
/// `Send + Sync` because the pane runs [`load`](Self::load) on a WORKER thread — the
/// read is a room question plus a gRPC round trip per table and must never happen on
/// the frame that is trying to draw. The `Arc` the pane holds is what the worker clones.
pub trait CoverageSource: Send + Sync {
    /// Read the room's answer and the tables — or the reason NOTHING could be read
    /// (the server did not answer). A room that answered with a refusal is `Ok`: its
    /// state is the reading.
    fn load(&self) -> Result<Loaded, String>;
    /// What to show in the header.
    fn describe(&self) -> String;
}

/// The shipped source: the forge — nornir's code room for the graph, its warehouse
/// for the tables, its registry for the roster.
pub struct WarehouseCoverage {
    pub endpoint: String,
    pub token: String,
    pub workspace: String,
}

impl WarehouseCoverage {
    pub fn new(
        endpoint: impl Into<String>,
        token: impl Into<String>,
        workspace: impl Into<String>,
    ) -> Self {
        Self {
            endpoint: endpoint.into(),
            token: token.into(),
            workspace: workspace.into(),
        }
    }

    /// One table over the wire.
    fn scan(&self, table: &str) -> Result<Table, String> {
        nornir_warehouse_embed::scan_warehouse_table(
            &self.endpoint,
            &self.token,
            &self.workspace,
            table,
            SCAN_LIMIT,
        )
        .map(|s| Table::new(s.columns, s.rows))
        .map_err(|e| e.to_string())
    }
}

impl CoverageSource for WarehouseCoverage {
    fn load(&self) -> Result<Loaded, String> {
        // The room's answer is the one read without which there is no surface at all.
        // When the question never reached a reader (server down, bad credentials) THAT
        // is the failure — an unreachable server must not look like an empty index.
        let graph = ForgeRoom::new(&self.endpoint, &self.token, &self.workspace)
            .ask(GRAPH_SURFACE, &serde_json::json!({}));
        if graph.kind == crate::room::RoomState::NotAsked {
            return Err(format!("[{}] {}", graph.state, graph.evidence_text()));
        }
        // Every table degrades on its own: a missing feed is REPORTED as missing by
        // the engine, which is a different statement from "no gaps".
        let tables = TABLES
            .iter()
            .map(|t| ((*t).to_string(), self.scan(t).unwrap_or_default()))
            .collect();
        // The denominator: the members the server registers for this workspace.
        let roster =
            nornir_warehouse_embed::get_warehouse(&self.endpoint, &self.token, &self.workspace)
                .map(|w| w.members)
                .unwrap_or_default();
        Ok(Loaded {
            graph,
            tables,
            roster,
        })
    }

    fn describe(&self) -> String {
        format!(
            "code room + warehouse @ {} / {}",
            self.endpoint, self.workspace
        )
    }
}

/// **A deterministic reading for the tests** (feature `fixtures`; no shipped build
/// enables it). Its SHAPE is the real 2026-07-22 reading of the nordisk warehouse, so
/// it exercises each finding class:
///
/// * `znippy` — benched, one arm, one fn a test reaches, one it does not.
/// * `korp` — in the room's answer but **never benched**.
/// * `bifrost` — a member, **not in the room's answer** at all.
/// * an `oden-preview` row next to an `oden` one (unpublishable vs publishable), and
///   a 4-core `ci` row that makes the repo multi-host.
#[cfg(any(test, feature = "fixtures"))]
pub struct FixtureCoverage;

#[cfg(any(test, feature = "fixtures"))]
impl FixtureCoverage {
    fn table(cols: &[&str], rows: &[&[&str]]) -> Table {
        Table::new(
            cols.iter().map(|s| s.to_string()).collect(),
            rows.iter()
                .map(|r| r.iter().map(|s| s.to_string()).collect())
                .collect(),
        )
    }

    /// The room's answer, in the reader's own shape.
    pub fn graph_body() -> serde_json::Value {
        let f = |repo: &str,
                 module: &str,
                 name: &str,
                 file: &str,
                 line: u32,
                 reachable: bool,
                 fan_in: u32| {
            serde_json::json!({
                "repo": repo, "crate_name": repo, "module": module, "item_name": name, "fqn": format!("{module}::{name}"),
                "kind": "fn", "visibility": "pub", "file": file, "line": line,
                "test_code": false, "test_reachable": reachable, "fan_in": fan_in,
            })
        };
        serde_json::json!({
            "state": "complete",
            "files": [{ "repo": "znippy", "file": "src/decode.rs" }],
            "fns": [
                f("znippy", "znippy::decode", "decode_block", "src/decode.rs", 40, true, 1),
                f("znippy", "znippy::vault", "seal", "src/vault.rs", 12, false, 0),
                f("korp", "korp::clip", "clip_poly", "src/clip.rs", 88, false, 1),
                f("korp", "korp::io", "load_pbf", "src/io.rs", 17, false, 0),
            ],
            "tests": [{
                "repo": "znippy", "crate_name": "znippy", "module": "znippy::tests", "test_name": "roundtrip", "fqn": "znippy::tests::roundtrip",
                "file": "src/decode.rs", "line": 300, "calls_out": 1, "names": 0, "reached": 1, "files": [0],
            }],
            "evidence": { "missing": [], "notes": ["calls through closures are not recorded: a lower bound"] },
            "room": {
                "workspace": "nordisk", "universe": false, "nodes": 4210, "edges_total": 9877,
                "repos": [{ "repo": "korp", "seq": 4, "sha": "aaaaaaaaaabbbbbbbbbb" }, { "repo": "znippy", "seq": 7, "sha": "ccccccccccdddddddddd" }],
            },
        })
    }

    fn tables() -> BTreeMap<String, Table> {
        let mut t: BTreeMap<String, Table> = BTreeMap::new();
        t.insert(
            "bench_runs".into(),
            Self::table(
                &[
                    "run_id",
                    "repo",
                    "ts_micros",
                    "date",
                    "version",
                    "machine",
                    "cores",
                ],
                &[
                    &[
                        "r1",
                        "znippy",
                        "1784000000000000",
                        "2026-07-22",
                        "0.9.6",
                        "oden",
                        "32",
                    ],
                    &[
                        "r2",
                        "znippy",
                        "1784000000000000",
                        "2026-07-22",
                        "0.9.6",
                        "oden-preview",
                        "32",
                    ],
                    &[
                        "r3",
                        "znippy",
                        "1782000000000000",
                        "2026-07-08",
                        "0.9.0",
                        "ci",
                        "4",
                    ],
                ],
            ),
        );
        t.insert(
            "bench_results".into(),
            Self::table(
                &["run_id", "result_name", "metric_name", "metric_value"],
                &[
                    &["r1", "znippy.decode_mt", "mb_s", "808"],
                    &["r2", "znippy.decode_mt", "mb_s", "790"],
                    &["r3", "znippy.legacy_arm", "mb_s", "410"],
                ],
            ),
        );
        t.insert(
            "test_outcomes".into(),
            Self::table(
                &["run_id", "test_name", "passed"],
                &[
                    &["r1", "znippy.decode_mt", "true"],
                    &["r2", "znippy.decode_mt", "true"],
                    &["r3", "znippy.decode_mt", "true"],
                ],
            ),
        );
        // `test_results` / `coverage_fn` / `surface_coverage` / `git_heat_facts` are
        // left ABSENT on purpose: the reading must exercise a MISSING FEED, which is
        // the real state of the warehouse and the thing the pane must never hide.
        t
    }
}

#[cfg(any(test, feature = "fixtures"))]
impl CoverageSource for FixtureCoverage {
    fn load(&self) -> Result<Loaded, String> {
        Ok(Loaded {
            graph: RoomAnswer::from_body(GRAPH_SURFACE, &self.describe(), Self::graph_body()),
            tables: Self::tables(),
            roster: vec!["bifrost".into(), "korp".into(), "znippy".into()],
        })
    }
    fn describe(&self) -> String {
        "fixture (tests) — a benched repo, a never-benched repo, an unscanned repo, a preview row"
            .to_string()
    }
}

/// Which slice of the index the pane is showing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum View {
    /// Per-repo test + bench coverage.
    Repos,
    /// The host-partitioned bench tables.
    Integrity,
    /// The cannot-go-red detector.
    Weak,
    /// What the warehouse actually held, per table.
    Feeds,
}

impl View {
    fn as_str(self) -> &'static str {
        match self {
            View::Repos => "repos",
            View::Integrity => "integrity",
            View::Weak => "weak",
            View::Feeds => "feeds",
        }
    }
    fn parse(s: &str) -> Option<Self> {
        match s {
            "repos" => Some(View::Repos),
            "integrity" => Some(View::Integrity),
            "weak" => Some(View::Weak),
            "feeds" => Some(View::Feeds),
            _ => None,
        }
    }
}

/// The **📐 Coverage** pane.
pub struct CoveragePane {
    title: String,
    /// The read seam. `Arc` so the off-thread worker clones it — the SAME shape the
    /// Dupes pane's `reader` and the Lenses pane's source have.
    source: std::sync::Arc<dyn CoverageSource>,
    /// A read in flight (worker receiver + when it started). The frame polls it; the
    /// robot seam blocks on it. The read runs OFF the egui thread so a cold
    /// `call_edges` scan (half a million rows) never freezes the window.
    pending: Option<(
        std::sync::mpsc::Receiver<Result<Loaded, String>>,
        std::time::Instant,
    )>,
    /// `None` until the first scan — the pane opens on an explicit, honest
    /// "not scanned yet" rather than blocking the shell on a warehouse read.
    index: Option<CoverageIndex>,
    /// The code room's answer behind `index` — drawn as the callout (state, evidence,
    /// the room it was read from) above every view.
    graph: Option<RoomAnswer>,
    /// Honest failure text from the last attempt (`None` ⇒ no failure).
    error: Option<String>,
    view: View,
    /// Show only repos that have a gap (default: show all, so a clean repo is
    /// visibly clean rather than merely absent).
    gaps_only: bool,
    /// Expanded repo.
    selected: Option<String>,
    /// The GENERATION this pane last read at, and the background probe that moves it.
    /// The pane reads itself when the warehouse's snapshot id changes — never on a
    /// press. `gen` is `None` for a fixture/injected source, which has no warehouse to
    /// probe: that pane simply does its one cold read (LAW 5 / `crate::generation`).
    watch: crate::generation::Watch,
    gen: Option<crate::generation::WarehouseGeneration>,
}

impl CoveragePane {
    /// The shipped pane, bound to the selected workspace over the thin gRPC seam.
    pub fn live(
        endpoint: impl Into<String>,
        token: impl Into<String>,
        workspace: impl Into<String>,
    ) -> Self {
        let (endpoint, token, workspace) = (endpoint.into(), token.into(), workspace.into());
        let mut me = Self::with_source(Box::new(WarehouseCoverage::new(
            endpoint.clone(),
            token.clone(),
            workspace.clone(),
        )));
        me.gen = Some(crate::generation::WarehouseGeneration::new(
            endpoint,
            token,
            workspace,
            GENERATION_PROBE_S,
        ));
        me
    }

    /// The pane over an injected source (the robot-test seam). Taken as a `Box` (the
    /// caller's spelling is unchanged) and held as an `Arc`, which is what the read
    /// worker clones.
    pub fn with_source(source: Box<dyn CoverageSource>) -> Self {
        Self {
            title: "Coverage".to_string(),
            source: source.into(),
            pending: None,
            index: None,
            graph: None,
            error: None,
            view: View::Repos,
            gaps_only: false,
            selected: None,
            // Interval 0: the warehouse probe below is what rate-limits; this watch only
            // compares.
            watch: crate::generation::Watch::new(0.0),
            gen: None,
        }
    }

    /// The pane over the tests' deterministic reading (feature `fixtures`).
    #[cfg(any(test, feature = "fixtures"))]
    pub fn fixture() -> Self {
        Self::with_source(Box::new(FixtureCoverage))
    }

    /// **Start the index read OFF the egui thread.** The worker owns a clone of the
    /// source; the frame keeps painting and [`Self::poll`] lands the result. A scan
    /// started while one is in flight is not a second round trip.
    ///
    /// MEASURED before this was off-thread (2026-08-25, this box, `SlowSource(600 ms)`
    /// standing in for the real per-table gRPC scans): the frame carrying the Scan click
    /// held for **700 ms** and the window could not paint for the whole read.
    pub fn scan(&mut self) {
        if self.pending.is_some() {
            return;
        }
        let source = std::sync::Arc::clone(&self.source);
        let (tx, rx) = std::sync::mpsc::channel();
        std::thread::Builder::new()
            .name("coverage-scan".into())
            .spawn(move || {
                let _ = tx.send(source.load());
            })
            .expect("spawn coverage scan worker");
        self.pending = Some((rx, std::time::Instant::now()));
    }

    /// Non-blocking: land the in-flight read if the worker finished. Called once/frame.
    pub fn poll(&mut self) {
        let Some((rx, _)) = &self.pending else { return };
        match rx.try_recv() {
            Ok(res) => {
                self.pending = None;
                self.land_scan(res);
            }
            Err(std::sync::mpsc::TryRecvError::Empty) => {}
            Err(std::sync::mpsc::TryRecvError::Disconnected) => {
                self.pending = None;
                // A dead worker is a FAILED REFRESH, which by this pane's own contract
                // keeps the previous reading and records the reason. Blanking a good
                // index because a thread died would be the very failure the pane exists
                // to prevent.
                self.error = Some("the coverage read worker died before it answered".into());
            }
        }
    }

    /// Blocking: wait for the in-flight read. The ROBOT seam (`update_json`) uses this
    /// so a scripted scan is complete when the call returns; the window never blocks.
    pub fn wait(&mut self) {
        let Some((rx, _)) = self.pending.take() else {
            return;
        };
        let res = rx
            .recv()
            .unwrap_or_else(|_| Err("the coverage read worker died before it answered".into()));
        self.land_scan(res);
    }

    /// Whether a read is in flight.
    pub fn is_pending(&self) -> bool {
        self.pending.is_some()
    }

    /// Fold a landed read into the index. On failure the pane keeps the previous result
    /// (if any) and records the reason — a failed refresh never silently blanks a good
    /// reading.
    fn land_scan(&mut self, res: Result<Loaded, String>) {
        match res {
            Ok(read) => {
                let borrowed: BTreeMap<&str, &Table> =
                    read.tables.iter().map(|(k, v)| (k.as_str(), v)).collect();
                let now = std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .map(|d| d.as_micros() as i64)
                    .unwrap_or(0);
                self.index = Some(build(
                    self.source.describe(),
                    &borrowed,
                    &GraphFeed::from(&read.graph),
                    &read.roster,
                    &BTreeSet::new(),
                    now,
                    dwarves::coverage::DEFAULT_HOT_PATHS,
                    dwarves::coverage::DEFAULT_WORST,
                ));
                self.graph = Some(read.graph);
                self.error = None;
            }
            Err(e) => self.error = Some(e),
        }
    }

    /// The one-line status. Never claims green without the evidence: before a scan
    /// it says so, after a failure it says why, and otherwise it is the ENGINE's own
    /// honest headline (which names the missing feeds).
    pub fn status(&self) -> String {
        if let Some(e) = &self.error {
            return format!("UNAVAILABLE — {e}");
        }
        match &self.index {
            // A read in flight is not "nothing here" and not "no gaps" — it is a third
            // state, and it says so rather than leaving the operator to guess.
            None if self.pending.is_some() => "reading the warehouse — not scanned yet".to_string(),
            // NOT "press Scan" — there is no button, and there never should have been.
            // This is the pre-first-read state: the generation probe has not answered
            // yet, so the pane has not been told what to read.
            None => "waiting for the warehouse generation — no reading yet".to_string(),
            Some(i) => i.honest_headline(),
        }
    }

    /// The repo rows the current filter shows.
    fn visible(&self) -> Vec<&dwarves::coverage::RepoCoverage> {
        let Some(i) = &self.index else {
            return Vec::new();
        };
        i.repos
            .iter()
            .filter(|r| {
                !self.gaps_only || r.unscanned || r.test.missing > 0 || r.bench.unbenched > 0
            })
            .collect()
    }
}

/// **One repo row, as text — the ONE spelling of it.**
///
/// The pane paints this and the copy button hands this over, so what lands on the
/// clipboard cannot drift from what the operator was reading. Two `format!`s of the
/// same columns is exactly how a copied table ends up with different widths, a stale
/// column, or a row the screen does not have (LAW 5).
///
/// An unscanned repo says so in place of numbers, here as on screen: a row of zeros
/// would read as "measured, and it is zero", which is the opposite of the truth.
fn repo_row_text(r: &dwarves::coverage::RepoCoverage) -> String {
    if r.unscanned {
        return format!(
            "{:<16} the code room's answer holds no fn of this repo",
            r.repo
        );
    }
    format!(
        "{:<16} {:>7} {:>7} {:>5.0}% {:>7} {:>5} {:>6}",
        r.repo,
        r.test.surface,
        r.test.covered,
        r.test.covered_frac() * 100.0,
        r.test.missing,
        r.bench.hot_paths,
        r.bench.unbenched,
    )
}

/// The whole visible listing as one clipboard payload: the header the pane paints,
/// then every row it paints, in the order it paints them.
fn repos_listing(header: &str, rows: &[dwarves::coverage::RepoCoverage]) -> String {
    std::iter::once(header.to_string())
        .chain(rows.iter().map(repo_row_text))
        .collect::<Vec<_>>()
        .join("\n")
}

fn chip(ui: &mut egui::Ui, text: &str, color: egui::Color32) {
    ui.label(
        egui::RichText::new(format!(" {text} "))
            .small()
            .strong()
            .color(egui::Color32::WHITE)
            .background_color(color),
    );
}

/// Red for a hard gap, amber for a caveat, teal for clean. Never hue-codes a
/// *magnitude* — the numbers do that.
fn gap_color(has_gap: bool) -> egui::Color32 {
    if has_gap {
        egui::Color32::from_rgb(0xc6, 0x28, 0x28)
    } else {
        egui::Color32::from_rgb(0x00, 0x69, 0x5c)
    }
}

impl Facet for CoveragePane {
    fn title(&self) -> &str {
        &self.title
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        // Land an in-flight read, if the worker finished, and keep asking for frames
        // while one is running so the "reading" line and any spinner stay live.
        self.poll();
        if self.pending.is_some() {
            ui.ctx().request_repaint();
        }

        // GENERATION, not a press. The pane reads itself the first time it is drawn
        // (a cold watch is a move) and again whenever the workspace's snapshot id
        // changes — i.e. when a populate has actually landed new rows. An unchanged
        // warehouse costs one small RPC every `GENERATION_PROBE_S` and no read at all.
        //
        // NO Scan button (owner 2026-08-25: SCAN is not a user action). A control
        // labelled `Scan` teaches that data is something you ask for, which is the
        // lesson `lens_chain` ("lenses are jobs, results are tables") abolished.
        let now_s = ui.input(|i| i.time);
        let g = match self.gen.as_mut() {
            Some(p) => p.poll(now_s),
            // No warehouse behind this source (fixture / injected): its generation is
            // constant, so the cold read is the only read.
            None => Some("fixture".to_string()),
        };
        if let Some(g) = g {
            if self.watch.saw(&g) {
                self.scan();
            }
        }

        ui.horizontal(|ui| {
            ui.heading("📐 coverage index");
            ui.checkbox(&mut self.gaps_only, "gaps only");
        });
        // The view toggle sits directly under the heading, ABOVE every line that comes
        // and goes (the "reading" line, the code-graph callout, MISSING FEED): a control
        // must not move under the pointer when a read lands.
        ui.horizontal(|ui| {
            for v in [View::Repos, View::Integrity, View::Weak, View::Feeds] {
                if ui.selectable_label(self.view == v, v.as_str()).clicked() {
                    self.view = v;
                }
            }
        });
        // A read IN FLIGHT is named on the surface. The previous reading (if any) stays
        // painted underneath — a refresh never blanks a good index.
        if let Some((_, started)) = &self.pending {
            ui.horizontal(|ui| {
                ui.spinner();
                ui.label(
                    egui::RichText::new(format!(
                        "reading the warehouse — {:.1}s (the scan runs off the UI thread; \
                         the reading below, if any, is the previous one)",
                        started.elapsed().as_secs_f32()
                    ))
                    .small(),
                );
            });
        }
        ui.label(egui::RichText::new(self.status()).monospace());
        ui.label(egui::RichText::new(format!("source: {}", self.source.describe())).small());
        // What the code half rests on: the room's state, its evidence, the room it was
        // read from — above every view, like the MISSING FEEDS callout below.
        if let Some(g) = &self.graph {
            crate::room::callout(ui, "code graph", g);
        }

        // The MISSING FEEDS callout sits ABOVE every view — an empty table must be
        // impossible to miss, because "no rows" read as "no gaps" is the failure this
        // whole pane exists to end.
        if let Some(i) = &self.index {
            let missing = i.missing_feeds();
            if !missing.is_empty() {
                ui.horizontal_wrapped(|ui| {
                    chip(ui, "MISSING FEED", egui::Color32::from_rgb(0xc6, 0x28, 0x28));
                    ui.label(
                        egui::RichText::new(format!(
                            "{} — no rows in the warehouse. This is a missing producer, NOT a clean bill of health.",
                            missing.join(", ")
                        ))
                        .small(),
                    );
                });
            }
        }
        ui.separator();
        ui.add_space(4.0);

        let Some(index) = &self.index else {
            ui.label(egui::RichText::new("no index yet — the pane asks nornir's code room and reads its warehouse itself, on open and whenever the workspace snapshot moves; it never re-scans source. Nothing to press.").small());
            return;
        };

        match self.view {
            View::Repos => {
                let rows: Vec<_> = self.visible().into_iter().cloned().collect();
                // ── WHY A BUTTON AND NOT A CopyableBlock ────────────────────────
                //
                // stinky `kopiera-2`: N labels in a clipped or scrolled area lose any
                // selection that reaches past the fold, because egui 0.35 drops a
                // cross-label selection the moment either end leaves the visible rect.
                // The cure for a LOG is `CopyableBlock` — one text widget.
                //
                // This table is not a log. Its rows are `selectable_label`s that PICK a
                // repo, and folding them into one `TextEdit` would take the clicking
                // away in order to give the copying. So the rows stay widgets and the
                // shared `copy_button` hands over the whole listing — header included,
                // in the same column widths the pane paints, so what lands on the
                // clipboard is what the operator was looking at.
                let header = format!(
                    "{:<16} {:>7} {:>7} {:>6} {:>7} {:>5} {:>6}",
                    "REPO", "FNS", "COVERED", "COV%", "NO-TEST", "HOT", "UNBEN"
                );
                ui.horizontal(|ui| {
                    ui.label(egui::RichText::new(&header).small().monospace().strong());
                    facett_app::widgets::copy_button(ui, &repos_listing(&header, &rows));
                });
                egui::ScrollArea::vertical()
                    .max_height(320.0)
                    .id_salt("coverage-repos")
                    .show(ui, |ui| {
                        for r in &rows {
                            ui.horizontal(|ui| {
                                if r.unscanned {
                                    chip(
                                        ui,
                                        "NOT SCANNED",
                                        egui::Color32::from_rgb(0x6a, 0x1b, 0x9a),
                                    );
                                    ui.monospace(repo_row_text(r));
                                    return;
                                }
                                if r.bench.never_benched() {
                                    chip(
                                        ui,
                                        "NEVER BENCHED",
                                        egui::Color32::from_rgb(0xc6, 0x28, 0x28),
                                    );
                                }
                                let clicked = ui
                                    .selectable_label(
                                        self.selected.as_deref() == Some(r.repo.as_str()),
                                        egui::RichText::new(repo_row_text(r)).monospace(),
                                    )
                                    .clicked();
                                if clicked {
                                    self.selected = Some(r.repo.clone());
                                }
                            });
                            if self.selected.as_deref() == Some(r.repo.as_str()) && !r.unscanned {
                                ui.indent(&r.repo, |ui| {
                                    ui.label(
                                        egui::RichText::new("un-benched hot paths (hottest first)")
                                            .small()
                                            .strong()
                                            .color(gap_color(r.bench.unbenched > 0)),
                                    );
                                    for h in r.bench.worst.iter().take(8) {
                                        ui.label(
                                            egui::RichText::new(format!(
                                                "  {:<40} score {:.2}  fan-in {:<5} churn90d {}",
                                                h.module, h.score, h.fan_in, h.commits_90d
                                            ))
                                            .small()
                                            .monospace(),
                                        );
                                    }
                                    ui.label(
                                        egui::RichText::new("most load-bearing untested fns")
                                            .small()
                                            .strong()
                                            .color(gap_color(r.test.missing > 0)),
                                    );
                                    for u in r.test.worst.iter().take(8) {
                                        ui.label(
                                            egui::RichText::new(format!(
                                                "  {:<52} fan-in {:<5} {}:{}",
                                                u.fqn, u.fan_in, u.file, u.line
                                            ))
                                            .small()
                                            .monospace(),
                                        );
                                    }
                                });
                            }
                        }
                    });
            }
            View::Integrity => {
                ui.label(
                    egui::RichText::new(
                        "ONE TABLE PER HOST — a 12-core loki number and a 32-core oden number are \
                         not comparable and are never merged here.",
                    )
                    .small(),
                );
                egui::ScrollArea::vertical().max_height(300.0).id_salt("coverage-integrity").show(ui, |ui| {
                    for t in &index.integrity.tables {
                        ui.horizontal(|ui| {
                            for d in &t.defects {
                                chip(ui, d.tag(), egui::Color32::from_rgb(0xc6, 0x28, 0x28));
                            }
                            if t.defects.is_empty() {
                                chip(ui, "publishable", gap_color(false));
                            }
                            ui.monospace(format!(
                                "{:<14} {:<14} {:>3} cores  {:>3} runs  {:>3} arms  {:>4}d old",
                                t.repo,
                                t.machine,
                                t.cores,
                                t.runs,
                                t.arms.len(),
                                if t.age_days == i64::MAX { -1 } else { t.age_days },
                            ));
                        });
                    }
                    for (repo, hosts) in &index.integrity.multi_host_repos {
                        ui.label(
                            egui::RichText::new(format!(
                                "! {repo} was measured on {} different hosts ({}) — two trend lines, never one",
                                hosts.len(),
                                hosts.join(" vs ")
                            ))
                            .small()
                            .color(egui::Color32::from_rgb(0xc6, 0x28, 0x28)),
                        );
                    }
                    for (arm, hosts) in &index.integrity.host_split_arms {
                        ui.label(
                            egui::RichText::new(format!(
                                "! arm {arm} has rows from {} — NOT COMPARABLE",
                                hosts.join(" and ")
                            ))
                            .small()
                            .color(egui::Color32::from_rgb(0xc6, 0x28, 0x28)),
                        );
                    }
                });
            }
            View::Weak => {
                // The evidence note comes FIRST: a verdict is never shown without the
                // statement of how much history it rests on.
                ui.label(
                    egui::RichText::new(&index.weak.evidence_note)
                        .small()
                        .monospace(),
                );
                ui.add_space(4.0);
                if !index.weak.has_usable_history() {
                    ui.label(
                        egui::RichText::new(
                            "No usable outcome history. This is a MISSING FEED, not a verdict of \
                             'no weak tests' — wire `nornir test` into the warehouse and this view \
                             fills in.",
                        )
                        .small()
                        .color(egui::Color32::from_rgb(0xc6, 0x28, 0x28)),
                    );
                }
                egui::ScrollArea::vertical()
                    .max_height(300.0)
                    .id_salt("coverage-weak")
                    .show(ui, |ui| {
                        for w in &index.weak.never_red {
                            ui.horizontal(|ui| {
                                chip(
                                    ui,
                                    "CANNOT-GO-RED",
                                    egui::Color32::from_rgb(0xc6, 0x28, 0x28),
                                );
                                ui.monospace(format!(
                                    "{:<14} {}  ({} runs / {} versions, 0 failures)",
                                    w.repo, w.test_name, w.runs, w.versions
                                ));
                            });
                        }
                    });
            }
            View::Feeds => {
                egui::ScrollArea::vertical()
                    .max_height(300.0)
                    .id_salt("coverage-feeds")
                    .show(ui, |ui| {
                        for f in &index.feeds {
                            ui.horizontal(|ui| {
                                if f.missing {
                                    chip(ui, "MISSING", egui::Color32::from_rgb(0xc6, 0x28, 0x28));
                                } else {
                                    chip(ui, "ok", gap_color(false));
                                }
                                ui.monospace(format!(
                                    "{:<18} {:>9} rows  {:>3} repos",
                                    f.table, f.rows, f.repos
                                ));
                            });
                        }
                    });
            }
        }
    }

    fn state_json(&self) -> serde_json::Value {
        let mut obj = serde_json::Map::new();
        obj.insert("kind".into(), serde_json::json!("coverage"));
        obj.insert("title".into(), serde_json::json!(self.title));
        obj.insert("source".into(), serde_json::json!(self.source.describe()));
        obj.insert("status".into(), serde_json::json!(self.status()));
        obj.insert("scanned".into(), serde_json::json!(self.index.is_some()));
        // The in-flight oracle a robot reads to tell "still reading" from "read nothing".
        obj.insert("pending".into(), serde_json::json!(self.pending.is_some()));
        obj.insert("error".into(), serde_json::json!(self.error));
        obj.insert("view".into(), serde_json::json!(self.view.as_str()));
        obj.insert("gaps_only".into(), serde_json::json!(self.gaps_only));
        obj.insert("selected".into(), serde_json::json!(self.selected));
        obj.insert("visible".into(), serde_json::json!(self.visible().len()));
        // The whole engine index, verbatim — the pane adds no numbers of its own, so
        // a robot asserting on the pane is asserting on the engine's RESULT.
        obj.insert(
            "index".into(),
            self.index
                .as_ref()
                .map(CoverageIndex::state_json)
                .unwrap_or(serde_json::Value::Null),
        );
        // The room's answer behind the index: its state, evidence, the room it names.
        obj.insert(
            "room".into(),
            self.graph
                .as_ref()
                .map(RoomAnswer::state_json)
                .unwrap_or(serde_json::Value::Null),
        );
        serde_json::Value::Object(obj)
    }

    fn update_json(&mut self, msg_json: &str) {
        // The remote seam: `"scan"` / `{"scan":true}` runs the index,
        // `{"view":"integrity"}` switches view, `{"select":"korp"}` expands a repo,
        // `{"gaps_only":true}` filters. The SAME messages the buttons above drive.
        if msg_json.trim() == "\"scan\"" {
            self.scan();
            self.wait();
            return;
        }
        let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) else {
            return;
        };
        if v.get("scan").and_then(|b| b.as_bool()).unwrap_or(false) {
            self.scan();
            self.wait();
        }
        if let Some(t) = v.get("view").and_then(|t| t.as_str()) {
            if let Some(f) = View::parse(t) {
                self.view = f;
            }
        }
        if let Some(s) = v.get("select").and_then(|s| s.as_str()) {
            self.selected = (!s.is_empty()).then(|| s.to_string());
        }
        if let Some(b) = v.get("gaps_only").and_then(|b| b.as_bool()) {
            self.gaps_only = b;
        }
    }
}

/// The shipped Coverage tab, bound to the SELECTED workspace. The roster is the
/// server's member list for it (read with the index), so a member the room has not
/// consumed shows as NOT SCANNED rather than vanishing from the denominator.
pub fn coverage_pane(endpoint: &str, token: &str, workspace: &str) -> CoveragePane {
    CoveragePane::live(endpoint, token, workspace)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// **A source that answers correctly, just not instantly.**
    ///
    /// The stand-in for what the SHIPPED source really does: `WarehouseCoverage::load`
    /// runs one `nornir_warehouse_embed::scan_warehouse_table` per table in
    /// [`TABLES`] — a gRPC round trip each in the thin build, an embedded skade read in
    /// the fat one. Neither is instant, and a cold `call_edges` (half a million rows for
    /// the constellation) is seconds.
    ///
    /// Sleeping is the honest model of that: what the timing test measures is WHERE the
    /// wait happens, not whether the read works.
    struct SlowSource(std::time::Duration);
    impl CoverageSource for SlowSource {
        fn load(&self) -> Result<Loaded, String> {
            std::thread::sleep(self.0);
            FixtureCoverage.load()
        }
        fn describe(&self) -> String {
            "slow warehouse (test) — answers, but not this frame".to_string()
        }
    }

    /// How long [`SlowSource`] holds its answer. Comfortably longer than a 60 fps
    /// frame budget (16 ms), so "the frame waited for the read" is not a timing
    /// coincidence on a loaded box.
    const SLOW: std::time::Duration = std::time::Duration::from_millis(600);

    /// **RED-when-broken: the pane READS ITSELF on open, and the opening frame comes
    /// back before the warehouse answers.**
    ///
    /// There is no Scan button any more (owner 2026-08-25: SCAN is not a user action),
    /// so the read is started by the GENERATION drive on the first frame the pane is
    /// drawn. That makes this assertion strictly stronger than the click version it
    /// replaces: a synchronous read would now hang the window on OPEN, with nobody
    /// having pressed anything.
    ///
    /// Driven through the REAL widget tree of the shipped pane (egui_kittest), against a
    /// source that holds its answer for [`SLOW`]. What is asserted is the wall-clock of
    /// the frame that starts the read, plus the state the pane publishes to a robot
    /// (`pending`) — not the presence of a channel.
    ///
    /// MEASURED before the conversion: `Coverage::scan` called `self.source.load()`
    /// straight from `Facet::ui`, so the frame held for the whole read and the window
    /// did not paint — read by the operator as "it hangs".
    #[test]
    fn the_pane_reads_itself_on_open_without_holding_the_frame() {
        use egui_kittest::kittest::Queryable;
        use egui_kittest::Harness;

        let pane = std::cell::RefCell::new(CoveragePane::with_source(Box::new(SlowSource(SLOW))));
        let mut h = Harness::builder()
            .with_size(egui::vec2(1100.0, 700.0))
            .build_ui(|ui| {
                pane.borrow_mut().ui(ui);
            });

        // ── THE OPENING FRAME, timed. Nobody pressed anything: the generation watch is
        //    cold, which counts as a move, so this frame STARTS the read.
        let t0 = std::time::Instant::now();
        h.step();
        let held = t0.elapsed();

        assert!(
            held < SLOW,
            "the opening frame held for {held:?} — the source needs {SLOW:?} to answer, \
             so the warehouse read ran ON the egui thread and the window could not paint \
             while it ran. With no button to press, that is a hang on OPEN."
        );
        assert_eq!(
            pane.borrow().state_json()["pending"],
            serde_json::json!(true),
            "the opening frame must START the read and hand the frame back — state {}",
            pane.borrow().state_json()
        );
        // And it really was self-started: no PRESSABLE widget in the tree could have
        // started it. Role-filtered, because the pane's own prose legitimately contains
        // the word "scanned" — it is the control that must be gone, not the vocabulary.
        assert!(
            h.query_all(
                egui_kittest::kittest::By::new()
                    .role(accesskit::Role::Button)
                    .label_contains("Scan")
            )
            .next()
            .is_none(),
            "the pane must carry no Scan control — the read is the generation's job"
        );
        assert_eq!(
            pane.borrow().state_json()["scanned"],
            serde_json::json!(false),
            "not scanned yet — the source is still holding its answer"
        );

        // ── NEVER BLANK WHILE IT LOADS. The pane must say what it is doing on the
        //    surface, not go quiet for the duration of the read.
        assert!(
            h.query_all_by_label_contains("reading the warehouse")
                .next()
                .is_some(),
            "a pane with a read in flight must SAY SO on the rendered surface"
        );

        // ── keep painting; the worker lands the result. `step` in a pump, never
        //    `run()`, which panics on a frame that keeps requesting repaints.
        let mut landed = false;
        for _ in 0..600 {
            h.step();
            if pane.borrow().state_json()["pending"] == serde_json::json!(false) {
                landed = true;
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(10));
        }
        assert!(landed, "the read must land within 6 s");
        let js = pane.borrow().state_json();
        assert_eq!(
            js["scanned"],
            serde_json::json!(true),
            "…and the index is the worker's"
        );
        assert_eq!(js["error"], serde_json::Value::Null);
        assert_eq!(
            js["index"]["repos_total"], 3,
            "the SAME index the inline read built"
        );
    }

    /// **The never-blank promise survives the conversion.** A refresh started over a
    /// slow source must not clear the reading already on screen: a good index stays
    /// visible, and stays visible for every frame the new read is in flight.
    #[test]
    fn a_refresh_in_flight_never_blanks_the_reading_already_on_screen() {
        let mut p = CoveragePane::fixture();
        p.update_json("\"scan\"");
        let good = p.state_json()["index"]["repos_scanned"].clone();
        assert_ne!(good, serde_json::Value::Null, "a reading to protect");

        p.source = std::sync::Arc::new(SlowSource(SLOW));
        p.scan();
        assert!(p.is_pending(), "the refresh is off-thread");
        // Every frame between the start and the landing keeps the old reading.
        for _ in 0..10 {
            p.poll();
            assert_eq!(
                p.state_json()["index"]["repos_scanned"],
                good,
                "a refresh in flight must never blank the previous reading"
            );
            std::thread::sleep(std::time::Duration::from_millis(10));
        }
        p.wait();
        assert_eq!(p.state_json()["index"]["repos_scanned"], good);
        assert_eq!(p.state_json()["pending"], serde_json::json!(false));
    }

    /// A source that fails, so the honest-unavailable path is drivable.
    struct DeadSource;
    impl CoverageSource for DeadSource {
        fn load(&self) -> Result<Loaded, String> {
            Err("[unreachable] the server did not answer: connect to http://127.0.0.1:0".into())
        }
        fn describe(&self) -> String {
            "code room + warehouse @ http://127.0.0.1:0".into()
        }
    }

    /// A source whose ROOM refuses (nothing consumed) while its warehouse answers.
    struct NotConsumed;
    impl CoverageSource for NotConsumed {
        fn load(&self) -> Result<Loaded, String> {
            let mut read = FixtureCoverage.load()?;
            // a refusal body that (wrongly) still carries rows: they must not be folded
            let mut body = FixtureCoverage::graph_body();
            body["state"] = serde_json::json!("not_consumed");
            body["refused"] = serde_json::json!(true);
            body["note"] =
                serde_json::json!("code room of `nordisk`: no repo has a consumer offset yet");
            read.graph = RoomAnswer::from_body(GRAPH_SURFACE, "test", body);
            Ok(read)
        }
        fn describe(&self) -> String {
            "a workspace nothing was consumed into (test)".into()
        }
    }

    /// **RED-when-broken.** A room that refuses draws its STATE: no repo is scanned,
    /// no fn count exists, the status leads with the room's own sentence — while the
    /// bench half, which is warehouse rows, is still reported.
    #[test]
    fn a_room_that_refuses_is_a_state_and_no_repo_number() {
        let mut p = CoveragePane::with_source(Box::new(NotConsumed));
        p.update_json("\"scan\"");
        let js = p.state_json();
        assert_eq!(js["room"]["state"], "not_consumed");
        assert_eq!(js["room"]["measured"], false);
        assert_eq!(
            js["room"]["evidence"],
            "code room of `nordisk`: no repo has a consumer offset yet"
        );
        assert!(
            js["index"]["graph"]["fns"].is_null(),
            "rows of a refusal are not folded, and not counted"
        );
        assert!(
            js["index"]["repos_scanned"].is_null() && js["index"]["test"].is_null(),
            "no number for a graph nobody read: {}",
            js["index"]
        );
        assert!(
            js["status"]
                .as_str()
                .unwrap()
                .starts_with("CODE GRAPH NOT READ [not_consumed]: code room of `nordisk`"),
            "{}",
            js["status"]
        );
        assert_eq!(
            js["index"]["repos_benched"], 1,
            "the bench runs are warehouse rows and are still read"
        );
    }

    /// **ROBOT SCENARIO — the whole pane driven over the remote seam, asserting a
    /// RESULT.**
    ///
    /// `state_json` / `update_json` only (no egui): Scan → korp is named as NEVER
    /// BENCHED, bifrost as NOT SCANNED, the preview row is unpublishable, and the
    /// missing feeds are listed. Every assertion is on a number the engine computed,
    /// never on a literal `true`.
    #[test]
    fn the_pane_scans_and_reports_the_real_gap_shapes() {
        let mut p = CoveragePane::fixture();

        // Before the scan the pane is HONESTLY empty, not green.
        let before = p.state_json();
        assert_eq!(before["scanned"], false);
        assert_eq!(before["index"], serde_json::Value::Null);
        assert!(
            before["status"]
                .as_str()
                .unwrap()
                .contains("no reading yet"),
            "the pane opens honestly un-read — and says so WITHOUT telling anyone to press \
             a button that no longer exists: {}",
            before["status"]
        );

        p.update_json("\"scan\"");
        let js = p.state_json();
        assert_eq!(js["scanned"], true);
        assert_eq!(js["error"], serde_json::Value::Null);

        let idx = &js["index"];
        assert_eq!(idx["repos_total"], 3, "the roster is the denominator");
        assert_eq!(
            idx["repos_scanned"], 2,
            "bifrost is in the roster with no rows"
        );
        assert_eq!(idx["repos_benched"], 1, "only znippy has a bench run");

        // korp is scanned but NEVER benched — named, not merely absent.
        let never: Vec<String> =
            serde_json::from_value(idx["bench"]["never_benched_repos"].clone()).unwrap();
        assert!(never.contains(&"korp".to_string()), "{never:?}");
        assert!(never.contains(&"bifrost".to_string()));

        // The empty tables are MISSING FEEDS and the headline says so.
        let missing: Vec<String> = serde_json::from_value(idx["missing_feeds"].clone()).unwrap();
        assert!(missing.contains(&"test_results".to_string()));
        assert!(missing.contains(&"coverage_fn".to_string()));
        assert!(js["status"].as_str().unwrap().contains("MISSING FEEDS"));

        // Hosts are partitioned: oden / oden-preview / ci are three tables, not one.
        assert_eq!(idx["integrity"]["host_tables"], 3);
        assert!(idx["integrity"]["defective_tables"].as_u64().unwrap() >= 2);
        let multi: Vec<String> =
            serde_json::from_value(idx["integrity"]["multi_host_repos"].clone()).unwrap();
        assert!(
            multi.contains(&"znippy".to_string()),
            "ci + oden ⇒ two trend lines"
        );

        // A real test-coverage result, not a tautology: the room says a test reaches
        // znippy::decode::decode_block and not znippy::vault::seal or either korp fn.
        assert_eq!(idx["test"]["covered"], 1);
        assert_eq!(idx["test"]["missing"], 3);
        // …and the pane says which room said so.
        assert_eq!(js["room"]["state"], "complete");
        assert_eq!(js["room"]["room"], "workspace nordisk · korp@4 (aaaaaaaaaa), znippy@7 (cccccccccc) · 4210 nodes · 9877 edges");
        assert_eq!(
            (idx["graph"]["fns"].as_u64(), idx["graph"]["tests"].as_u64()),
            (Some(4), Some(1))
        );
    }

    /// Within-pane interaction, asserted on the pane's own oracles: the view switch,
    /// the gaps-only filter and the repo expansion all move `state_json`.
    #[test]
    fn the_pane_controls_move_its_own_state_oracle() {
        let mut p = CoveragePane::fixture();
        p.update_json("\"scan\"");
        assert_eq!(p.state_json()["view"], "repos");
        assert_eq!(
            p.state_json()["visible"],
            3,
            "all three repos show by default"
        );

        p.update_json(r#"{"gaps_only":true}"#);
        // Every fixture repo HAS a gap, so the filter must not change the count — the
        // discriminating case is asserted below by construction of the fixture.
        assert_eq!(p.state_json()["gaps_only"], true);

        p.update_json(r#"{"view":"integrity"}"#);
        assert_eq!(p.state_json()["view"], "integrity");
        p.update_json(r#"{"view":"weak"}"#);
        assert_eq!(p.state_json()["view"], "weak");

        p.update_json(r#"{"select":"korp"}"#);
        assert_eq!(p.state_json()["selected"], "korp");
        p.update_json(r#"{"select":""}"#);
        assert_eq!(p.state_json()["selected"], serde_json::Value::Null);
    }

    /// **RED-when-broken.** An unreachable server must render the honest UNAVAILABLE
    /// state with the reason — never an empty index that reads as "no gaps".
    #[test]
    fn a_dead_server_says_unavailable_and_never_shows_a_clean_index() {
        let mut p = CoveragePane::with_source(Box::new(DeadSource));
        p.update_json("\"scan\"");
        let js = p.state_json();
        assert_eq!(
            js["scanned"], false,
            "a failed read must NOT produce an index"
        );
        assert_eq!(js["index"], serde_json::Value::Null);
        assert!(js["status"].as_str().unwrap().starts_with("UNAVAILABLE"));
        assert!(js["error"].as_str().unwrap().contains("unreachable"));
    }

    /// A failed REFRESH keeps the last good reading rather than blanking it — and
    /// still reports the failure.
    #[test]
    fn a_failed_refresh_keeps_the_previous_reading() {
        let mut p = CoveragePane::fixture();
        p.update_json("\"scan\"");
        let good = p.state_json()["index"]["repos_scanned"].clone();
        p.source = std::sync::Arc::new(DeadSource);
        p.update_json("\"scan\"");
        let js = p.state_json();
        assert_eq!(
            js["index"]["repos_scanned"], good,
            "the good reading survives"
        );
        assert!(js["status"].as_str().unwrap().starts_with("UNAVAILABLE"));
    }
}