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