nornir 0.5.0

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
//! **S3c — the PANE DISCOVERY REGISTRY: the formal, stable enumeration of every
//! addressable viz surface the atom-walk + functional matrix walk.**
//!
//! Before this, the only surface enumeration was [`Tab::ALL`] (20 variants) plus
//! ad-hoc `state_json` slicing in the matrix. That left the 🧬 Nornir tab's two
//! SUB-PANELS — `populate_status` (Viz.CloneEvents) and `jobs` (Viz.Jobs) — with
//! NO place in any list (they hide *inside* a single `Tab::ALL` variant), so they
//! slipped the walk before. [`viz_surfaces`] is the canonical list the walk drives
//! instead: one [`VizSurface`] per `Tab::ALL` variant, PLUS the known sub-panels.
//!
//! Each surface carries the STABLE AccessKit `id_salt` ([`VizSurface::id`], = the
//! pane's `ui.push_id` salt at its render root) so an atom is uniquely addressable
//! by the kit, and the `has_remote`/`has_local` capability flags the matrix reads.
//!
//! ANTI-DRIFT: `surfaces_cover_every_tab` (in `app.rs`, where `Tab::ALL` is in
//! scope) fails the build if [`viz_surfaces`] stops covering a `Tab::ALL` variant
//! — so adding a tab forces a registry entry, mirroring `viz_tabs_const_matches_tab_enum`.

use super::app::Tab;

/// One mechanically-addressable viz surface — either a top-level tab pane (one per
/// [`Tab::ALL`] variant) or a known sub-panel that hides inside a tab. The atom-walk
/// and functional matrix enumerate `viz_surfaces()` to cover EVERY surface; the
/// `id` is the stable AccessKit `id_salt` pushed at the pane's render root.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VizSurface {
    /// The tab this surface lives on (top-level pane, or the tab a sub-panel hides
    /// inside). The atom-walk drives here via `apply_command(VizCommand{tab})`.
    pub tab: Tab,
    /// The STABLE pane id == the AccessKit `id_salt` pushed at this surface's render
    /// root (`Tab::id` for top-level panes; `"<Tab>.<panel>"` for sub-panels). Never
    /// collides across surfaces, so `By::label`/the drift gate match precisely.
    pub id: &'static str,
    /// Human-readable label (the header glyph+words for tabs; a short name for
    /// sub-panels). Kept readable — distinct from the machine `id`.
    pub label: &'static str,
    /// True if this surface reads from a remote `nornir-server` (thin/RPC mode).
    pub has_remote: bool,
    /// True if this surface reads from a local embedded warehouse.
    pub has_local: bool,
    /// True if this is a SUB-PANEL nested inside a tab (not a top-level pane) — the
    /// `populate_status`/`jobs` panels that no per-tab list covered before.
    pub is_subpanel: bool,
}

impl VizSurface {
    const fn tab_pane(tab: Tab, id: &'static str, label: &'static str) -> Self {
        VizSurface { tab, id, label, has_remote: true, has_local: true, is_subpanel: false }
    }
    const fn subpanel(tab: Tab, id: &'static str, label: &'static str) -> Self {
        VizSurface { tab, id, label, has_remote: true, has_local: true, is_subpanel: true }
    }
}

/// The PANE DISCOVERY REGISTRY: every addressable viz surface, in header order,
/// followed by the known sub-panels. This SUPERSEDES `Tab::ALL` + ad-hoc
/// `state_json` slicing as the surface enumerator the atom-walk + matrix walk.
///
/// Invariant (enforced by `app::surfaces_cover_every_tab`): there is exactly one
/// top-level [`VizSurface`] per [`Tab::ALL`] variant, with `id == Tab::id()`.
pub fn viz_surfaces() -> Vec<VizSurface> {
    let mut v: Vec<VizSurface> = Tab::ALL
        .iter()
        .map(|t| VizSurface::tab_pane(*t, t.id(), t.label()))
        .collect();

    // KNOWN SUB-PANELS that hide inside a tab (no `Tab::ALL` variant of their own),
    // so the walk would miss them without an explicit registry entry. Both live on
    // the 🧬 Nornir root pane and read Viz.CloneEvents / Viz.Jobs (remote) or the
    // local warehouse / jobs.redb (local).
    v.push(VizSurface::subpanel(Tab::Nornir, "Nornir.populate_status", "Populate status (CloneEvents)"));
    v.push(VizSurface::subpanel(Tab::Nornir, "Nornir.jobs", "Jobs (Viz.Jobs)"));

    v
}

/// The id_salts of just the top-level tab panes (one per `Tab::ALL` variant), in
/// header order — the list each tab pane's render root pushes via `ui.push_id`.
pub fn tab_pane_ids() -> Vec<&'static str> {
    viz_surfaces().iter().filter(|s| !s.is_subpanel).map(|s| s.id).collect()
}

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

    /// inject-assert: the registry enumerates every tab pane PLUS the two known
    /// sub-panels, and every id is unique (no AccessKit `id_salt` collision).
    #[test]
    fn registry_has_every_tab_plus_subpanels_with_unique_ids() {
        let surfaces = viz_surfaces();
        // 20 tab panes + 2 sub-panels.
        let tab_panes = surfaces.iter().filter(|s| !s.is_subpanel).count();
        let subpanels = surfaces.iter().filter(|s| s.is_subpanel).count();
        assert_eq!(tab_panes, Tab::ALL.len(), "one VizSurface per Tab::ALL variant");
        assert_eq!(subpanels, 2, "the populate_status + jobs sub-panels are enumerated");

        // Ids are globally unique (so id_salt never collides -> atoms addressable).
        let mut ids: Vec<&str> = surfaces.iter().map(|s| s.id).collect();
        let n = ids.len();
        ids.sort_unstable();
        ids.dedup();
        assert_eq!(ids.len(), n, "every VizSurface id must be unique");

        // The two sub-panels are the ones that slipped the per-tab list before.
        assert!(
            surfaces.iter().any(|s| s.id == "Nornir.populate_status" && s.is_subpanel),
            "populate_status sub-panel enumerated"
        );
        assert!(
            surfaces.iter().any(|s| s.id == "Nornir.jobs" && s.is_subpanel),
            "jobs sub-panel enumerated"
        );
    }

    /// Each top-level pane's id is exactly its `Tab::id()` (so the `ui.push_id`
    /// salt at the render root and the registry agree — the addressing contract).
    #[test]
    fn tab_pane_ids_equal_tab_id() {
        for s in viz_surfaces().iter().filter(|s| !s.is_subpanel) {
            assert_eq!(s.id, s.tab.id(), "tab pane id must equal Tab::id()");
        }
    }
}