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