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

nordisk/facett

facett-releaseview/src/vault.rs

//! **The GPU-hero lowering** (feature `graph3d`) — lower the neutral
//! [`ReleaseConvergence`] model onto the shared facett-graph3d **CodeVault** so the
//! three release graphs (LOCAL · SPARRING · CRATES_IO) render as a real
//! depth-composited 3D box scene that **collapses into agreement as a release
//! converges**.
//!
//! ## The reuse (L5 — no forked renderer)
//!
//! [`convergence_to_codegraph`] expresses the three planes as **depth-staggered
//! boxes in ONE [`CodeGraph`]**, then [`crate::ReleaseView`] hands it to
//! [`CodeVault`] — whose orbit camera, depth-sorted projection, glitch flicker, and
//! GPU bloom lane give the *real* depth the CPU parallelogram only faked. The only
//! net-new work here is **placement + state-mapping**; the renderer is reused verbatim
//! (positions ride in through [`CodeVault::with_positions`], the shared-placement
//! seam). Nothing dwarves-shaped leaks in: the same one-way model flows in.
//!
//! ## The convergence-collapse
//!
//! For each crate, the plane(s) already at the **target** (the highest of its three
//! versions — a release drives everything up to it) merge into ONE calm green box at
//! `z = 0` (the goal). Every plane still **behind** the target gets its own box pushed
//! back to its depth (`z = −(plane+1)·gap·separation`), coloured by role and flickering
//! [`NodeState::Glitch`]/[`Changed`] by [severity](ConvergeStatus::severity) — the
//! release/lock blocker throbbing red. A **convergence beam** edge joins each laggard
//! to the target box. `separation = 1 − converged_fraction` (with the view's breathing
//! pulse), so as crates converge the laggard boxes **snap onto** the target and the
//! three planes collapse into one — the animation IS the convergence.

use facett_graph3d::{CodeGraph, CodeNode, NodeState};

use crate::model::{ConvergeStatus, ReleaseConvergence};
use crate::role_color;

/// Depth (scene units) between adjacent planes at full separation. Scaled by the
/// per-frame `separation` so the planes travel from apart → coincident.
const PLANE_GAP: f32 = 0.7;

/// A box below this absolute Z is treated as sitting on the merged/front plane — the
/// "collapsed" band the `state_json` reports and the tests assert against.
const COLLAPSE_EPS: f32 = 1e-4;

/// Which crate / plane a rendered box belongs to — kept parallel to the [`CodeGraph`]
/// nodes so [`crate::ReleaseView`] can round-trip a CodeVault box selection back to a
/// [`crate::Msg::SelectCrate`] (any box of a crate selects that crate) and place the
/// selection on the crate's target box.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BoxMeta {
    /// Index into [`ReleaseConvergence::nodes`] this box represents.
    pub krate: usize,
    /// The role plane this box sits on when it is a **laggard** (`0=local`,
    /// `1=sparring`, `2=cratesio`), or `None` for the merged **target** box.
    pub plane: Option<usize>,
    /// `true` for the merged target box (the convergence goal at `z = 0`).
    pub target: bool,
}

/// Map a crate's [`ConvergeStatus`] to the CodeVault [`NodeState`]: severity-2
/// (needs-release / lock-desync) → the red **glitch** flicker (the blocker throbbing);
/// severity-1 (bump-drift) → steady amber **changed**; else calm **ok**.
fn state_for(status: ConvergeStatus) -> NodeState {
    match status.severity() {
        2 => NodeState::Glitch,
        1 => NodeState::Changed,
        _ => NodeState::Ok,
    }
}

/// A deterministic layered (x, y) placement in ~`[-0.8, 0.8]`: `x` by longest
/// dependency depth, `y` by row within the column. Pure over the edge list so the
/// scene is reproducible/headless-snapshottable.
fn layered_xy(n: usize, edges: &[(usize, usize)]) -> Vec<(f32, f32)> {
    if n == 0 {
        return Vec::new();
    }
    let dense_edges: Vec<_> = edges.iter().filter(|&&(a, b)| a < n && b < n)
        .map(|&(a, b)| (a as u32, b as u32)).collect();
    let layout = facett_core::layered::layered(n, &dense_edges, Default::default());
    let col = &layout.col;
    let row = &layout.row;
    // Keep the CodeVault's existing normalisation and declaration-order rows.
    let max_col = layout.cols.saturating_sub(1).max(1) as f32;
    let max_rows = layout.rows_max.max(1) as f32;
    (0..n)
        .map(|i| {
            let x = (col[i] as f32 / max_col) * 1.6 - 0.8;
            let y = if max_rows > 1.0 { (row[i] as f32 / (max_rows - 1.0)) * 1.6 - 0.8 } else { 0.0 };
            (x, y)
        })
        .collect()
}

/// **Lower the convergence model to a CodeVault [`CodeGraph`] on three depth planes.**
///
/// Returns the graph, the per-box positions (normalised ~`[-1, 1]`, in node-index
/// order — feed straight to [`facett_graph3d::CodeVault::with_positions`]), and a
/// [`BoxMeta`] per box (for the selection round-trip). `separation ∈ [0, 1]` scales the
/// plane depth: `1.0` = fully apart, `0.0` = collapsed onto the target plane.
///
/// A **converged** crate contributes ONE box at its merged `(x, y, 0)`; a **diverged**
/// crate contributes the merged target box PLUS one laggard box per plane still behind
/// the target, joined by convergence-beam edges. Dependency edges connect the crates'
/// target boxes.
pub fn convergence_to_codegraph(
    conv: &ReleaseConvergence,
    separation: f32,
) -> (CodeGraph, Vec<[f32; 3]>, Vec<BoxMeta>) {
    let n = conv.nodes.len();
    let xy = layered_xy(n, &conv.edges.iter().map(|e| (e.from, e.to)).collect::<Vec<_>>());
    let sep = separation.clamp(0.0, 1.0);

    let mut nodes: Vec<CodeNode> = Vec::new();
    let mut positions: Vec<[f32; 3]> = Vec::new();
    let mut meta: Vec<BoxMeta> = Vec::new();
    let mut edges: Vec<(usize, usize)> = Vec::new();
    // The target box index per crate (for dependency edges + selection placement).
    let mut target_box: Vec<usize> = vec![usize::MAX; n];

    for (i, node) in conv.nodes.iter().enumerate() {
        let (x, y) = xy.get(i).copied().unwrap_or((0.0, 0.0));
        let box_state = state_for(node.status);

        // The merged target box (the convergence goal): green, at z = 0.
        let ti = nodes.len();
        target_box[i] = ti;
        nodes.push(CodeNode::new(node.krate.clone(), role_color(2), NodeState::Ok));
        positions.push([x, y, 0.0]);
        meta.push(BoxMeta { krate: i, plane: None, target: true });

        // Which planes are still BEHIND the target (the highest of the three versions)?
        let (l_off, s_off, c_off) = node.chip_divergence();
        let plane_tag = ["L", "S", "C"];
        for (role, diverges) in [l_off, s_off, c_off].into_iter().enumerate() {
            if !diverges {
                continue;
            }
            // Push the laggard back to its plane depth; sep drives the collapse.
            let z = -((role as f32) + 1.0) * PLANE_GAP * sep;
            let bi = nodes.len();
            nodes.push(CodeNode::new(format!("{} {}", node.krate, plane_tag[role]), role_color(role), box_state));
            positions.push([x, y, z]);
            meta.push(BoxMeta { krate: i, plane: Some(role), target: false });
            // The convergence beam: target ← laggard (collapses as z → 0).
            edges.push((ti, bi));
        }
    }

    // Dependency edges between crates' target boxes (front-plane wiring).
    for e in &conv.edges {
        if let (Some(&a), Some(&b)) = (target_box.get(e.from), target_box.get(e.to)) {
            if a != usize::MAX && b != usize::MAX {
                edges.push((a, b));
            }
        }
    }

    (CodeGraph::new(nodes, edges), positions, meta)
}

/// **The headless observable (LAW 6)** of the CodeVault lowering — box tally, glitch
/// count, whether the planes have collapsed, and the per-crate laggard planes + Z, so a
/// robot asserts the convergence-collapse WITHOUT a GPU: all-agree ⇒ `collapsed=true`
/// and one box per crate; a crate live-behind ⇒ a `cratesio`-plane laggard with a
/// nonzero Z.
pub fn vault_state_json(conv: &ReleaseConvergence, separation: f32) -> serde_json::Value {
    let (cg, positions, meta) = convergence_to_codegraph(conv, separation);
    let plane_name = |p: Option<usize>| match p {
        Some(0) => "local",
        Some(1) => "sparring",
        Some(2) => "cratesio",
        _ => "target",
    };
    let glitch_count = cg.nodes.iter().filter(|nd| nd.state == NodeState::Glitch).count();
    let max_abs_z = positions.iter().map(|p| p[2].abs()).fold(0.0_f32, f32::max);
    let collapsed = positions.iter().all(|p| p[2].abs() < COLLAPSE_EPS);

    let crates: Vec<serde_json::Value> = conv
        .nodes
        .iter()
        .enumerate()
        .map(|(ci, node)| {
            let laggards: Vec<serde_json::Value> = meta
                .iter()
                .enumerate()
                .filter(|(_, m)| m.krate == ci && !m.target)
                .map(|(bi, m)| {
                    serde_json::json!({
                        "plane": plane_name(m.plane),
                        "z": positions[bi][2],
                        "state": cg.nodes[bi].state.label(),
                    })
                })
                .collect();
            serde_json::json!({
                "krate": node.krate,
                "status": node.status.label(),
                "boxes": 1 + laggards.len(),
                "laggards": laggards,
            })
        })
        .collect();

    serde_json::json!({
        "renderer": "codevault",
        "planes": crate::PLANES,
        "separation": separation,
        "box_count": positions.len(),
        "crate_count": conv.nodes.len(),
        "glitch_count": glitch_count,
        "converged_fraction": conv.converged_fraction(),
        "collapsed": collapsed,
        "max_abs_z": max_abs_z,
        "crates": crates,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{CrateConvergence, DepEdge};

    fn diverged() -> ReleaseConvergence {
        ReleaseConvergence::new("t")
            .with_node(CrateConvergence::new("skade", "0.5.1", "0.5.1", "0.5.1")) // converged
            .with_node(CrateConvergence::new("dwarves", "0.1.4", "0.1.4", "0.1.3")) // needs-release (cratesio behind)
            .with_edge(DepEdge::new(1, 0))
    }

    #[test]
    fn converged_crate_is_one_box_at_the_merged_plane() {
        let all = ReleaseConvergence::new("t")
            .with_node(CrateConvergence::new("a", "1.0.0", "1.0.0", "1.0.0"))
            .with_node(CrateConvergence::new("b", "2.0.0", "2.0.0", "2.0.0"));
        let (cg, pos, meta) = convergence_to_codegraph(&all, 1.0);
        assert_eq!(cg.nodes.len(), 2, "each converged crate is a single box");
        assert!(meta.iter().all(|m| m.target && m.plane.is_none()), "no laggard boxes when all agree");
        assert!(pos.iter().all(|p| p[2].abs() < COLLAPSE_EPS), "converged boxes sit on the merged plane (z=0)");
    }

    #[test]
    fn crate_live_behind_separates_on_the_cratesio_plane() {
        // dwarves: cratesio 0.1.3 behind local/sparring 0.1.4 → the CRATES_IO plane box
        // is pushed back off z=0 (separated), the target box stays at the goal, and it
        // throbs red (glitch). RED-when-broken: if the laggard doesn't separate, z==0.
        let (cg, pos, meta) = convergence_to_codegraph(&diverged(), 1.0);
        // dwarves is crate index 1.
        let target = meta.iter().position(|m| m.krate == 1 && m.target).unwrap();
        assert!(pos[target][2].abs() < COLLAPSE_EPS, "the target box is on the merged plane");
        let cratesio = meta.iter().position(|m| m.krate == 1 && m.plane == Some(2)).expect("a cratesio laggard box exists");
        assert!(pos[cratesio][2].abs() > COLLAPSE_EPS, "the crates.io laggard is separated in depth");
        assert_eq!(cg.nodes[cratesio].state, NodeState::Glitch, "a needs-release laggard throbs red");
        // The converged crate (skade, idx 0) has no laggard.
        assert!(!meta.iter().any(|m| m.krate == 0 && !m.target), "the converged crate stays a single box");
    }

    #[test]
    fn separation_collapses_the_planes_as_convergence_completes() {
        // Same diverged model: at full separation the crates.io laggard is apart; at
        // zero separation every box has snapped onto the merged plane. RED-when-broken:
        // if separation stops driving depth, the two frames are identical.
        let (_, apart, meta) = convergence_to_codegraph(&diverged(), 1.0);
        let (_, together, _) = convergence_to_codegraph(&diverged(), 0.0);
        let cratesio = meta.iter().position(|m| m.krate == 1 && m.plane == Some(2)).unwrap();
        assert!(apart[cratesio][2].abs() > COLLAPSE_EPS, "apart at separation=1");
        assert!(together.iter().all(|p| p[2].abs() < COLLAPSE_EPS), "collapsed at separation=0");
        assert!(apart[cratesio][2].abs() > together[cratesio][2].abs(), "separation drives the depth");
    }

    #[test]
    fn state_json_reports_collapse_and_the_separated_plane() {
        let j = vault_state_json(&diverged(), 1.0);
        assert_eq!(j["renderer"], "codevault");
        assert_eq!(j["crate_count"], 2);
        assert_eq!(j["box_count"], 3, "2 target boxes + 1 crates.io laggard");
        assert_eq!(j["glitch_count"], 1);
        assert_eq!(j["collapsed"], false, "the diverged model has a separated plane");
        assert!(j["max_abs_z"].as_f64().unwrap() > 0.0);
        // All-agree ⇒ collapsed, convergence 1.0, one box per crate.
        let all = ReleaseConvergence::new("t").with_node(CrateConvergence::new("a", "1.0.0", "1.0.0", "1.0.0"));
        let ja = vault_state_json(&all, 1.0);
        assert_eq!(ja["collapsed"], true);
        assert_eq!(ja["box_count"], 1);
        assert!((ja["converged_fraction"].as_f64().unwrap() - 1.0).abs() < 1e-9);
    }
}