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

nordisk/nornir

src/viz/uiplane_plan.rs

//! # nornir-viz as a `UiPlan` — the LAW 9 worked example (the reference impl)
//!
//! This is nornir *applying* the generic
//! [`nornir_testmatrix::uiplane`](nornir_testmatrix::uiplane) primitive to ITS OWN
//! UI — the dogfood that proves the harness. The generic core (planes / plan /
//! `PlaneDriver` / `RobotPlan::walk`) lives in the framework crate and knows
//! nothing about viz; here we:
//!
//! 1. **Model the viz as a [`UiPlan`]** ([`viz_plan`]): one **plane per
//!    [`Tab`](crate::viz)** (read from the app's canonical `state_json["all_tabs"]`
//!    enumerator, never hand-listed), with a transition `tab-strip click` from the
//!    boot tab to every other tab (the tab-strip is fully connected from the start
//!    plane — every tab is one click away). Each plane declares ONE data surface:
//!    its `state_json` pane block, which must be PRESENT and have RAN (populated).
//!
//! 2. **Drive it with a native [`PlaneDriver`]** ([`VizPlaneDriver`]) over the live
//!    [`UrdrThreadsApp`](crate::viz::UrdrThreadsApp): a transition is a real
//!    `apply_command(VizCommand{tab})` (the SAME control-channel click `viz.click`
//!    uses), `current_plane()` reads back `state_json["tab"]`, and present/ran read
//!    the per-tab pane block — emitted data, never pixels (LAW 6).
//!
//! The walk then proves every tab is **reachable + its pane RAN** (real data, not
//! empty — ties RAGNARÖK / LAW 2). A tab whose pane block is absent or empty after
//! navigation is a RED LAW 9 row; an orphan tab (no transition) is RED.
//!
//! The **wasm/deployed seam** for LAW 1 is defined (not implemented here) in
//! [`crate::viz::uiplane_plan::wasm`] — a [`PlaneDriver`] over a headless browser
//! that reads `window.__facett_state()`; the facett-demo agent wires the JS hook,
//! and the SAME [`viz_plan`] (well, facett-demo's deck plan) runs against it.

use std::collections::BTreeSet;

use eframe::egui;
use nornir_testmatrix::uiplane::{PlaneDriver, Transition, UiPlan, UiPlane, UiSurface};

use super::UrdrThreadsApp;
use super::control::VizCommand;

/// The `state_json` pane-block key each tab populates — the surface that must be
/// PRESENT and have RAN once the tab plane is reached. The tuple is
/// `(tab-debug-name, pane-block-key)`. The tab-debug-name is the SAME id the app's
/// `state_json["tab"]` / `all_tabs` use and that `apply_command` resolves; the
/// pane-block-key is the top-level `state_json` key whose presence + non-emptiness
/// proves the pane ran.
///
/// Kept in lock-step with `Tab::label` / the `state_json` builder in `app.rs` — a
/// tab whose block key is wrong shows up immediately as a RED `ReachableAbsent`
/// walk row, so this map cannot silently drift.
pub const TAB_PANE_BLOCK: &[(&str, &str)] = &[
    ("Nornir", "nornir"),
    ("Timeline", "timeline"),
    ("DepGraph", "dep_graph"),
    ("CallGraph", "callgraph"),
    ("Architecture", "architecture"),
    ("Funnel", "funnel"),
    ("TimeTravel", "timetravel"),
    ("LiveRun", "live"),
    ("Release", "release"),
    ("Knowledge", "knowledge"),
    ("Warehouse", "warehouse"),
    ("Mcp", "mcp"),
    ("Search", "search"),
    ("Gates", "gates"),
    ("Bench", "bench"),
    ("Test", "test"),
    ("Leaderboard", "leaderboard"),
    ("Security", "security"),
    ("Holger", "holger"),
    ("Manual", "manual"),
    // The 7 tabs whose panes were reachable-but-surfaceless: each emits a
    // top-level `state_json` block, so declare the surface so the uiplane walk
    // actually runs a row for it (they were silent holes before).
    ("Metro", "metro"),
    ("Helix", "helix"),
    ("Bloodstream", "bloodstream"),
    ("WarehouseDeck", "warehouse_deck"),
    ("Claude", "claude"),
    ("Models", "models"),
    // ⚒ Dwarves (N5) — the merged build-runner + CI-forge tab. Its own
    // `state_json["dwarves"]` block (the active pane + both sub-slices) is the
    // surface the LAW-9 walk runs a green cell for; the former Build/Brokkr blocks
    // are still emitted at the top level for back-compat.
    ("Dwarves", "dwarves"),
];

/// The transition-action prefix for a tab-strip click — the native driver parses
/// `"click_tab:<TabName>"` and routes it to `apply_command`.
pub const CLICK_TAB: &str = "click_tab:";

/// The surface id used for a tab's data pane (one per plane). Stable so the driver
/// reports present/ran under the same id.
fn pane_surface_id(pane_key: &str) -> String {
    format!("pane:{pane_key}")
}

/// Build the viz [`UiPlan`] from the canonical tab list `all_tabs` (the debug
/// names from the running app's `state_json["all_tabs"]`) and the boot tab `start`
/// (the app's `state_json["tab"]`). Discovery-contract: the plane set is READ from
/// the running app, never hard-coded — so a tab added to `Tab::ALL` automatically
/// becomes a plane the walk must reach.
///
/// Topology: the tab strip is reachable from the start plane in one click each, so
/// we add a `start → tab` transition for every other tab. (A real deck with nested
/// dialogs would add deeper edges; the tab strip is flat by construction.)
pub fn viz_plan(all_tabs: &[String], start: &str) -> UiPlan {
    let block_of = |tab: &str| {
        TAB_PANE_BLOCK
            .iter()
            .find(|(t, _)| *t == tab)
            .map(|(_, b)| *b)
    };
    let mut plan = UiPlan::new(start.to_string());
    for tab in all_tabs {
        let mut plane = UiPlane::new(tab.clone());
        if let Some(block) = block_of(tab) {
            // The pane block is a data surface that must be present + ran.
            plane = plane.surface(
                UiSurface::new(pane_surface_id(block)).with_label(format!("{tab} pane ({block})")),
            );
        }
        plan = plan.plane(plane);
    }
    // Tab strip: every tab is one click from the start plane.
    for tab in all_tabs {
        if tab != start {
            plan = plan.transition(Transition::new(
                start.to_string(),
                tab.clone(),
                format!("{CLICK_TAB}{tab}"),
            ));
        }
    }
    plan
}

/// A native [`PlaneDriver`] over the live [`UrdrThreadsApp`]. Drives tab
/// transitions through the app's REAL control channel (`apply_command`) and reads
/// plane / present / ran from `state_json` — the exact emitted data the operator's
/// `viz.click` / `viz.state` MCP tools read. No pixels (LAW 6).
pub struct VizPlaneDriver<'a> {
    app: &'a mut UrdrThreadsApp,
    /// How many headless frames to settle after each transition (the pane block is
    /// recomputed on render; a couple of frames lets it populate, esp. thin mode).
    settle_frames: usize,
}

impl<'a> VizPlaneDriver<'a> {
    /// Wrap a live app; `settle_frames` headless renders are run after each
    /// transition so the destination pane populates before we read it.
    pub fn new(app: &'a mut UrdrThreadsApp, settle_frames: usize) -> Self {
        Self { app, settle_frames }
    }

    /// Settle by rendering the CURRENT tab a few frames — NOT
    /// `render_all_tabs_headless`, which loops `Tab::ALL` and would leave
    /// `self.tab` on the last tab, clobbering the transition we just applied. A
    /// single-tab `draw_ui` frame preserves the active plane while letting the
    /// pane recompute / fire its render-side emitters.
    fn settle(&mut self) {
        for _ in 0..self.settle_frames.max(1) {
            let ctx = egui::Context::default();
            // egui 0.36 debug-asserts on a FullOutput dropped with unapplied texture
            // deltas; nothing is uploaded headless, so they are cleared (2026-10-03).
            ctx.run_ui(egui::RawInput::default(), |ui| self.app.draw_ui(ui)).textures_delta.clear();
        }
    }

    /// Read the per-tab pane block under the current plane and decide present/ran.
    /// PRESENT iff the `state_json` pane block exists and is not JSON `null`; RAN
    /// iff it carries real populated data (a non-empty object/array, or a scalar) —
    /// a present-but-empty block is present-not-ran (RAGNARÖK: empty ≠ ran).
    fn pane_state(&mut self) -> (BTreeSet<String>, BTreeSet<String>) {
        let mut present = BTreeSet::new();
        let mut ran = BTreeSet::new();
        let state = self.app.state_json();
        let tab = state
            .get("tab")
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string();
        if let Some(block) = TAB_PANE_BLOCK
            .iter()
            .find(|(t, _)| *t == tab)
            .map(|(_, b)| *b)
        {
            let id = pane_surface_id(block);
            match state.get(block) {
                None | Some(serde_json::Value::Null) => {} // absent
                Some(v) => {
                    present.insert(id.clone());
                    if pane_block_ran(v) {
                        ran.insert(id);
                    }
                }
            }
        }
        (present, ran)
    }
}

/// Did a pane block actually RUN (populate), or is it an empty stub? An object
/// with ≥1 key, an array with ≥1 element, or any non-null scalar counts as ran; an
/// empty object/array is present-but-not-ran (RAGNARÖK — empty ≠ ran). This is the
/// LAW 2 hook: a blank pane after navigation is a RED `ReachableNotRan` row, not a
/// silent green.
pub fn pane_block_ran(v: &serde_json::Value) -> bool {
    match v {
        serde_json::Value::Null => false,
        serde_json::Value::Object(m) => !m.is_empty(),
        serde_json::Value::Array(a) => !a.is_empty(),
        // A bare scalar (bool/number/string) the pane emitted is real data.
        _ => true,
    }
}

impl PlaneDriver for VizPlaneDriver<'_> {
    fn current_plane(&mut self) -> String {
        self.app
            .state_json()
            .get("tab")
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string()
    }

    fn apply(&mut self, action: &str) -> Result<(), String> {
        let tab = action
            .strip_prefix(CLICK_TAB)
            .ok_or_else(|| format!("VizPlaneDriver: unknown action '{action}'"))?;
        // The REAL control-channel click (parity with viz.click / the tab strip).
        self.app.apply_command(&VizCommand {
            tab: Some(tab.to_string()),
            ..Default::default()
        });
        self.settle();
        Ok(())
    }

    fn surfaces_present(&mut self) -> BTreeSet<String> {
        self.pane_state().0
    }

    fn surfaces_ran(&mut self) -> BTreeSet<String> {
        self.pane_state().1
    }
}

/// The **wasm / deployed-artifact driver seam** for LAW 1 (SHIPPED ≠ TESTED).
///
/// The native [`VizPlaneDriver`] drives the in-process app; LAW 1 demands the SAME
/// plan also runs against the **shipped wasm bundle** a user loads. This module
/// DEFINES the seam (the trait the headless-browser backend implements) so it
/// plugs in unchanged — the facett-demo agent is building the JS hook
/// (`window.__facett_state()` + `window.__facett_click(action)`) in parallel; this
/// is the Rust side that consumes it.
pub mod wasm {
    use std::collections::BTreeSet;

    use nornir_testmatrix::uiplane::PlaneDriver;

    /// The browser/JS bridge a deployed-wasm driver needs — implemented by a
    /// headless-browser harness (e.g. a `fantoccini`/`chromedriver` or `wasm-pack
    /// test --headless` backend) that evaluates JS against the LOADED `*.wasm`
    /// bundle the deploy serves.
    ///
    /// The facett-demo wasm build exposes (the parallel agent's job):
    /// - `window.__facett_state()` → the app's `state_json` (same shape as native),
    /// - `window.__facett_click(action)` → performs a transition action and settles.
    ///
    /// This trait is the thin Rust seam over those two JS calls; a concrete impl
    /// `eval`s them via the browser driver. Defined here so [`WasmPlaneDriver`]
    /// (and thus `RobotPlan::walk`) is ready the moment the hook lands.
    pub trait BrowserBridge {
        /// Evaluate `window.__facett_state()` and return the parsed JSON. Err on a
        /// driver / navigation failure (a load error is itself a LAW 1 finding —
        /// the shipped bundle didn't come up).
        fn eval_state(&mut self) -> Result<serde_json::Value, String>;
        /// Evaluate `window.__facett_click(action)` (perform a transition) and wait
        /// for the app to settle (one animation frame / a state-stable poll).
        fn eval_click(&mut self, action: &str) -> Result<(), String>;
    }

    /// A [`PlaneDriver`] over the SHIPPED wasm bundle, via a [`BrowserBridge`]. It
    /// reads plane / present / ran from `window.__facett_state()` and drives
    /// transitions through `window.__facett_click(action)` — the SAME `UiPlan` the
    /// native driver runs, now proving the DEPLOYED artifact (LAW 1).
    ///
    /// The present/ran extraction reuses the SAME pane-block logic as native
    /// ([`super::pane_block_ran`]) against the wasm state, so native↔wasm verdicts
    /// are computed identically — only the transport differs. The `pane_block` /
    /// `surface_id` closures are injected so this is reusable for any deck (the
    /// facett-demo deck supplies its own plane→block map).
    pub struct WasmPlaneDriver<B: BrowserBridge> {
        bridge: B,
        /// Maps a plane id → its `state_json` pane-block key (the facett-demo deck
        /// or the viz tab map; see [`super::TAB_PANE_BLOCK`]).
        block_of: fn(&str) -> Option<&'static str>,
        /// Builds the surface id from a pane-block key (parity with native).
        surface_id: fn(&str) -> String,
    }

    impl<B: BrowserBridge> WasmPlaneDriver<B> {
        /// Construct over a browser bridge + the deck's plane→block resolver +
        /// surface-id builder (the native viz passes [`super::TAB_PANE_BLOCK`]'s
        /// lookup and `pane:<block>` builder; facett-demo passes its own).
        pub fn new(
            bridge: B,
            block_of: fn(&str) -> Option<&'static str>,
            surface_id: fn(&str) -> String,
        ) -> Self {
            Self {
                bridge,
                block_of,
                surface_id,
            }
        }

        fn pane_state(&mut self) -> (BTreeSet<String>, BTreeSet<String>) {
            let mut present = BTreeSet::new();
            let mut ran = BTreeSet::new();
            let Ok(state) = self.bridge.eval_state() else {
                return (present, ran); // bridge error ⇒ nothing present (RED downstream)
            };
            let tab = state
                .get("tab")
                .and_then(|v| v.as_str())
                .unwrap_or_default();
            if let Some(block) = (self.block_of)(tab) {
                let id = (self.surface_id)(block);
                if let Some(v) = state.get(block) {
                    if !v.is_null() {
                        present.insert(id.clone());
                        if super::pane_block_ran(v) {
                            ran.insert(id);
                        }
                    }
                }
            }
            (present, ran)
        }
    }

    impl<B: BrowserBridge> PlaneDriver for WasmPlaneDriver<B> {
        fn current_plane(&mut self) -> String {
            self.bridge
                .eval_state()
                .ok()
                .and_then(|s| s.get("tab").and_then(|v| v.as_str()).map(String::from))
                .unwrap_or_default()
        }
        fn apply(&mut self, action: &str) -> Result<(), String> {
            self.bridge.eval_click(action)
        }
        fn surfaces_present(&mut self) -> BTreeSet<String> {
            self.pane_state().0
        }
        fn surfaces_ran(&mut self) -> BTreeSet<String> {
            self.pane_state().1
        }
    }

    /// The viz deck's plane→block resolver (over [`super::TAB_PANE_BLOCK`]) — the
    /// `block_of` a [`WasmPlaneDriver`] for the nornir-viz wasm bundle uses.
    pub fn viz_block_of(tab: &str) -> Option<&'static str> {
        super::TAB_PANE_BLOCK
            .iter()
            .find(|(t, _)| *t == tab)
            .map(|(_, b)| *b)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use nornir_testmatrix::uiplane::{RobotPlan, SurfaceVerdict};

    /// A tiny stand-in `all_tabs` (subset of the real list) for plan-shape tests
    /// that don't need a full app.
    fn sample_tabs() -> Vec<String> {
        ["Nornir", "Bench", "Test"]
            .iter()
            .map(|s| s.to_string())
            .collect()
    }

    #[test]
    fn plan_makes_a_plane_per_tab_and_a_click_from_start() {
        let plan = viz_plan(&sample_tabs(), "Nornir");
        assert_eq!(plan.planes.len(), 3, "one plane per tab");
        // start has an edge to every OTHER tab.
        assert_eq!(
            plan.transitions.len(),
            2,
            "Bench + Test reachable from Nornir"
        );
        assert!(
            plan.orphan_planes().is_empty(),
            "every tab reachable from start"
        );
        assert!(
            plan.dangling_transitions().is_empty(),
            "every edge lands on a declared plane"
        );
        // each non-Manual tab declares its pane surface.
        let bench = plan.planes.get("Bench").unwrap();
        assert!(
            bench.surfaces.contains_key(&pane_surface_id("bench")),
            "Bench pane surface declared"
        );
    }

    #[test]
    fn pane_block_ran_distinguishes_populated_from_empty() {
        // RAGNARÖK: an empty object/array is present-but-NOT-ran.
        assert!(
            !pane_block_ran(&serde_json::json!({})),
            "empty object ≠ ran"
        );
        assert!(!pane_block_ran(&serde_json::json!([])), "empty array ≠ ran");
        assert!(!pane_block_ran(&serde_json::Value::Null), "null ≠ ran");
        assert!(
            pane_block_ran(&serde_json::json!({"rows": 3})),
            "populated object = ran"
        );
        assert!(
            pane_block_ran(&serde_json::json!([1, 2])),
            "non-empty array = ran"
        );
        assert!(
            pane_block_ran(&serde_json::json!(42)),
            "a scalar emit = ran"
        );
    }

    /// A wasm `BrowserBridge` test double: a scripted state + a click that switches
    /// the `tab` field — proves the wasm seam runs the SAME walk logic offline.
    struct FakeBrowser {
        tab: String,
        // plane → its pane-block value the "shipped wasm" would report.
        blocks: std::collections::BTreeMap<String, serde_json::Value>,
    }
    impl wasm::BrowserBridge for FakeBrowser {
        fn eval_state(&mut self) -> Result<serde_json::Value, String> {
            let mut obj = serde_json::Map::new();
            obj.insert("tab".into(), serde_json::Value::String(self.tab.clone()));
            // expose the current tab's block under viz_block_of(tab).
            if let Some(block) = wasm::viz_block_of(&self.tab) {
                if let Some(v) = self.blocks.get(&self.tab) {
                    obj.insert(block.to_string(), v.clone());
                }
            }
            Ok(serde_json::Value::Object(obj))
        }
        fn eval_click(&mut self, action: &str) -> Result<(), String> {
            let tab = action
                .strip_prefix(CLICK_TAB)
                .ok_or_else(|| format!("bad action {action}"))?;
            self.tab = tab.to_string();
            Ok(())
        }
    }

    #[test]
    fn wasm_seam_walks_the_same_plan_green_when_shipped_panes_populate() {
        // The DEPLOYED-artifact driver (LAW 1) runs the SAME viz_plan; here the
        // shipped wasm reports populated panes → GREEN.
        let tabs = sample_tabs();
        let plan = viz_plan(&tabs, "Nornir");
        let mut blocks = std::collections::BTreeMap::new();
        blocks.insert("Nornir".to_string(), serde_json::json!({"server": "up"}));
        blocks.insert("Bench".to_string(), serde_json::json!({"series": [1, 2]}));
        blocks.insert("Test".to_string(), serde_json::json!({"runs": 4}));
        let bridge = FakeBrowser {
            tab: "Nornir".into(),
            blocks,
        };
        let mut driver =
            wasm::WasmPlaneDriver::new(bridge, wasm::viz_block_of, |b| pane_surface_id(b));
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(
            report.is_green(),
            "shipped wasm panes populate → GREEN: {}",
            report.summary()
        );
        assert_eq!(
            report.reached_planes.len(),
            3,
            "every tab reached in the wasm bundle"
        );
    }

    #[test]
    fn wasm_seam_goes_red_when_a_shipped_pane_is_blank() {
        // SENSITIVITY (LAW 1 + LAW 2): the shipped wasm reaches Bench but its pane
        // is EMPTY → a RED ReachableNotRan row — the exact "green natively, blank in
        // the shipped bundle" catch.
        let tabs = sample_tabs();
        let plan = viz_plan(&tabs, "Nornir");
        let mut blocks = std::collections::BTreeMap::new();
        blocks.insert("Nornir".to_string(), serde_json::json!({"server": "up"}));
        blocks.insert("Bench".to_string(), serde_json::json!({})); // BLANK in the shipped wasm
        blocks.insert("Test".to_string(), serde_json::json!({"runs": 4}));
        let bridge = FakeBrowser {
            tab: "Nornir".into(),
            blocks,
        };
        let mut driver =
            wasm::WasmPlaneDriver::new(bridge, wasm::viz_block_of, |b| pane_surface_id(b));
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(!report.is_green(), "blank shipped pane → RED");
        let bench = report
            .rows
            .iter()
            .find(|r| r.plane == "Bench")
            .expect("Bench row present");
        assert_eq!(
            bench.verdict,
            SurfaceVerdict::ReachableNotRan,
            "blank pane = present-not-ran"
        );
    }
}