nornir 0.5.0

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! # 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::control::VizCommand;
use super::UrdrThreadsApp;

/// 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 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();
            let _ = ctx.run(egui::RawInput::default(), |ctx| self.app.draw_ui(ctx));
        }
    }

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