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