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

nordisk/facett

facet-wrapped/src/forge.rs

//! **The forge family** (added 2026-09-29, inventory item 6): the CI/CD forge panel,
//! the brokkr run roster and the holger registry browser — three of the crates the
//! inventory listed as "no data seam yet", wrapped on the registry's terms: the payload
//! carries facts (a status word, a count, a name), the component decides the pixels, and
//! an empty payload is a refusal, never a fixture.
//!
//! Status words are parsed leniently and never flatteringly: an unknown word is the
//! component's neutral state (`queued` for the forge), never `passed`.

use facett_core::Facet;
use serde_json::Value;

use crate::registry::{Entry, OPS, Registry};

/// Register the forge family.
pub(crate) fn register(r: &mut Registry) {
    r.register(
        Entry::new("cicd", "A CI/CD forge: pipeline roster, run history, stages, steps, log tail.", || {
            Box::new(facett_cicd::CicdPanel::demo())
        })
        .in_group(OPS)
        .with_data(cicd),
    );
    r.register(
        Entry::new("brokkrview", "brokkr run results: the run roster (a dwarves Brokkr state).", || {
            Box::new(facett_brokkrview::BrokkrView::demo("brokkr"))
        })
        .in_group(OPS)
        .with_data(brokkrview),
    );
    r.register(
        Entry::new("holger", "The holger registry: repository roster + the selected repo's artifacts.", || {
            Box::new(facett_holger::RegistryBrowser::from_fixture())
        })
        .in_group(OPS)
        .with_data(holger),
    );
    r.register(
        Entry::new("releaseview", "Release convergence: each crate's local / sparring / crates.io version over its dependency DAG.", || {
            use facett_releaseview::{CrateConvergence, DepEdge, ReleaseConvergence, ReleaseView};
            Box::new(ReleaseView::from_model(
                ReleaseConvergence::new("release")
                    .with_node(CrateConvergence::new("facett-core", "0.1.19", "0.1.19", "0.1.18"))
                    .with_node(CrateConvergence::new("facett-graph", "0.1.19", "0.1.19", "0.1.19"))
                    .with_edge(DepEdge::new(1, 0)),
            ))
        })
        .in_group(OPS)
        .with_data(releaseview),
    );
    r.register(
        Entry::new("binview", "Binary analysis of a built artifact: checksec badges, crate treemap, size diff.", || {
            Box::new(facett_binview::BinaryPane::new(Box::new(facett_binview::StubBinaryData)))
        })
        .in_group(OPS)
        .with_data(binview),
    );
    r.register(
        Entry::new("coderoom", "The world coderoom: crate@version graph, compat edges, what-breaks overlay.", || {
            Box::new(facett_coderoom_view::CoderoomPane::new(Box::new(facett_coderoom_view::StubCoderoomData)))
        })
        .in_group(OPS)
        .with_data(coderoom),
    );
    r.register(
        Entry::new("fleet", "The fleet coverage board: members × surfaces from a testmatrix run, under the Gap verdict.", || {
            Box::new(facett_fleet_view::FleetView::local())
        })
        .in_group(OPS)
        .with_data(fleet),
    );
    r.register(
        Entry::new("buildspace", "A repo's evaluated feature axis: features → crates, the AND-groups that keep a crate in.", || {
            buildspace(&buildspace_fixture()).expect("the fixture is a valid axis")
        })
        .in_group(OPS)
        .with_data(buildspace),
    );
}

fn s<'a>(v: &'a Value, k: &str) -> &'a str {
    v.get(k).and_then(Value::as_str).unwrap_or("")
}

/// `passed`/`ok`/`success` → Passed, `failed`/`error` → Failed, `running` → Running,
/// anything else → Queued (neutral, never flattering).
fn run_status(word: &str) -> facett_cicd::RunStatus {
    use facett_cicd::RunStatus as R;
    match word.trim().to_ascii_lowercase().as_str() {
        "passed" | "pass" | "ok" | "success" | "succeeded" | "done" | "green" => R::Passed,
        "failed" | "fail" | "error" | "failure" | "crashed" | "red" => R::Failed,
        "running" | "in_progress" | "started" => R::Running,
        _ => R::Queued,
    }
}

/// **`cicd`** — [`facett_cicd::CicdPanel`].
///
/// ```json
/// { "title": "forge",
///   "pipelines": [{ "id": "nornir-ci", "name": "nornir CI", "trigger": "push",
///     "runs": [{ "id": "412", "status": "passed", "trigger": "push", "commit": "a1b2c3d",
///                "duration_s": 312.0, "stages": [{ "name": "build", "status": "passed", "duration_s": 120.0 }],
///                "steps": [{ "name": "cargo test", "status": "passed" }], "log": ["ok"] }] }] }
/// ```
/// Runs newest first. A pipeline needs an `id`; no pipelines is a refusal.
#[must_use]
pub fn cicd(v: &Value) -> Option<Box<dyn Facet>> {
    use facett_cicd::{Pipeline, Run, RunStep, StageRun};
    let arr = |x: &Value, k: &str| x.get(k).and_then(Value::as_array).cloned().unwrap_or_default();
    let pipelines: Vec<Pipeline> = arr(v, "pipelines")
        .iter()
        .filter(|p| !s(p, "id").trim().is_empty())
        .map(|p| {
            let runs = arr(p, "runs")
                .iter()
                .filter(|r| !s(r, "id").is_empty())
                .map(|r| {
                    Run::new(s(r, "id"), run_status(s(r, "status")), s(r, "trigger"))
                        .with_commit(s(r, "commit"))
                        .with_duration(r.get("duration_s").and_then(Value::as_f64).unwrap_or(0.0) as f32)
                        .with_stages(
                            arr(r, "stages")
                                .iter()
                                .map(|st| {
                                    StageRun::new(
                                        s(st, "name"),
                                        run_status(s(st, "status")),
                                        st.get("duration_s").and_then(Value::as_f64).unwrap_or(0.0) as f32,
                                    )
                                })
                                .collect(),
                        )
                        .with_steps(arr(r, "steps").iter().map(|st| RunStep::new(s(st, "name"), run_status(s(st, "status")))).collect())
                        .with_log(arr(r, "log").iter().filter_map(|l| l.as_str().map(str::to_string)).collect::<Vec<_>>())
                })
                .collect();
            let name = if s(p, "name").is_empty() { s(p, "id") } else { s(p, "name") };
            Pipeline::new(s(p, "id"), name, s(p, "trigger"), runs)
        })
        .collect();
    if pipelines.is_empty() {
        return None;
    }
    let title = if s(v, "title").is_empty() { "CI/CD" } else { s(v, "title") };
    Some(Box::new(facett_cicd::CicdPanel::new(title, pipelines)))
}

/// **`brokkrview`** — [`facett_brokkrview::BrokkrView::from_brokkr_state`], dwarves'
/// `Brokkr::state_json()` roster wire, handed across as is:
///
/// ```json
/// { "title": "brokkr", "pipelines": ["nornir-ci"], "selected": "nornir-ci",
///   "runs": [{ "run_id": "r7", "pipeline": "nornir-ci", "status": "passed", "trigger": "push",
///              "sha": "a1b2c3d", "steps": "4/4", "duration": "1.2s" }], "log": [] }
/// ```
/// No pipelines and no runs is a refusal.
#[must_use]
pub fn brokkrview(v: &Value) -> Option<Box<dyn Facet>> {
    let has = |k: &str| v.get(k).and_then(Value::as_array).is_some_and(|a| !a.is_empty());
    if !has("pipelines") && !has("runs") {
        return None;
    }
    let title = if s(v, "title").is_empty() { "brokkr" } else { s(v, "title") };
    Some(Box::new(facett_brokkrview::BrokkrView::from_brokkr_state(title, v)))
}

/// **`holger`** — [`facett_holger::RegistryBrowser::from_model`].
///
/// ```json
/// { "repos": [{ "name": "crates-mirror", "kind": "rust", "writable": false, "has_archive": true,
///               "artifact_count": 1200 }],
///   "selected_repo": "crates-mirror",
///   "artifacts": [{ "name": "serde", "version": "1.0.228", "size_bytes": 78000,
///                   "content_type": "application/x-rust-crate" }] }
/// ```
/// A repo needs a `name`; no repos is a refusal. `selected_repo` defaults to the first.
#[must_use]
pub fn holger(v: &Value) -> Option<Box<dyn Facet>> {
    use facett_holger::{ArtifactEntry, RegistryModel, RepoEntry};
    let b = |x: &Value, k: &str| x.get(k).and_then(Value::as_bool).unwrap_or(false);
    let u = |x: &Value, k: &str| x.get(k).and_then(Value::as_u64).unwrap_or(0);
    let repos: Vec<RepoEntry> = v
        .get("repos")
        .and_then(Value::as_array)?
        .iter()
        .filter(|r| !s(r, "name").trim().is_empty())
        .map(|r| RepoEntry {
            name: s(r, "name").to_string(),
            kind: s(r, "kind").to_string(),
            writable: b(r, "writable"),
            has_archive: b(r, "has_archive"),
            artifact_count: u(r, "artifact_count"),
        })
        .collect();
    if repos.is_empty() {
        return None;
    }
    let artifacts = v
        .get("artifacts")
        .and_then(Value::as_array)
        .map(|a| {
            a.iter()
                .filter(|x| !s(x, "name").is_empty())
                .map(|x| ArtifactEntry {
                    name: s(x, "name").to_string(),
                    version: s(x, "version").to_string(),
                    size_bytes: u(x, "size_bytes"),
                    content_type: s(x, "content_type").to_string(),
                })
                .collect()
        })
        .unwrap_or_default();
    let selected_repo = if s(v, "selected_repo").is_empty() { repos[0].name.clone() } else { s(v, "selected_repo").to_string() };
    Some(Box::new(facett_holger::RegistryBrowser::from_model(RegistryModel { repos, artifacts, selected_repo })))
}

/// **`releaseview`** — [`facett_releaseview::ReleaseView`], the release-convergence DAG.
///
/// ```json
/// { "title": "nordisk release-3",
///   "nodes": [{ "krate": "facett-core", "local": "0.1.19", "sparring": "0.1.19", "cratesio": "0.1.18" }],
///   "edges": [[1, 0]] }
/// ```
/// Three versions per crate are the FACTS; the status (converged / needs release / lock
/// desync / bump drift) is the component's own `derive_status`, never the caller's.
/// Edges are `[from, to]` node indices (or `{from, to}`); an index out of range is
/// dropped. No nodes is a refusal.
#[must_use]
pub fn releaseview(v: &Value) -> Option<Box<dyn Facet>> {
    use facett_releaseview::{CrateConvergence, DepEdge, ReleaseConvergence};
    let nodes: Vec<CrateConvergence> = v
        .get("nodes")
        .and_then(Value::as_array)?
        .iter()
        .filter(|n| !s(n, "krate").trim().is_empty())
        .map(|n| CrateConvergence::new(s(n, "krate"), s(n, "local"), s(n, "sparring"), s(n, "cratesio")))
        .collect();
    if nodes.is_empty() {
        return None;
    }
    let idx = |x: Option<&Value>| x.and_then(Value::as_u64).map(|i| i as usize);
    let mut conv = ReleaseConvergence::new(if s(v, "title").is_empty() { "release" } else { s(v, "title") });
    let len = nodes.len();
    for n in nodes {
        conv = conv.with_node(n);
    }
    for e in v.get("edges").and_then(Value::as_array).into_iter().flatten() {
        let (f, t) = match e {
            Value::Array(a) => (idx(a.first()), idx(a.get(1))),
            o => (idx(o.get("from")), idx(o.get("to"))),
        };
        if let (Some(f), Some(t)) = (f, t)
            && f < len
            && t < len
        {
            conv = conv.with_edge(DepEdge::new(f, t));
        }
    }
    Some(Box::new(facett_releaseview::ReleaseView::from_model(conv)))
}

/// A caller-supplied binary analysis, as the [`facett_binview::BinaryDataSeam`] the pane
/// renders. The pane computes nothing; neither does this — it hands the facts over.
struct PayloadBinary {
    name: String,
    checksec: facett_binview::Checksec,
    crates: Vec<facett_binview::CrateSize>,
    symbols: Vec<facett_binview::SymbolSize>,
    diff: Option<facett_binview::ArtifactDiff>,
    history: Vec<u64>,
}

impl facett_binview::BinaryDataSeam for PayloadBinary {
    fn source(&self) -> &str {
        "caller payload"
    }
    fn artifact_name(&self) -> String {
        self.name.clone()
    }
    fn checksec(&self) -> facett_binview::Checksec {
        self.checksec.clone()
    }
    fn crate_sizes(&self) -> Vec<facett_binview::CrateSize> {
        self.crates.clone()
    }
    fn symbol_sizes(&self) -> Vec<facett_binview::SymbolSize> {
        self.symbols.clone()
    }
    fn diff(&self) -> Option<facett_binview::ArtifactDiff> {
        self.diff.clone()
    }
    fn size_history(&self) -> Vec<u64> {
        self.history.clone()
    }
}

/// **`binview`** — [`facett_binview::BinaryPane`], a built artifact's analysis.
///
/// ```json
/// { "artifact": "nornir",
///   "checksec": { "relro": "Full", "pie": true, "nx": true, "stack_canary": true, "stripped": false, "fortify": true },
///   "crate_sizes": [{ "crate_name": "std", "bytes": 812000 }],
///   "symbol_sizes": [{ "crate_name": "std", "symbol": "fmt::write", "bytes": 4200 }],
///   "size_history": [9100000, 9240000] }
/// ```
/// `checksec` and `diff` are the component's own serde shapes; `artifact` and a
/// non-empty, parseable `crate_sizes` are required, and a malformed `checksec` is a
/// refusal (a hardening verdict must never be guessed).
#[must_use]
pub fn binview(v: &Value) -> Option<Box<dyn Facet>> {
    let name = s(v, "artifact");
    if name.trim().is_empty() {
        return None;
    }
    let parse = |k: &str| v.get(k).cloned();
    let checksec: facett_binview::Checksec = serde_json::from_value(parse("checksec")?).ok()?;
    let crates: Vec<facett_binview::CrateSize> = serde_json::from_value(parse("crate_sizes")?).ok()?;
    if crates.is_empty() {
        return None;
    }
    let symbols = parse("symbol_sizes").and_then(|x| serde_json::from_value(x).ok()).unwrap_or_default();
    let diff = parse("diff").and_then(|x| serde_json::from_value(x).ok());
    let history = parse("size_history").and_then(|x| serde_json::from_value(x).ok()).unwrap_or_default();
    let seam = PayloadBinary { name: name.to_string(), checksec, crates, symbols, diff, history };
    Some(Box::new(facett_binview::BinaryPane::new(Box::new(seam)).with_title(if s(v, "title").is_empty() { name } else { s(v, "title") })))
}

/// A caller-supplied world coderoom as the pane's own [`facett_coderoom_view::CoderoomDataSeam`].
/// `what_breaks` is a LOOKUP into the payload's precomputed answers, never a computation:
/// a target the payload did not answer is "nothing to highlight", as the trait defines.
struct PayloadCoderoom {
    crates: Vec<facett_coderoom_view::CrateNode>,
    edges: Vec<facett_coderoom_view::CompatEdge>,
    breaks: Vec<facett_coderoom_view::WhatBreaks>,
    diff: Option<facett_coderoom_view::CoderoomSnapshotDiff>,
}

impl facett_coderoom_view::CoderoomDataSeam for PayloadCoderoom {
    fn source(&self) -> &str {
        "caller payload"
    }
    fn crates(&self) -> Vec<facett_coderoom_view::CrateNode> {
        self.crates.clone()
    }
    fn compat_edges(&self) -> Vec<facett_coderoom_view::CompatEdge> {
        self.edges.clone()
    }
    fn what_breaks(&self, target: &str) -> Option<facett_coderoom_view::WhatBreaks> {
        self.breaks.iter().find(|w| w.target == target).cloned()
    }
    fn snapshot_diff(&self) -> Option<facett_coderoom_view::CoderoomSnapshotDiff> {
        self.diff.clone()
    }
}

/// **`coderoom`** — [`facett_coderoom_view::CoderoomPane`], the world-coderoom graph.
///
/// ```json
/// { "crates": [{ "name": "facett-core", "version": "0.1.19", "symbols": 5200 }],
///   "compat_edges": [{ "from": "facett-graph@0.1.19", "to": "facett-core@0.1.19", "compat": "Compatible",
///                      "suggested_bump": "patch", "via": [] }],
///   "what_breaks": [{ "target": "facett-core@0.1.19", "removed_symbols": ["draw"],
///                     "impacted": [{ "caller": "facett-graph@0.1.19", "symbol": "draw" }] }],
///   "snapshot_diff": null }
/// ```
/// All in the component's own serde shapes; a malformed list is a refusal, and no crates
/// is a refusal.
#[must_use]
pub fn coderoom(v: &Value) -> Option<Box<dyn Facet>> {
    fn list<T: serde::de::DeserializeOwned>(v: &Value, k: &str) -> Option<Vec<T>> {
        match v.get(k) {
            None | Some(Value::Null) => Some(Vec::new()),
            Some(x) => serde_json::from_value(x.clone()).ok(),
        }
    }
    let crates: Vec<facett_coderoom_view::CrateNode> = list(v, "crates")?;
    if crates.is_empty() {
        return None;
    }
    let seam = PayloadCoderoom {
        crates,
        edges: list(v, "compat_edges")?,
        breaks: list(v, "what_breaks")?,
        diff: v.get("snapshot_diff").filter(|d| !d.is_null()).map(|d| serde_json::from_value(d.clone())).transpose().ok()?,
    };
    let pane = facett_coderoom_view::CoderoomPane::new(Box::new(seam));
    Some(Box::new(if s(v, "title").is_empty() { pane } else { pane.with_title(s(v, "title")) }))
}

/// **`fleet`** — [`facett_fleet_view::FleetView`], the fleet coverage board.
///
/// The payload IS the component's own serde state, [`facett_fleet_view::FleetRunView`]:
/// nornir-testmatrix's `GateReport` (the Gap = ∅ verdict), its `TestResultRow`s and the
/// fleet members — exactly what a nornir fleet run writes. The banner and every cell
/// colour are derived by the component from those facts. No rows is a refusal; a
/// malformed report is a refusal (a gate verdict is never guessed).
#[must_use]
pub fn fleet(v: &Value) -> Option<Box<dyn Facet>> {
    let run: facett_fleet_view::FleetRunView = serde_json::from_value(v.clone()).ok()?;
    if run.rows.is_empty() {
        return None;
    }
    let title = if s(v, "title").is_empty() { "fleet" } else { s(v, "title") };
    Some(Box::new(facett_fleet_view::FleetView::new(title).with_run(run.report, run.rows, run.members)))
}

/// **`buildspace`** — [`facett_buildthing::GaloisPane`], a repo's EVALUATED feature axis:
/// which crates each feature pulls in, the AND-groups (conjunctions) that keep a crate in
/// the build, and what turning a feature off removes.
///
/// The payload is the component's own serde bridge, [`facett_buildthing::FeatureAxis`]
/// (`repo`, `root`, `features`, `analyses` = nornir-feature-graph's `CrateAnalysis` per
/// crate, optional `gated_items`, `sizes`, `provenance`). The EVALUATION happens on the
/// host (nornir-feature-graph over the manifests); the pane only applies the component's
/// one membership predicate to it. No features or no analyses is a refusal.
#[must_use]
pub fn buildspace(v: &Value) -> Option<Box<dyn Facet>> {
    let axis: facett_buildthing::FeatureAxis = serde_json::from_value(v.clone()).ok()?;
    if axis.features.is_empty() || axis.analyses.is_empty() {
        return None;
    }
    Some(Box::new(facett_buildthing::GaloisPane::new(axis)))
}

/// The `buildspace` fixture: a two-feature axis in the component's own serde shape.
fn buildspace_fixture() -> Value {
    let drop = |c: &str, removable: bool, off: &[&str]| {
        serde_json::json!({ "crate_name": c, "removable": removable, "features_to_disable": off,
                            "alternatives": [], "verified": true, "note": "" })
    };
    serde_json::json!({
        "repo": "edda", "root": "auth-policy", "features": ["auth-oidc", "auth-bearer"],
        "analyses": {
            "oidc": { "crate_name": "oidc", "status": "conditional", "causes": [["auth-oidc"]],
                      "causes_complete": true, "drop": drop("oidc", true, &["auth-oidc"]) },
            "bearer": { "crate_name": "bearer", "status": "conditional", "causes": [["auth-bearer"]],
                        "causes_complete": true, "drop": drop("bearer", true, &["auth-bearer"]) },
            "serde": { "crate_name": "serde", "status": "unconditional", "causes": [],
                       "causes_complete": true, "drop": drop("serde", false, &[]) }
        },
        "provenance": "fixture (facet-wrapped)"
    })
}

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

    #[test]
    fn cicd_builds_from_facts_and_refuses_nothing() {
        let v = json!({ "pipelines": [{ "id": "ci", "runs": [{ "id": "1", "status": "weird-word" },
                                                            { "id": "0", "status": "ok" }] }] });
        let f = cicd(&v).expect("builds");
        let st = f.state_json().to_string();
        assert!(st.contains("\"ci\""), "{st}");
        assert!(st.contains("queued"), "an unknown word is neutral, never passed: {st}");
        assert!(cicd(&json!({ "pipelines": [] })).is_none());
        assert!(cicd(&json!({ "pipelines": [{ "name": "no id" }] })).is_none());
    }

    #[test]
    fn brokkrview_takes_the_dwarves_roster_wire() {
        let v = json!({ "pipelines": ["p"], "runs": [{ "run_id": "r1", "pipeline": "p", "status": "passed",
                        "trigger": "push", "steps": "1/1", "duration": "1s" }] });
        let st = brokkrview(&v).expect("builds").state_json().to_string();
        assert!(st.contains("r1"), "{st}");
        assert!(brokkrview(&json!({})).is_none());
    }

    #[test]
    fn releaseview_derives_the_status_itself() {
        let v = json!({ "nodes": [{ "krate": "a", "local": "0.2.0", "sparring": "0.1.0", "cratesio": "0.1.0" },
                                  { "krate": "b", "local": "1.0.0", "sparring": "1.0.0", "cratesio": "1.0.0" }],
                        "edges": [[1, 0], [0, 9], { "from": 0, "to": 1 }] });
        let st = releaseview(&v).expect("builds").state_json().to_string();
        assert!(st.contains("\"a\"") && st.contains("\"b\""), "{st}");
        assert!(releaseview(&json!({ "nodes": [] })).is_none());
    }

    #[test]
    fn binview_hands_the_facts_over_and_never_guesses_a_verdict() {
        let ok = json!({ "artifact": "nornir",
            "checksec": { "relro": "Full", "pie": true, "nx": true, "stack_canary": true, "stripped": false, "fortify": true },
            "crate_sizes": [{ "crate_name": "std", "bytes": 812000 }] });
        let st = binview(&ok).expect("builds").state_json().to_string();
        assert!(st.contains("nornir"), "{st}");
        let mut bad = ok.clone();
        bad["checksec"] = json!({ "relro": "sort of" });
        assert!(binview(&bad).is_none(), "a malformed checksec is refused, not guessed");
        assert!(binview(&json!({ "artifact": "x", "checksec": ok["checksec"], "crate_sizes": [] })).is_none());
    }

    #[test]
    fn coderoom_looks_what_breaks_up_and_refuses_malformed_lists() {
        use facett_coderoom_view::CoderoomDataSeam as _;
        let v = json!({ "crates": [{ "name": "a", "version": "1.0.0", "symbols": 3 }, { "name": "b", "version": "1.0.0", "symbols": 1 }],
                        "compat_edges": [{ "from": "b@1.0.0", "to": "a@1.0.0", "compat": "Breaking", "suggested_bump": "major", "via": ["f"] }],
                        "what_breaks": [{ "target": "a@1.0.0", "removed_symbols": ["f"], "impacted": [{ "caller": "b@1.0.0", "symbol": "f" }] }] });
        assert!(coderoom(&v).expect("builds").state_json().to_string().contains("a@1.0.0"));
        let seam = PayloadCoderoom { crates: vec![], edges: vec![], breaks: serde_json::from_value(v["what_breaks"].clone()).unwrap(), diff: None };
        assert!(seam.what_breaks("a@1.0.0").is_some());
        assert!(seam.what_breaks("unknown@0").is_none(), "an unanswered target is nothing to highlight");
        let mut bad = v.clone();
        bad["compat_edges"] = json!([{ "from": "b" }]);
        assert!(coderoom(&bad).is_none(), "a malformed edge list is refused");
        assert!(coderoom(&json!({ "crates": [] })).is_none());
    }

    /// The fleet payload is the component's own state: the fixture's state serialised is
    /// a payload that builds, and prints it so the gallery payload can be refreshed from it.
    #[test]
    fn fleet_takes_its_own_serde_state_and_refuses_no_rows() {
        let state = serde_json::to_value(facett_fleet_view::FleetView::local().state()).unwrap();
        if std::env::var_os("PRINT_FLEET_PAYLOAD").is_some() {
            println!("{}", serde_json::to_string(&state).unwrap());
        }
        let f = fleet(&state).expect("the component's own state is a payload");
        let strip = |mut j: Value| { j.as_object_mut().unwrap().remove("title"); j };
        assert_eq!(strip(f.state_json()), strip(facett_fleet_view::FleetView::local().state_json()), "same facts, same board");
        let mut empty = state.clone();
        empty["rows"] = json!([]);
        assert!(fleet(&empty).is_none());
        assert!(fleet(&json!({ "rows": [1] })).is_none(), "no report → refused");
    }

    #[test]
    fn buildspace_applies_the_components_predicate_to_the_evaluated_axis() {
        let st = buildspace(&buildspace_fixture()).expect("builds").state_json();
        let present = st["present"].to_string();
        assert!(present.contains("serde") && present.contains("oidc"), "all features on: {st}");
        assert_eq!(st["root"], "auth-policy");
        let mut bad = buildspace_fixture();
        bad["analyses"]["oidc"]["status"] = json!("maybe");
        assert!(buildspace(&bad).is_none(), "a malformed analysis is refused");
        assert!(buildspace(&json!({ "repo": "r", "root": "x", "features": [], "analyses": {} })).is_none());
    }

    #[test]
    fn holger_builds_from_a_roster_and_says_it_is_live() {
        let v = json!({ "repos": [{ "name": "pypi-cache", "kind": "python" }],
                        "artifacts": [{ "name": "numpy", "version": "2.3.0", "size_bytes": 1 }] });
        let st = holger(&v).expect("builds").state_json();
        assert!(st.to_string().contains("pypi-cache"), "{st}");
        assert!(holger(&json!({ "repos": [] })).is_none());
    }
}