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

nordisk/facett

facett-demo/src/releaseview_tab.rs

//! **The `releaseview` tab** — a release process as **three converging graphs**
//! ([`facett_releaseview::ReleaseView`]). The same nordisk constellation dependency
//! DAG in three states — LOCAL (manifest) · SPARRING (the Gate-B holger rehearsal) ·
//! CRATES_IO (published) — that a healthy release drives to CONVERGE.
//!
//! Seeded DIVERGED (several red needs-release + one amber bump-drift) and driven by
//! a **scripted converge cascade**: every few frames the tab applies the next
//! [`ReleaseEvent`] (publish a red crate, then adopt the drift bump), so the deck
//! shows the three graphs snapping together node-by-node — then loops. This is the
//! realtime `apply` seam a live dwarves release would drive one-way.

use facett_core::{Facet, FacetCaps};
use facett_releaseview::model::{CrateConvergence, DepEdge, ReleaseConvergence, ReleaseEvent};
use facett_releaseview::ReleaseView;

/// The stable tab title (the deck / discovery-contract / LEGEND key).
pub const TITLE: &str = "releaseview";

/// Frames between scripted cascade steps (≈ a beat per publish at 60fps).
const STEP_FRAMES: u32 = 90;

/// The seeded nordisk constellation, starting DIVERGED. Versions chosen so the
/// derivation lights every marker: dwarves + holger + nornir are behind on
/// crates.io (red needs-release), skade carries an un-hoisted bump (amber
/// bump-drift), edda is already converged (green).
pub fn sample_convergence() -> ReleaseConvergence {
    ReleaseConvergence::new("nordisk release-3")
        // idx 0: edda — already published, converged (green).
        .with_node(CrateConvergence::new("edda", "0.9.2", "0.9.2", "0.9.2"))
        // idx 1: skade — sparring hoisted 0.5.2, working tree still 0.5.1 (amber drift).
        .with_node(CrateConvergence::new("skade", "0.5.1", "0.5.2", "0.5.2"))
        // idx 2: dwarves — 0.1.4 local+sparring, crates.io behind at 0.1.3 (red).
        .with_node(CrateConvergence::new("dwarves", "0.1.4", "0.1.4", "0.1.3"))
        // idx 3: holger — 0.6.9 local+sparring, crates.io behind at 0.6.8 (red).
        .with_node(CrateConvergence::new("holger", "0.6.9", "0.6.9", "0.6.8"))
        // idx 4: nornir — 0.5.4 local+sparring, crates.io behind at 0.5.3 (red).
        .with_node(CrateConvergence::new("nornir", "0.5.4", "0.5.4", "0.5.3"))
        // deps: dwarves→{edda,skade}, holger→dwarves, nornir→holger.
        .with_edge(DepEdge::new(2, 0))
        .with_edge(DepEdge::new(2, 1))
        .with_edge(DepEdge::new(3, 2))
        .with_edge(DepEdge::new(4, 3))
}

/// A plain (un-scripted) [`ReleaseView`] over the seeded diverged constellation —
/// the mega-matrix's `Populated` cell (deterministic, no cascade timer).
pub fn populated_view() -> ReleaseView {
    ReleaseView::from_model(sample_convergence()).with_title(TITLE)
}

/// An empty [`ReleaseView`] (0 crates) — the mega-matrix's `Empty` data-state cell.
pub fn empty_view() -> ReleaseView {
    ReleaseView::from_model(ReleaseConvergence::new(TITLE)).with_title(TITLE)
}

/// The scripted converge cascade — the exact one-way events a live dwarves release
/// would drive, in topo order (skade's bump first, then publish up the graph).
fn cascade_script() -> Vec<ReleaseEvent> {
    vec![
        ReleaseEvent::LocalBumped { krate: "skade".into(), version: "0.5.2".into() },
        ReleaseEvent::PublishStart { krate: "dwarves".into() },
        ReleaseEvent::Published { krate: "dwarves".into(), version: "0.1.4".into() },
        ReleaseEvent::PublishStart { krate: "holger".into() },
        ReleaseEvent::Published { krate: "holger".into(), version: "0.6.9".into() },
        ReleaseEvent::PublishStart { krate: "nornir".into() },
        ReleaseEvent::Published { krate: "nornir".into(), version: "0.5.4".into() },
    ]
}

/// The `releaseview` deck facet: a [`ReleaseView`] over the seeded constellation,
/// plus a scripted cascade that animates the convergence and then loops.
pub struct ReleaseTab {
    view: ReleaseView,
    script: Vec<ReleaseEvent>,
    next: usize,
    frames: u32,
}

impl Default for ReleaseTab {
    fn default() -> Self {
        Self::new()
    }
}

impl ReleaseTab {
    /// Seed the diverged constellation + the cascade script.
    pub fn new() -> Self {
        let view = ReleaseView::from_model(sample_convergence()).with_title(TITLE);
        Self { view, script: cascade_script(), next: 0, frames: 0 }
    }

    /// Advance the scripted cascade one step (host/robot hook + the ui() driver).
    /// Applies the next event; when the script finishes it RESETS the constellation
    /// back to diverged so the deck loops the convergence story.
    pub fn step(&mut self) {
        if self.next < self.script.len() {
            let _ = self.view.apply_event(self.script[self.next].clone());
            self.next += 1;
        } else {
            // Loop: reseed diverged and replay from the top.
            let sel = self.view.selected().map(str::to_string);
            self.view = ReleaseView::from_model(sample_convergence()).with_title(TITLE);
            if let Some(s) = sel {
                let _ = self.view.update(facett_releaseview::Msg::SelectCrate(s));
            }
            self.next = 0;
        }
    }
}

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

    fn ui(&mut self, ui: &mut egui::Ui) {
        // Drive the scripted cascade on a frame timer, then paint the view. Only
        // step while following ambient time (a pinned/headless clock stays put).
        self.frames = self.frames.wrapping_add(1);
        if self.frames.is_multiple_of(STEP_FRAMES) {
            self.step();
        }
        self.view.ui(ui);
        ui.ctx().request_repaint();
    }

    fn state_json(&self) -> serde_json::Value {
        self.view.state_json()
    }

    fn caps(&self) -> FacetCaps {
        self.view.caps()
    }

    fn selection_json(&self) -> serde_json::Value {
        self.view.selection_json()
    }
}

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

    #[test]
    fn tab_seeds_diverged_then_the_cascade_converges_it() {
        let mut tab = ReleaseTab::new();
        let sj = tab.state_json();
        assert_eq!(sj["node_count"], 5, "the seeded nordisk constellation has 5 crates");
        assert_eq!(sj["converged"], false, "it starts DIVERGED");
        assert_eq!(sj["statuses"]["needs_release"], 3, "three red needs-release crates");
        assert_eq!(sj["statuses"]["bump_drift"], 1, "one amber bump-drift crate");
        assert_eq!(sj["title"], TITLE);

        // Run the whole scripted cascade → the three graphs converge.
        for _ in 0..cascade_script().len() {
            tab.step();
        }
        assert_eq!(tab.state_json()["converged"], true, "the cascade drives the constellation to converged");

        // One more step loops it back to diverged (the deck replays the story).
        tab.step();
        assert_eq!(tab.state_json()["converged"], false, "the tab loops back to diverged");
    }
}