facett-coderoom-view/src/lib.rs
//! # facett-coderoom-view — the shared **world-coderoom** pane
//!
//! The ONE robot-testable egui [`Facet`](facett_core::Facet) that renders the
//! constellation's **world coderoom** — every `crate@version` the coderoom
//! warehouse holds (with its `symbol_facts` / `call_edges`) plus the
//! `semver_diffs` **compat edges** between a producer and the callers that
//! reference it — as an interactive **dependency node-graph**, so every suite
//! surface that wants to show "which crate breaks whom" (nornir's `coderoom`
//! deck · dwarves' release view) mounts **one pane, not a per-app twin** (LAW #5
//! — DRY). It renders:
//!
//! 1. **Dependency graph** — crates as node chips laid out in toposort columns
//! (deps right, consumers left), labelled + sized by crate.
//! 2. **Compat edges** — each `semver_diff` edge coloured **green = compatible**
//! / **red = breaking**, its `suggested_bump` on the arrow label.
//! 3. **What-breaks mode** — given a selected `crate@Z`, the world callers that
//! reference a **removed** symbol (the impact set) are **highlighted** (a red
//! ring + a ✗ badge, kept bright) while everything off the impact path is
//! **dimmed**.
//! 4. **Summary line** — `N crates · M breaking edges · K impacted callers`.
//!
//! ## Layering — the pane is WAREHOUSE-AGNOSTIC (important)
//! facett carries **no** nornir / dwarves / warehouse dependency. The pane is
//! **presentational + host-driven**: the HOST implements the [`CoderoomDataSeam`]
//! trait (feeding the crate nodes + compat edges + the what-breaks result from
//! the dedicated coderoom warehouse), and [`CoderoomPane`] only ever **renders
//! the rows the seam surfaces**. This mirrors how the Release tab takes a
//! `ReleaseConvergence` in and the binary pane takes a `BinaryDataSeam` in — the
//! pane never reaches for a backend (a ONE-WAY adapter: rows flow in, nothing
//! leaks back).
//!
//! ## Reuse — no twinned graph layout
//! The dependency graph is drawn with **`facett-graphview`**: the crate DAG is
//! laid into columns by [`facett_graphview::DepGraphLayout`] (the shared,
//! egui-free toposort layout) and painted by
//! [`facett_graphview::DecoratedGraphView`] (per-edge colour, node rings/badges,
//! pan/zoom, click-select, `state_json`). This pane contributes only the
//! **colour + what-breaks policy** on top of that shared renderer — it invents no
//! layout of its own.
//!
//! A network-free [`StubCoderoomData`] drives the shipped demo + the robot test.
use std::collections::{BTreeSet, HashMap};
use facett_core::look::oklch::Oklch;
use facett_graphview::depgraph_layout::{DepEdge as LayoutEdge, DepGraphLayout};
use facett_graphview::model::{Decorations, GraphEdge, GraphModel, GraphNode, Pos};
use facett_graphview::DecoratedGraphView;
use serde::{Deserialize, Serialize};
pub use facett_core::Facet;
// Re-exported so a host/robot speaks the pane's colour + decoration types without
// depending on facett-graphview directly.
pub use facett_graphview::model::{Color, NodeDecoration};
// ─────────────────────────────────────────────────────────────────────────────
// Data rows — the neutral shapes the HOST feeds over the seam. These mirror the
// coderoom warehouse's tables (crates@versions with symbol_facts, and the
// semver_diffs compat edges) but carry NO warehouse type dependency.
// ─────────────────────────────────────────────────────────────────────────────
/// One `crate@version` node in the world coderoom — a crate at a specific
/// version, plus its symbol count (the coderoom warehouse's `symbol_facts`
/// cardinality) which sizes/annotates the node.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrateNode {
/// Crate name (e.g. `"a"`, `"nornir"`).
pub name: String,
/// The version this node pins (e.g. `"0.2.0"`).
pub version: String,
/// How many public symbols this version exposes (`symbol_facts` count) — the
/// node's size/importance weight.
pub symbols: u64,
}
impl CrateNode {
/// A `name`/`version`/`symbols` node row.
pub fn new(name: impl Into<String>, version: impl Into<String>, symbols: u64) -> Self {
Self { name: name.into(), version: version.into(), symbols }
}
/// The stable node id — `"{name}@{version}"`. Edges + the what-breaks impact
/// set key on this.
pub fn id(&self) -> String {
format!("{}@{}", self.name, self.version)
}
}
/// The semver verdict on a compat edge (a `semver_diffs` row): does the
/// producer's change break the consumer that references it?
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Compat {
/// Non-breaking — the consumer still compiles/links against the new version.
Compatible,
/// Breaking — the producer removed/changed a symbol the consumer references.
Breaking,
}
impl Compat {
/// `true` for [`Compat::Breaking`].
pub fn is_breaking(self) -> bool {
matches!(self, Compat::Breaking)
}
/// A short, stable label for the JSON / arrow.
pub fn label(self) -> &'static str {
match self {
Compat::Compatible => "compatible",
Compat::Breaking => "breaking",
}
}
}
/// One `semver_diffs` **compat edge**: the consumer `from` references the
/// producer `to`; [`compat`](Self::compat) is the verdict for the producer's
/// pending version change, [`suggested_bump`](Self::suggested_bump) the semver
/// bump that change warrants, and [`via`](Self::via) the referenced symbols
/// justifying the edge.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompatEdge {
/// The consumer crate id (`name@version`) — it depends on / references `to`.
pub from: String,
/// The producer crate id (`name@version`) being changed.
pub to: String,
/// The compatibility verdict (green/red).
pub compat: Compat,
/// The semver bump the producer's change warrants (`"major"`, `"minor"`, …).
pub suggested_bump: Option<String>,
/// The referenced symbols justifying the edge (empty is fine).
pub via: Vec<String>,
}
impl CompatEdge {
/// A breaking `from → to` edge (with the referenced symbols + suggested bump).
pub fn breaking(from: impl Into<String>, to: impl Into<String>, bump: impl Into<String>, via: &[&str]) -> Self {
Self {
from: from.into(),
to: to.into(),
compat: Compat::Breaking,
suggested_bump: Some(bump.into()),
via: via.iter().map(|s| s.to_string()).collect(),
}
}
/// A compatible `from → to` edge.
pub fn compatible(from: impl Into<String>, to: impl Into<String>, bump: impl Into<String>, via: &[&str]) -> Self {
Self {
from: from.into(),
to: to.into(),
compat: Compat::Compatible,
suggested_bump: Some(bump.into()),
via: via.iter().map(|s| s.to_string()).collect(),
}
}
}
/// One breaking caller in a what-breaks result: the crate `caller` that references
/// a `symbol` the analysed target removed.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Impact {
/// The breaking caller's crate id (`name@version`).
pub caller: String,
/// The removed symbol the caller referenced.
pub symbol: String,
}
impl Impact {
/// A `caller`/`symbol` impact row.
pub fn new(caller: impl Into<String>, symbol: impl Into<String>) -> Self {
Self { caller: caller.into(), symbol: symbol.into() }
}
}
/// A **what-breaks** result (the coderoom `what-breaks crate@Z` query): the world
/// callers that break because the target `crate@version` removed one or more
/// symbols they reference.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WhatBreaks {
/// The analysed target crate id (`name@version`).
pub target: String,
/// The symbols the target removed at this version.
pub removed_symbols: Vec<String>,
/// The world callers that break (they reference a removed symbol) — the impact
/// set the pane highlights.
pub impacted: Vec<Impact>,
}
impl WhatBreaks {
/// The impacted caller ids, deduplicated.
pub fn impacted_ids(&self) -> BTreeSet<String> {
self.impacted.iter().map(|i| i.caller.clone()).collect()
}
}
/// The per-crate **symbol churn** for one crate that survived across the two
/// snapshots — the *counts* of symbols added / removed / signature-changed. facett
/// carries only the counts the pane renders (not the symbol names): the host maps
/// them from nornir's `crates_changed[].symbols_{added,removed,changed}` arrays.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrateDelta {
/// The crate id (`name@version`) whose public surface changed.
pub crate_id: String,
/// Symbols present in B but not A.
pub symbols_added: u64,
/// Symbols present in A but not B.
pub symbols_removed: u64,
/// Surviving symbols whose signature set changed.
pub symbols_changed: u64,
}
impl CrateDelta {
/// A `crate_id` delta with its `added`/`removed`/`changed` symbol counts.
pub fn new(crate_id: impl Into<String>, added: u64, removed: u64, changed: u64) -> Self {
Self { crate_id: crate_id.into(), symbols_added: added, symbols_removed: removed, symbols_changed: changed }
}
/// The total symbol churn (`added + removed + changed`).
pub fn total(&self) -> u64 {
self.symbols_added + self.symbols_removed + self.symbols_changed
}
}
/// A **snapshot diff** — the corpus-wide "what changed" between two coderoom
/// snapshots `A → B` (nornir's `coderoom snapshot-diff <A> <B>`): the crates added
/// to / removed from the corpus, and the per-crate symbol deltas of the crates that
/// survived but whose surface churned. HOST-fed; the pane only renders it.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CoderoomSnapshotDiff {
/// The "before" snapshot label (`snapshot_id`).
pub snapshot_a: String,
/// The "after" snapshot label (`snapshot_id`).
pub snapshot_b: String,
/// Crate ids (`name@version`) present in B but not A.
pub crates_added: Vec<String>,
/// Crate ids (`name@version`) present in A but not B.
pub crates_removed: Vec<String>,
/// Surviving crates whose symbol surface changed, with the per-crate counts.
pub crates_changed: Vec<CrateDelta>,
}
impl CoderoomSnapshotDiff {
/// `(added, removed, changed)` — the crate-level counts the summary line paints.
pub fn counts(&self) -> (usize, usize, usize) {
(self.crates_added.len(), self.crates_removed.len(), self.crates_changed.len())
}
/// `true` when nothing changed between the two snapshots.
pub fn is_empty(&self) -> bool {
self.crates_added.is_empty() && self.crates_removed.is_empty() && self.crates_changed.is_empty()
}
}
/// What JOINS two crates, with NO verdict: how many dependency edges, item-level
/// calls (and their call sites) and type uses the host's graph holds from `from` to
/// `to`. Drawn in a neutral colour with its counts on the arrow — never green or
/// red, because a count is not a compatibility verdict.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrateLink {
/// The crate id (`name@version`) the edges leave.
pub from: String,
/// The crate id (`name@version`) they arrive at.
pub to: String,
/// Dependency edges (`from` depends on `to`).
pub depends_on: u64,
/// Item → item call edges.
pub calls: u64,
/// The call sites carrying those call edges.
pub call_sites: u64,
/// Item → type use edges.
pub uses_type: u64,
}
impl CrateLink {
/// The arrow label: only the counts that are not zero (`dep · 12 calls`).
pub fn label(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if self.depends_on > 0 {
parts.push("dep".to_string());
}
if self.calls > 0 {
parts.push(format!("{} calls", self.calls));
}
if self.uses_type > 0 {
parts.push(format!("{} types", self.uses_type));
}
parts.join(" · ")
}
}
/// What ONE layer of the pane rests on, in the host's words: the layer's name
/// ([`LAYER_GRAPH`], [`LAYER_VERDICTS`], [`LAYER_DIFF`]), the answer's state by name
/// (`complete`, `partial`, `not_wired`, `not_consumed` …), whether that state is a
/// MEASUREMENT, and the evidence text. The pane draws it as a state chip with the
/// evidence beside it, and — this is the point — draws NO number for a layer that
/// was not measured: zero breaking edges from a layer that never judged anything is
/// not a finding.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LayerState {
/// Which layer: one of the `LAYER_*` names.
pub layer: String,
/// The answer's state, by its own name.
pub state: String,
/// The state is a measurement (the layer's numbers may be drawn).
pub measured: bool,
/// What the answer rests on / why there is none.
pub evidence: String,
}
impl LayerState {
/// A layer's state row.
pub fn new(layer: impl Into<String>, state: impl Into<String>, measured: bool, evidence: impl Into<String>) -> Self {
Self { layer: layer.into(), state: state.into(), measured, evidence: evidence.into() }
}
}
/// The layer of crates and what joins them ([`CoderoomDataSeam::crates`] /
/// [`links`](CoderoomDataSeam::links)).
pub const LAYER_GRAPH: &str = "crate graph";
/// The layer of compatibility verdicts ([`compat_edges`](CoderoomDataSeam::compat_edges)
/// / [`what_breaks`](CoderoomDataSeam::what_breaks)).
pub const LAYER_VERDICTS: &str = "compat verdicts";
/// The layer of the snapshot diff ([`snapshot_diff`](CoderoomDataSeam::snapshot_diff)).
pub const LAYER_DIFF: &str = "snapshot diff";
/// A neutral join / an unjudged edge — a grey that reads as "no verdict".
pub const NEUTRAL: Color = Color::rgb(0x8a, 0x93, 0x9e);
// ─────────────────────────────────────────────────────────────────────────────
// The seam — implemented by the HOST (nornir/dwarves), which feeds rows from the
// dedicated coderoom warehouse. No method reaches for a backend from the pane.
// ─────────────────────────────────────────────────────────────────────────────
/// The world-coderoom **data seam** the [`CoderoomPane`] renders — implemented by
/// the **HOST** (nornir/dwarves), which feeds the rows from the coderoom
/// warehouse's `crates@versions` (+ `symbol_facts`) and its `semver_diffs` compat
/// edges. The pane never computes any of these; it only paints whatever the seam
/// returns.
pub trait CoderoomDataSeam {
/// An honest banner for *where* the coderoom came from, e.g.
/// `"nornir coderoom warehouse"` or `"stub (demo)"`.
fn source(&self) -> &str {
"coderoom"
}
/// Every `crate@version` node in the world coderoom.
fn crates(&self) -> Vec<CrateNode>;
/// The `semver_diffs` compat edges (consumer → producer, with a verdict).
fn compat_edges(&self) -> Vec<CompatEdge>;
/// The **what-breaks** result for a `crate@version` `target` — the world
/// callers referencing a symbol the target removed. `None` ⇒ the target is
/// unknown or nothing breaks (no impact set to highlight).
fn what_breaks(&self, target: &str) -> Option<WhatBreaks>;
/// The **snapshot diff** — the corpus-wide "what changed" between two coderoom
/// snapshots (nornir's `coderoom snapshot-diff <A> <B>`). Default `None`: a host
/// that has no two-snapshot history simply doesn't offer the what-changed mode.
fn snapshot_diff(&self) -> Option<CoderoomSnapshotDiff> {
None
}
/// What joins the crates, with no verdict ([`CrateLink`]): dependency, call and
/// type-use counts. Default none — a host that only has verdict edges feeds
/// [`compat_edges`](Self::compat_edges).
fn links(&self) -> Vec<CrateLink> {
Vec::new()
}
/// What each layer rests on ([`LayerState`]). Default none: a host that says
/// nothing about its layers is drawn as before. A host that names
/// [`LAYER_VERDICTS`] as NOT measured gets no breaking / compatible count, no
/// red or green edge and no impact set drawn — only the state and its evidence.
fn layers(&self) -> Vec<LayerState> {
Vec::new()
}
/// Lines printed under the `via …` banner: WHICH graph this is (workspace,
/// commits, node and edge counts), so a person can compare with the source.
fn identity(&self) -> Vec<String> {
Vec::new()
}
}
/// Which view the [`CoderoomPane`] is showing.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CoderoomMode {
/// The dependency node-graph + what-breaks overlay (the default).
Graph,
/// The snapshot-diff "what changed" list (added / removed / changed crates).
WhatChanged,
}
// ─────────────────────────────────────────────────────────────────────────────
// Colours — green = compatible, red = breaking, amber = the analysed target.
// Node hues reuse facett-core's OKLCH authoring space (no twinned colour maths).
// ─────────────────────────────────────────────────────────────────────────────
/// A compatible edge / OK verdict — a cool green.
pub const GREEN: Color = Color::rgb(0x3f, 0xa6, 0x60);
/// A breaking edge / removed-symbol verdict — a warm red.
pub const RED: Color = Color::rgb(0xd9, 0x53, 0x4f);
/// The analysed what-breaks target ring — an amber.
pub const AMBER: Color = Color::rgb(0xf0, 0xa8, 0x30);
/// A stable fill colour for a crate name — reuses [`Oklch`] (no twinned colour
/// maths): a golden-ratio-ish hue hash at a fixed lightness/chroma so a crate
/// always paints the same hue and adjacent chips read apart.
pub fn crate_color(name: &str) -> Color {
// FNV-1a over the bytes → a stable hue in [0, 360).
let mut h: u64 = 0xcbf29ce484222325;
for b in name.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
let hue = (h % 360) as f32;
Color::from(Oklch::new(0.62, 0.13, hue).to_color32())
}
/// The hex string (`#rrggbb`) for a [`Color`] — the render-truth the robot test
/// asserts an edge painted (red for breaking, green for compatible).
fn hex(c: Color) -> String {
format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)
}
// Layout world-space spacing between the toposort columns/rows.
const COL_SPACING: f32 = 240.0;
const ROW_SPACING: f32 = 88.0;
// ─────────────────────────────────────────────────────────────────────────────
// The pane.
// ─────────────────────────────────────────────────────────────────────────────
/// The shared **world-coderoom pane** — a [`Facet`](facett_core::Facet) the host
/// mounts, driving its host-supplied [`CoderoomDataSeam`]. Presentational: it
/// caches a snapshot of the seam's rows, lays them into a dependency graph via the
/// REUSED [`DecoratedGraphView`], and paints the compat colours + the what-breaks
/// highlight. [`refresh`](Self::refresh) re-pulls from the seam;
/// [`select_target`](Self::select_target) drives the what-breaks mode.
pub struct CoderoomPane {
seam: Box<dyn CoderoomDataSeam>,
title: String,
// Cached snapshot of the seam's rows (re-pulled on `refresh`).
crates: Vec<CrateNode>,
edges: Vec<CompatEdge>,
// The neutral joins (dependency / call / type-use counts), the layer states
// and the identity lines — re-pulled with the rest on `refresh`.
links: Vec<CrateLink>,
layers: Vec<LayerState>,
identity: Vec<String>,
// The what-breaks target (`crate@version`) + its impact set, when in that mode.
target: Option<String>,
what_breaks: Option<WhatBreaks>,
// The snapshot-diff "what changed" payload, when the host feeds one.
diff: Option<CoderoomSnapshotDiff>,
// Which view (dependency graph vs. snapshot diff) is showing.
mode: CoderoomMode,
// The REUSED graph viz, rebuilt from the scene whenever the mode changes.
graph: DecoratedGraphView,
// The host opted the viz into the shared GPU cloud (`with_gpu`): above
// `cloud::RESIDENT_ABOVE` crates it paints on the RESIDENT lane.
gpu: bool,
// `self.edges` that are breaking — counted per refresh, not per frame.
breaking: usize,
// The laid-out world positions — computed per refresh (the layout is a function of
// the crates + edges only), reused by every what-breaks rebuild.
pos: HashMap<String, Pos>,
}
impl CoderoomPane {
/// Build the pane over a host-supplied [`CoderoomDataSeam`], pulling an initial
/// snapshot of its rows and laying out the dependency graph.
pub fn new(seam: Box<dyn CoderoomDataSeam>) -> Self {
let mut pane = Self {
seam,
title: "World coderoom".to_string(),
crates: Vec::new(),
edges: Vec::new(),
links: Vec::new(),
layers: Vec::new(),
identity: Vec::new(),
target: None,
what_breaks: None,
diff: None,
mode: CoderoomMode::Graph,
graph: DecoratedGraphView::new("coderoom deps"),
gpu: false,
breaking: 0,
pos: HashMap::new(),
};
pane.refresh();
pane
}
/// Opt the reused [`DecoratedGraphView`] into the shared instanced GPU cloud
/// ([`DecoratedGraphView::with_gpu`]). Above `cloud::RESIDENT_ABOVE` crates the
/// world paints on the RESIDENT lane: uploaded once per scene (a refresh, a new
/// what-breaks target), the camera alone per frame; rings, badges and on-screen
/// labels stay overlays. Without an installed renderer the CPU painter draws.
#[must_use]
/// The graph canvas's rect on the last paint — what a robot reads to check the graph
/// gets the bulk of the tab.
pub fn graph_rect(&self) -> Option<egui::Rect> {
self.graph.last_paint_rect()
}
pub fn with_gpu(mut self, on: bool) -> Self {
self.gpu = on;
self.rebuild_graph();
self
}
/// Override the pane heading (default `"World coderoom"`).
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
/// Re-pull the whole snapshot from the seam (call when the host has fresh
/// warehouse rows) and re-lay the graph.
pub fn refresh(&mut self) {
self.crates = self.seam.crates();
self.layers = self.seam.layers();
self.identity = self.seam.identity();
self.links = self.seam.links();
// A verdict layer that was NOT measured has no edges to draw: whatever the
// seam hands over, a red or green arrow would be a verdict nobody reached.
self.edges = if self.verdicts_measured() { self.seam.compat_edges() } else { Vec::new() };
self.breaking = self.edges.iter().filter(|e| e.compat.is_breaking()).count();
self.pos = self.layout_positions();
self.diff = self.seam.snapshot_diff();
// The host stopped feeding a diff ⇒ don't strand the pane in a mode it can
// no longer render.
if self.diff.is_none() {
self.mode = CoderoomMode::Graph;
}
self.reload_what_breaks();
self.rebuild_graph();
}
/// Enter (or leave, with `None`) **what-breaks** mode for a `crate@version`
/// target: the seam is asked which world callers break, and the graph is
/// rebuilt with the impact set highlighted + everything else dimmed.
pub fn select_target(&mut self, target: Option<String>) {
self.target = target;
self.reload_what_breaks();
self.rebuild_graph();
}
/// The current what-breaks target (`crate@version`), if any.
pub fn target(&self) -> Option<&str> {
self.target.as_deref()
}
/// The view the pane is showing ([`CoderoomMode::Graph`] by default).
pub fn mode(&self) -> CoderoomMode {
self.mode
}
/// Switch the view. Selecting [`CoderoomMode::WhatChanged`] is a no-op when the
/// host feeds no snapshot diff (there is nothing to render).
pub fn set_mode(&mut self, mode: CoderoomMode) {
if mode == CoderoomMode::WhatChanged && self.diff.is_none() {
return;
}
self.mode = mode;
}
/// The host-fed snapshot diff, if any (`None` ⇒ no what-changed mode offered).
pub fn snapshot_diff(&self) -> Option<&CoderoomSnapshotDiff> {
self.diff.as_ref()
}
/// `(added, removed, changed)` crate counts for the what-changed summary line
/// (all zero when the host feeds no diff).
pub fn diff_counts(&self) -> (usize, usize, usize) {
self.diff.as_ref().map(CoderoomSnapshotDiff::counts).unwrap_or((0, 0, 0))
}
/// The impacted caller ids for the current target (empty when not in
/// what-breaks mode).
pub fn impacted(&self) -> BTreeSet<String> {
self.what_breaks.as_ref().map(WhatBreaks::impacted_ids).unwrap_or_default()
}
/// The ring/badge decorations the pane handed the REUSED viz (read-only) — the
/// what-breaks highlight as data, so a host/robot asserts the impacted rings
/// without a screenshot.
pub fn graph_decorations(&self) -> &HashMap<String, NodeDecoration> {
&self.graph.state().decorations.nodes
}
/// The reused graph viz (read-only) — its lane readouts (`legibility()`), camera
/// and model, for a host that publishes a summary instead of the full state.
pub fn graph(&self) -> &DecoratedGraphView {
&self.graph
}
/// `(crates, breaking_edges, impacted_callers)` — the summary line's numbers.
pub fn summary(&self) -> (usize, usize, usize) {
(self.crates.len(), self.breaking, self.what_breaks.as_ref().map_or(0, |w| w.impacted_ids().len()))
}
fn reload_what_breaks(&mut self) {
// no impact set from a verdict layer that was not measured
self.what_breaks = if self.verdicts_measured() { self.target.as_deref().and_then(|t| self.seam.what_breaks(t)) } else { None };
}
/// The state the host gave for `layer`, if it gave one.
pub fn layer(&self, layer: &str) -> Option<&LayerState> {
self.layers.iter().find(|l| l.layer == layer)
}
/// May the verdict layer's numbers be drawn? Yes when the host says it was
/// measured — and when the host says nothing about its layers at all (the
/// contract before layers existed).
pub fn verdicts_measured(&self) -> bool {
self.layer(LAYER_VERDICTS).is_none_or(|l| l.measured)
}
/// The neutral joins the host fed (dependency / call / type-use counts).
pub fn links(&self) -> &[CrateLink] {
&self.links
}
/// Lay the crate DAG into world positions using the REUSED, egui-free toposort
/// layout, centred around the origin so the default camera frames it.
fn layout_positions(&self) -> HashMap<String, Pos> {
let repos: Vec<String> = self.crates.iter().map(CrateNode::id).collect();
let layout_edges: Vec<LayoutEdge> = self
.edges
.iter()
.map(|e| {
let via: Vec<&str> = e.via.iter().map(String::as_str).collect();
LayoutEdge::new(e.from.clone(), e.to.clone(), &via)
})
// the neutral joins shape the layout too (a dependency is a dependency)
.chain(self.links.iter().map(|l| LayoutEdge::new(l.from.clone(), l.to.clone(), &[])))
.collect();
let lay = DepGraphLayout::build(&repos, &layout_edges, false, &BTreeSet::new());
// Rows-per-column so each column can be centred vertically.
let ncols = lay.nodes.iter().map(|n| n.col).max().unwrap_or(0);
let mut rows_in_col: HashMap<usize, usize> = HashMap::new();
for n in &lay.nodes {
*rows_in_col.entry(n.col).or_insert(0) += 1;
}
let mut out = HashMap::new();
for n in &lay.nodes {
let count = *rows_in_col.get(&n.col).unwrap_or(&1);
let x = (n.col as f32 - ncols as f32 / 2.0) * COL_SPACING;
let y = (n.row as f32 - (count as f32 - 1.0) / 2.0) * ROW_SPACING;
out.insert(n.repo.clone(), Pos::new(x, y));
}
out
}
/// Build the [`GraphModel`] + [`Decorations`] fed to the reused viz. This is
/// the whole colour + what-breaks POLICY: node fills by crate (dimmed off the
/// impact path), edges green/red by verdict, a red ring + ✗ badge on each
/// impacted caller, an amber ring on the analysed target.
fn build_scene(&self) -> (GraphModel, Decorations) {
let pos = &self.pos;
let impacted = self.impacted();
let has_target = self.target.is_some();
let target = self.target.clone();
let max_sym = self.crates.iter().map(|c| c.symbols).max().unwrap_or(1).max(1) as f32;
let mut deco_nodes: HashMap<String, NodeDecoration> = HashMap::new();
let mut nodes: Vec<GraphNode> = Vec::with_capacity(self.crates.len());
for c in &self.crates {
let id = c.id();
let is_target = target.as_deref() == Some(id.as_str());
let is_impacted = impacted.contains(&id);
let base = crate_color(&c.name);
// Dim everything off the impact path once a target is chosen.
let fill = if has_target && !is_target && !is_impacted { base.dim(0.30) } else { base };
let stroke = if has_target && !is_target && !is_impacted { Color::GRAY.dim(0.5) } else { Color::WHITE };
nodes.push(GraphNode {
id: id.clone(),
label: format!("{}@{} · {} sym", c.name, c.version, c.symbols),
fill,
stroke,
pos: *pos.get(&id).unwrap_or(&Pos::new(0.0, 0.0)),
});
// Size-by-crate: a marker-radius multiplier the L0/GPU raster path reads
// (the egui reference chip is a fixed size; the intent is carried here +
// in state_json). Range ~[0.6, 1.6].
let scale = 0.6 + (c.symbols as f32 / max_sym);
let deco = deco_nodes.entry(id.clone()).or_default();
deco.scale = Some(scale.min(1.6));
if has_target {
if is_impacted {
deco.ring = Some(RED);
deco.badge = Some("✗".into());
deco.badge_color = Some(RED);
} else if is_target {
deco.ring = Some(AMBER);
deco.badge = Some("Z".into());
deco.badge_color = Some(AMBER);
}
}
}
let mut edges: Vec<GraphEdge> = self
.edges
.iter()
.map(|e| {
let mut color = if e.compat.is_breaking() { RED } else { GREEN };
// Off the impact path, fade the edge too.
if has_target {
let on_path = impacted.contains(&e.from)
|| impacted.contains(&e.to)
|| target.as_deref() == Some(e.from.as_str())
|| target.as_deref() == Some(e.to.as_str());
if !on_path {
color = color.dim(0.30);
}
}
GraphEdge {
from: e.from.clone(),
to: e.to.clone(),
color,
dashed: false,
label: e.suggested_bump.clone(),
}
})
.collect();
// The neutral joins: grey, dashed when nothing but a dependency joins the
// two, the counts on the arrow. Never a verdict colour.
edges.extend(self.links.iter().map(|l| GraphEdge {
from: l.from.clone(),
to: l.to.clone(),
color: if has_target { NEUTRAL.dim(0.5) } else { NEUTRAL },
dashed: l.calls == 0 && l.uses_type == 0,
label: Some(l.label()).filter(|t| !t.is_empty()),
}));
(GraphModel { nodes, edges }, Decorations { nodes: deco_nodes, edges: Vec::new() })
}
/// Rebuild the embedded [`DecoratedGraphView`] from the current scene. The
/// reused viz's OWN selection is left cleared: the highlight/dim is baked into
/// the model colours + ring decorations, so the seam's impact set (not the
/// graph topology) is authoritative.
///
/// The camera carries over (a new target is a new scene of the SAME graph — the
/// user's pan/zoom must not jump back to zoom 1 on every click).
fn rebuild_graph(&mut self) {
let (model, decorations) = self.build_scene();
let (zoom, px, py) = {
let s = self.graph.state();
(s.zoom, s.pan_x, s.pan_y)
};
self.graph =
DecoratedGraphView::new("coderoom deps").with_model(model).with_decorations(decorations).with_gpu(self.gpu);
self.graph.set_camera(zoom, px, py);
}
/// **Frame the whole world in a `viewport`-sized pane** — the reused viz's
/// [`DecoratedGraphView::fit_model`]. A host that hands the pane a big seam calls it
/// once; the camera then survives every what-breaks rebuild.
pub fn fit_graph(&mut self, viewport: egui::Vec2, margin: f32) {
self.graph.fit_model(viewport, margin);
}
/// Render the snapshot-diff **what-changed** list: a `N added · M removed · K
/// changed` summary, then the added crates (green `+`), the removed crates (red
/// `−`), and the changed crates with their per-crate symbol add/remove/change
/// counts (amber `~`). All real widgets so every row reaches the a11y tree.
fn ui_what_changed(&mut self, ui: &mut egui::Ui) {
let green32 = egui::Color32::from_rgb(GREEN.r, GREEN.g, GREEN.b);
let red32 = egui::Color32::from_rgb(RED.r, RED.g, RED.b);
let amber32 = egui::Color32::from_rgb(AMBER.r, AMBER.g, AMBER.b);
let Some(diff) = &self.diff else {
ui.weak("no snapshot diff available");
return;
};
let (added, removed, changed) = diff.counts();
ui.label(format!("{added} added · {removed} removed · {changed} changed"));
ui.weak(format!("{} → {}", diff.snapshot_a, diff.snapshot_b));
ui.separator();
if !diff.crates_added.is_empty() {
ui.strong("Added");
for c in &diff.crates_added {
ui.colored_label(green32, format!("+ {c}"));
}
}
if !diff.crates_removed.is_empty() {
ui.strong("Removed");
for c in &diff.crates_removed {
ui.colored_label(red32, format!("− {c}"));
}
}
if !diff.crates_changed.is_empty() {
ui.strong("Changed");
for c in &diff.crates_changed {
ui.colored_label(
amber32,
format!("~ {} +{} −{} ~{}", c.crate_id, c.symbols_added, c.symbols_removed, c.symbols_changed),
);
}
}
if diff.is_empty() {
ui.weak("no crates changed between the two snapshots");
}
}
}
impl Facet for CoderoomPane {
fn title(&self) -> &str {
&self.title
}
fn ui(&mut self, ui: &mut egui::Ui) {
// The header (title, identity, layer states, the summary) in a top region with a
// drag bar, the graph below with ALL the remaining height (facett_core::
// graph_split, remembered per viewer) — it used to get only what the header
// left: "only the lower part of the window, and no drag bar".
facett_core::graph_split::header_above(ui, "coderoom", |ui| {
ui.heading(&self.title);
ui.horizontal(|ui| {
ui.weak(format!("via {}", self.seam.source()));
});
// WHICH graph this is — the host's own lines (workspace, commits, counts).
for line in &self.identity {
ui.weak(line);
}
// What each layer rests on: the state by name, then the evidence.
for l in &self.layers {
ui.horizontal_wrapped(|ui| {
let (r, g, b) = if l.measured { (0x00, 0x69, 0x5c) } else { (0xc6, 0x28, 0x28) };
ui.label(
egui::RichText::new(format!(" {} ", l.state.to_uppercase().replace('_', " ")))
.small()
.strong()
.color(egui::Color32::WHITE)
.background_color(egui::Color32::from_rgb(r, g, b)),
);
ui.strong(&l.layer);
ui.label(egui::RichText::new(&l.evidence).small());
});
}
// Mode toggle — the graph/what-breaks view vs. the snapshot-diff "what
// changed" list. The what-changed toggle only appears when the host feeds a
// diff (so a diff-less host shows just the graph).
if self.diff.is_some() {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.mode, CoderoomMode::Graph, "Graph");
ui.selectable_value(&mut self.mode, CoderoomMode::WhatChanged, "What changed");
});
}
// Summary line — real widgets so the numbers reach the a11y tree.
let (n, m, k) = self.summary();
let red32 = egui::Color32::from_rgb(RED.r, RED.g, RED.b);
let amber32 = egui::Color32::from_rgb(AMBER.r, AMBER.g, AMBER.b);
let judged = self.verdicts_measured();
ui.horizontal_wrapped(|ui| {
ui.label(format!("{n} crates"));
if !self.links.is_empty() {
ui.label(format!("{} joins", self.links.len()));
}
if judged {
ui.colored_label(red32, format!("{m} breaking edges"));
} else {
// not "0 breaking edges": nothing judged them
ui.weak("no compat verdict");
}
if let Some(t) = &self.target {
if judged {
ui.colored_label(amber32, format!("{k} impacted callers"));
ui.weak(format!("what-breaks: {t}"));
} else {
ui.weak(format!("selected: {t} — what it breaks is not judged"));
}
}
});
});
match self.mode {
CoderoomMode::Graph => {
// The REUSED dependency-graph viz. Detect a node-box click (it toggles
// the viz's own selection) and adopt it as the what-breaks target.
let before = self.graph.state().selected.clone();
Facet::ui(&mut self.graph, ui);
let after = self.graph.state().selected.clone();
if after != before {
// Re-clicking the current target clears what-breaks mode.
let next = match after {
Some(id) if self.target.as_deref() == Some(id.as_str()) => None,
other => other,
};
self.select_target(next);
}
}
CoderoomMode::WhatChanged => self.ui_what_changed(ui),
}
}
fn state_json(&self) -> serde_json::Value {
let (n, m, k) = self.summary();
let impacted = self.impacted();
let has_target = self.target.is_some();
let crates: Vec<serde_json::Value> = self
.crates
.iter()
.map(|c| {
let id = c.id();
serde_json::json!({
"id": id,
"name": c.name,
"version": c.version,
"symbols": c.symbols,
"impacted": impacted.contains(&id),
"target": self.target.as_deref() == Some(id.as_str()),
})
})
.collect();
let edges: Vec<serde_json::Value> = self
.edges
.iter()
.map(|e| {
let breaking = e.compat.is_breaking();
let color = if breaking { RED } else { GREEN };
serde_json::json!({
"from": e.from,
"to": e.to,
"compat": e.compat.label(),
"breaking": breaking,
"color": hex(color),
"suggested_bump": e.suggested_bump,
"via": e.via,
})
})
.collect();
let judged = self.verdicts_measured();
let summary = match (judged, has_target) {
(true, true) => format!("{n} crates · {m} breaking edges · {k} impacted callers"),
(true, false) => format!("{n} crates · {m} breaking edges"),
(false, _) => format!("{n} crates · {} joins · no compat verdict", self.links.len()),
};
let (da, dr, dc) = self.diff_counts();
let mode = match self.mode {
CoderoomMode::Graph => "graph",
CoderoomMode::WhatChanged => "what_changed",
};
serde_json::json!({
"kind": "coderoom",
"source": self.seam.source(),
"mode": mode,
// The snapshot-diff "what changed" payload (null when the host feeds none).
"snapshot_diff": self.diff,
"diff_added": da,
"diff_removed": dr,
"diff_changed": dc,
"diff_summary": self.diff.as_ref().map(|_| format!("{da} added · {dr} removed · {dc} changed")),
"crate_count": n,
"edge_count": self.edges.len(),
// null, not 0, when no verdict layer was measured: a zero would read as
// "judged, and nothing breaks"
"breaking_edges": judged.then_some(m),
"compatible_edges": judged.then_some(self.edges.len() - m),
"verdicts_measured": judged,
// the neutral joins (dependency / call / type-use counts) and their totals
"link_count": self.links.len(),
"links": self.links,
"depends_on_edges": self.links.iter().map(|l| l.depends_on).sum::<u64>(),
"call_edges": self.links.iter().map(|l| l.calls).sum::<u64>(),
"call_sites": self.links.iter().map(|l| l.call_sites).sum::<u64>(),
"layers": self.layers,
"identity": self.identity,
"target": self.target,
"impacted": impacted.iter().cloned().collect::<Vec<_>>(),
"impacted_count": k,
"removed_symbols": self.what_breaks.as_ref().map(|w| w.removed_symbols.clone()).unwrap_or_default(),
"summary": summary,
"crates": crates,
"edges": edges,
// Proof the REUSED viz actually received the laid-out scene.
"graph_nodes": self.graph.state().model.nodes.len(),
"graph_edges": self.graph.state().model.edges.len(),
})
}
fn update_json(&mut self, msg_json: &str) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) {
if v.get("refresh").and_then(|t| t.as_bool()) == Some(true) {
self.refresh();
}
if v.get("clear").and_then(|t| t.as_bool()) == Some(true) {
self.select_target(None);
}
if let Some(t) = v.get("target").and_then(|t| t.as_str()) {
self.select_target(Some(t.to_string()));
}
if let Some(m) = v.get("mode").and_then(|t| t.as_str()) {
match m {
"graph" => self.set_mode(CoderoomMode::Graph),
"what_changed" => self.set_mode(CoderoomMode::WhatChanged),
_ => {}
}
}
}
}
fn kind(&self) -> &'static str {
"coderoom"
}
}
// ── testmatrix emit shim (real row emitter with `--features testmatrix`, a
// compiled-out no-op otherwise — the facett-binview idiom) ────────────────
/// Emit one functional-status row (no-op unless built `--features testmatrix`).
#[cfg(feature = "testmatrix")]
pub fn emit(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
/// No-op shape compiled when `testmatrix` is OFF (the release path).
#[cfg(not(feature = "testmatrix"))]
#[inline(always)]
pub fn emit(_component: &str, _check: &str, _ok: bool, _detail: &str) {}
// ─────────────────────────────────────────────────────────────────────────────
// A network-free stub seam for the demo + the robot test.
// ─────────────────────────────────────────────────────────────────────────────
/// A **stub** [`CoderoomDataSeam`] with a deterministic small world: crate `a`
/// (at `a@0.2.0`) is referenced by callers `b`, `c`, `d`, `e`. At `0.2.0`, `a`
/// **drops `foo`** — so `b`, `c`, `d` (which reference `foo`) **break** (red
/// edges), while `e` references the still-present `bar` and stays **compatible**
/// (a green edge). The what-breaks result for `a@0.2.0` is exactly the three
/// callers `{b, c, d}` — NOT `e`, proving the impact set is the seam's authority,
/// not merely "every dependent".
pub struct StubCoderoomData;
const STUB_A: &str = "a@0.2.0";
const STUB_B: &str = "b@1.0.0";
const STUB_C: &str = "c@1.1.0";
const STUB_D: &str = "d@0.9.0";
const STUB_E: &str = "e@2.0.0";
// A crate NOT in the current corpus — the snapshot-diff's "added" row.
const STUB_F: &str = "f@0.1.0";
impl CoderoomDataSeam for StubCoderoomData {
fn source(&self) -> &str {
"stub (demo)"
}
fn crates(&self) -> Vec<CrateNode> {
vec![
CrateNode::new("a", "0.2.0", 12),
CrateNode::new("b", "1.0.0", 8),
CrateNode::new("c", "1.1.0", 6),
CrateNode::new("d", "0.9.0", 5),
CrateNode::new("e", "2.0.0", 7),
]
}
fn compat_edges(&self) -> Vec<CompatEdge> {
vec![
CompatEdge::breaking(STUB_B, STUB_A, "major", &["foo"]),
CompatEdge::breaking(STUB_C, STUB_A, "major", &["foo"]),
CompatEdge::breaking(STUB_D, STUB_A, "major", &["foo"]),
CompatEdge::compatible(STUB_E, STUB_A, "minor", &["bar"]),
]
}
fn what_breaks(&self, target: &str) -> Option<WhatBreaks> {
if target != STUB_A {
return None;
}
Some(WhatBreaks {
target: STUB_A.to_string(),
removed_symbols: vec!["foo".to_string()],
impacted: vec![
Impact::new(STUB_B, "foo"),
Impact::new(STUB_C, "foo"),
Impact::new(STUB_D, "foo"),
],
})
}
/// A deterministic snapshot diff `snap-A → snap-B`: crate `f@0.1.0` was **added**
/// to the corpus, `d@0.9.0` was **removed**, and the producer `a@0.2.0` survived
/// but **churned** (2 symbols added, 1 removed — `foo` — and 3 signatures
/// changed) — so the what-changed mode always demos one of each row.
fn snapshot_diff(&self) -> Option<CoderoomSnapshotDiff> {
Some(CoderoomSnapshotDiff {
snapshot_a: "snap-A".to_string(),
snapshot_b: "snap-B".to_string(),
crates_added: vec![STUB_F.to_string()],
crates_removed: vec![STUB_D.to_string()],
crates_changed: vec![CrateDelta::new(STUB_A, 2, 1, 3)],
})
}
}
/// The stub target (`a@0.2.0`) — the crate that dropped `foo`. Exposed so the
/// demo + the robot test drive the what-breaks mode without hard-coding the id.
pub const STUB_TARGET: &str = STUB_A;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stub_snapshot_reads_through_state_json() {
let pane = CoderoomPane::new(Box::new(StubCoderoomData));
let s = pane.state_json();
assert_eq!(s["kind"], "coderoom");
assert_eq!(s["crate_count"], 5);
assert_eq!(s["edge_count"], 4);
assert_eq!(s["breaking_edges"], 3);
assert_eq!(s["compatible_edges"], 1);
// The reused viz received all 5 nodes + 4 edges.
assert_eq!(s["graph_nodes"], 5);
assert_eq!(s["graph_edges"], 4);
// Breaking edges paint red, the compatible one green.
let edges = s["edges"].as_array().unwrap();
let breaking: Vec<_> = edges.iter().filter(|e| e["breaking"] == true).collect();
assert_eq!(breaking.len(), 3);
assert!(breaking.iter().all(|e| e["color"] == hex(RED)));
let compat: Vec<_> = edges.iter().filter(|e| e["breaking"] == false).collect();
assert_eq!(compat.len(), 1);
assert_eq!(compat[0]["color"], hex(GREEN));
// No target chosen yet ⇒ no impact set.
assert!(s["target"].is_null());
assert_eq!(s["impacted_count"], 0);
}
#[test]
fn what_breaks_highlights_exactly_the_three_callers() {
let mut pane = CoderoomPane::new(Box::new(StubCoderoomData));
pane.select_target(Some(STUB_TARGET.to_string()));
let s = pane.state_json();
assert_eq!(s["target"], STUB_TARGET);
assert_eq!(s["impacted_count"], 3);
let impacted: BTreeSet<String> =
s["impacted"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect();
assert_eq!(impacted, [STUB_B, STUB_C, STUB_D].iter().map(|s| s.to_string()).collect());
// e depends on a but does NOT break — the impact set is the seam's authority.
assert!(!impacted.contains(STUB_E));
assert_eq!(s["removed_symbols"].as_array().unwrap().len(), 1);
assert_eq!(s["summary"], "5 crates · 3 breaking edges · 3 impacted callers");
}
#[test]
fn impacted_callers_get_a_red_ring_in_the_reused_viz() {
let mut pane = CoderoomPane::new(Box::new(StubCoderoomData));
pane.select_target(Some(STUB_TARGET.to_string()));
// The scene handed to the reused viz rings the 3 impacted callers (red) +
// the target (amber) = 4 rings; the untouched `e` gets none.
let decos = &pane.graph.state().decorations.nodes;
let ring_of = |id: &str| decos.get(id).and_then(|d| d.ring);
assert_eq!(ring_of(STUB_B), Some(RED));
assert_eq!(ring_of(STUB_C), Some(RED));
assert_eq!(ring_of(STUB_D), Some(RED));
assert_eq!(ring_of(STUB_A), Some(AMBER));
assert_eq!(ring_of(STUB_E), None);
}
#[test]
fn layout_places_the_producer_right_of_its_consumers() {
let pane = CoderoomPane::new(Box::new(StubCoderoomData));
let pos = pane.layout_positions();
let a_x = pos.get(STUB_A).unwrap().x;
// Every consumer sits strictly to the LEFT of the producer `a` (deps right).
for caller in [STUB_B, STUB_C, STUB_D, STUB_E] {
assert!(pos.get(caller).unwrap().x < a_x, "{caller} is left of the producer");
}
}
#[test]
fn empty_seam_does_not_panic() {
struct Empty;
impl CoderoomDataSeam for Empty {
fn crates(&self) -> Vec<CrateNode> {
Vec::new()
}
fn compat_edges(&self) -> Vec<CompatEdge> {
Vec::new()
}
fn what_breaks(&self, _target: &str) -> Option<WhatBreaks> {
None
}
}
let pane = CoderoomPane::new(Box::new(Empty));
let s = pane.state_json();
assert_eq!(s["crate_count"], 0);
assert_eq!(s["graph_nodes"], 0);
assert_eq!(pane.summary(), (0, 0, 0));
}
#[test]
fn reselecting_the_target_clears_what_breaks_mode() {
let mut pane = CoderoomPane::new(Box::new(StubCoderoomData));
pane.select_target(Some(STUB_TARGET.to_string()));
assert_eq!(pane.target(), Some(STUB_TARGET));
pane.select_target(None);
assert!(pane.target().is_none());
assert_eq!(pane.impacted().len(), 0);
}
#[test]
fn stub_snapshot_diff_reads_through_state_json() {
let pane = CoderoomPane::new(Box::new(StubCoderoomData));
// Default mode is the graph; the diff payload is still surfaced.
assert_eq!(pane.mode(), CoderoomMode::Graph);
let s = pane.state_json();
assert_eq!(s["mode"], "graph");
assert_eq!(s["diff_added"], 1);
assert_eq!(s["diff_removed"], 1);
assert_eq!(s["diff_changed"], 1);
assert_eq!(s["diff_summary"], "1 added · 1 removed · 1 changed");
let d = &s["snapshot_diff"];
assert_eq!(d["crates_added"][0], "f@0.1.0");
assert_eq!(d["crates_removed"][0], "d@0.9.0");
assert_eq!(d["crates_changed"][0]["crate_id"], "a@0.2.0");
assert_eq!(d["crates_changed"][0]["symbols_added"], 2);
assert_eq!(d["crates_changed"][0]["symbols_removed"], 1);
assert_eq!(d["crates_changed"][0]["symbols_changed"], 3);
}
#[test]
fn set_mode_toggles_what_changed_and_state_json_follows() {
let mut pane = CoderoomPane::new(Box::new(StubCoderoomData));
pane.set_mode(CoderoomMode::WhatChanged);
assert_eq!(pane.mode(), CoderoomMode::WhatChanged);
assert_eq!(pane.state_json()["mode"], "what_changed");
// Toggling back to the graph works.
pane.set_mode(CoderoomMode::Graph);
assert_eq!(pane.mode(), CoderoomMode::Graph);
assert_eq!(pane.diff_counts(), (1, 1, 1));
}
#[test]
fn what_changed_mode_is_refused_without_a_diff() {
struct NoDiff;
impl CoderoomDataSeam for NoDiff {
fn crates(&self) -> Vec<CrateNode> {
vec![CrateNode::new("a", "0.1.0", 1)]
}
fn compat_edges(&self) -> Vec<CompatEdge> {
Vec::new()
}
fn what_breaks(&self, _target: &str) -> Option<WhatBreaks> {
None
}
// snapshot_diff defaults to None.
}
let mut pane = CoderoomPane::new(Box::new(NoDiff));
assert!(pane.snapshot_diff().is_none());
// The mode cannot be entered with no diff to render.
pane.set_mode(CoderoomMode::WhatChanged);
assert_eq!(pane.mode(), CoderoomMode::Graph);
assert!(pane.state_json()["snapshot_diff"].is_null());
assert_eq!(pane.diff_counts(), (0, 0, 0));
}
/// A graph with joins and NO verdict layer: the host says the verdicts were not
/// measured, hands neutral joins — and, wrongly, verdict edges and an impact set.
struct Unjudged;
impl CoderoomDataSeam for Unjudged {
fn source(&self) -> &str {
"a host graph"
}
fn crates(&self) -> Vec<CrateNode> {
vec![CrateNode::new("app", "1.0.0", 40), CrateNode::new("lib", "0.3.0", 12), CrateNode::new("dep", "2.1.0", 0)]
}
// a verdict edge the pane must NOT draw while the layer is unmeasured
fn compat_edges(&self) -> Vec<CompatEdge> {
vec![CompatEdge::breaking("app@1.0.0", "lib@0.3.0", "major", &["gone"])]
}
fn what_breaks(&self, _target: &str) -> Option<WhatBreaks> {
Some(WhatBreaks { target: "lib@0.3.0".into(), removed_symbols: vec!["gone".into()], impacted: vec![Impact::new("app@1.0.0", "gone")] })
}
fn links(&self) -> Vec<CrateLink> {
vec![
CrateLink { from: "app@1.0.0".into(), to: "lib@0.3.0".into(), depends_on: 1, calls: 7, call_sites: 19, uses_type: 2 },
CrateLink { from: "lib@0.3.0".into(), to: "dep@2.1.0".into(), depends_on: 1, ..Default::default() },
]
}
fn layers(&self) -> Vec<LayerState> {
vec![
LayerState::new(LAYER_GRAPH, "complete", true, ""),
LayerState::new(LAYER_VERDICTS, "not_wired", false, "no producer writes compat verdicts"),
]
}
fn identity(&self) -> Vec<String> {
vec!["workspace w · app@3 (0123456789) · 52 nodes · 9 edges".into()]
}
}
/// The joins are drawn with their counts and no verdict colour; the unmeasured
/// verdict layer draws its state and NO number — not "0 breaking edges", and not
/// the red edge / impact set the seam handed over anyway.
#[test]
fn an_unmeasured_verdict_layer_draws_its_state_and_no_verdict() {
let mut pane = CoderoomPane::new(Box::new(Unjudged));
pane.select_target(Some("lib@0.3.0".to_string()));
let s = pane.state_json();
assert_eq!((s["crate_count"].as_u64(), s["link_count"].as_u64()), (Some(3), Some(2)));
assert_eq!((s["depends_on_edges"].as_u64(), s["call_edges"].as_u64(), s["call_sites"].as_u64()), (Some(2), Some(7), Some(19)));
assert_eq!(s["links"][0]["calls"], 7);
// the verdict layer: a state, no number
assert_eq!(s["verdicts_measured"], false);
assert!(s["breaking_edges"].is_null() && s["compatible_edges"].is_null(), "no count from an unmeasured layer: {s}");
assert_eq!((s["edge_count"].as_u64(), s["impacted_count"].as_u64()), (Some(0), Some(0)));
assert!(s["removed_symbols"].as_array().unwrap().is_empty());
assert_eq!(s["summary"], "3 crates · 2 joins · no compat verdict");
assert_eq!(s["layers"][1]["state"], "not_wired");
assert_eq!(s["identity"][0], "workspace w · app@3 (0123456789) · 52 nodes · 9 edges");
// the reused viz got the two joins, neutral — never red or green
let model = &pane.graph().state().model;
assert_eq!((model.nodes.len(), model.edges.len()), (3, 2));
assert!(model.edges.iter().all(|e| e.color != RED && e.color != GREEN));
assert_eq!(model.edges[0].label.as_deref(), Some("dep · 7 calls · 2 types"));
assert!(model.edges[1].dashed && !model.edges[0].dashed, "a join that is only a dependency is dashed");
// no ring on the crate the seam called impacted
assert!(pane.graph_decorations().get("app@1.0.0").is_none_or(|d| d.ring.is_none()));
}
/// A host that names no layers is drawn as before (the stub's verdicts count).
#[test]
fn a_host_without_layer_states_keeps_its_verdicts() {
let pane = CoderoomPane::new(Box::new(StubCoderoomData));
let s = pane.state_json();
assert_eq!((s["verdicts_measured"].as_bool(), s["breaking_edges"].as_u64(), s["link_count"].as_u64()), (Some(true), Some(3), Some(0)));
}
}