dwarves-gui/src/build_forge.rs
//! **The forge behind the scene** — the side of [`crate::build_scene::Forge`] that is
//! allowed to know what a product is.
//!
//! Everything here is a translation, not a decision. The declarations already say
//! which atoms a repo ships, which suites each one is in, which delivery cells chain
//! off which atom, and what kind each of those produces; this file turns that into the
//! [`Menu`] the scene renders and hands the verbs back to the engines that already
//! exist ([`BuildRun`] for a build, [`BootRun`] for a container log or a guest
//! console). No build logic is re-implemented here.
//!
//! # The matrix is the declaration, unchanged
//!
//! Rows are the `[[build.variant]]` tables **in declaration order**. Columns are the
//! delivery targets those same declarations name, in first-appearance order across the
//! repo, and a row that does not declare a column gets it greyed rather than dropped —
//! that is what makes it a matrix instead of five different lists. Nothing is merged,
//! renamed, sorted or invented: a column exists here exactly when some declaration in
//! that repo produced it, and a suite badge says exactly what `suites = [...]` says.
//!
//! **Every row is an ASSEMBLY, so its switches are its own.** A column strip is a map
//! and therefore shows cells this row lacks, greyed; a settings list is the argv a
//! build will run with, so it shows only what THIS row resolved. See [`settings_of`]
//! for why the repo's feature space is not a switch list.
//!
//! # The receipt, read back out of what was built
//!
//! [`DeclaredForge::settle`] hands the finished artifact to
//! [`audit_artifact`], which lifts `.nordisk.buildthing` out of it with an object
//! parser and measures every claim in it against the binary's own symbol table.
//! Both halves of the pane's answer come from that one call:
//!
//! ```text
//! kvitto what the build SAID it did the receipt, from the section
//! bevis what the binary SHOWS it did the claims, from the symbol table
//! ```
//!
//! Nothing is executed to obtain either, which is why it works on a cross-built
//! musl binary this host could not run, on a stripped one, and on a hostile one.
//!
//! **A medium is audited through the program it carries.** An installer ISO has no
//! symbol table and no receipt section, so asking it those two questions can only
//! ever answer NOT MEASURED. [`audit_first_readable`] names the subject rule: the
//! audit is about the first listed artifact it can read, and
//! `assemble_cell::compose_installer_row` lists the appliance binary beside the ISO
//! for exactly that reason. The artifact the operator is SHOWN stays the medium.
//!
//! # What is honestly missing
//!
//! The [`Coordinate`] in the menu is still derived from the declaration:
//! `<repo>-build-thing` at the repo's own version, with **no hash** — a coordinate
//! typed before a first build has nothing to pin to, and inventing a hash here
//! would put a number in the receipt that means nothing. The receipt read back out
//! of a finished artifact carries the real one, because the recipe hashed its own
//! sources while it compiled.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use dwarves::build_thing::{code, BuildRun, ConstellationBuilds, RepoBuilds};
use dwarves::feature_axis_source::{feature_axis, AxisRequest};
use nornir_build_thing::contract::{Coordinate, Receipt};
use nornir_build_thing::kernel::{HardeningProfile, Preset};
use nornir_build_thing::receipt_proof::{audit_artifact, ReceiptVerdict};
use nornir_build_thing::variant::{ArtifactKind, BuildVariant};
use crate::boot_run::{
ArtifactExecLauncher, BootLauncher, BootRun, ContainerLauncher, ExecSandbox, HwConfig,
};
use crate::build_scene::{
Act, Axis, AxisPick, BuildScene, Choice, Forge, Governed, Leaf, Menu, Outcome, Phase, Proof,
Query, Report, Row, Screen, Setting, Target,
};
use crate::kernel_scene::{KconfigWhy, KernelScene, PRESETS};
/// How often the kernel leaf asks whether a build rewrote `kernel.why.toml`.
const KERNEL_WHY_RECHECK: std::time::Duration = std::time::Duration::from_secs(2);
/// The settings key the kernel preset rides under — the ONE spelling shared by the
/// picker the operator turns, the leaf that redraws itself from it, and the override
/// that reaches the build. Three call sites and one string.
const PRESET_KEY: &str = "preset";
/// The suffix a recipe crate's name carries. Only used to turn a typed coordinate back
/// into the checkout it addresses.
const RECIPE_SUFFIX: &str = "-build-thing";
/// The forge the shipped scene runs on.
pub struct DeclaredForge {
/// Every side-by-side checkout that declares a build space.
builds: ConstellationBuilds,
/// Which checkout the last resolve landed on — the build verbs need its root.
repo: Option<String>,
/// The cell each column maps back to, keyed by `<row>\u{1f}<column>`. Built during
/// [`Self::menu_for`] so a build never has to re-derive what the operator picked.
cells: BTreeMap<String, Cell>,
/// The build in flight, or the last one that finished.
run: Option<BuildRun>,
/// **The pane's ONE running subject** — a container log, a guest console, or a
/// sandboxed exe run. At most one, ever: [`Self::take_live`] is the only way it is
/// replaced, and it tears the previous one down first. Concurrent containers are the
/// thing this field exists to prevent.
live: Option<BootRun>,
/// The last thing the pane was asked to bring up, so ↻ can bring it up again.
last_launch: Option<Cell>,
/// What the finished build produced.
outcome: Option<Outcome>,
/// The honest reason the pane cannot do what it offers.
note: Option<String>,
/// Where a boot goes when the console asks for one.
boots: Arc<dyn BootLauncher>,
/// Which engine [`Self::boots`] is — shown, so an operator can tell WHICH install
/// they are watching rather than infer it from the serial.
boot_engine: &'static str,
/// Where a container run goes.
containers: Arc<dyn ContainerLauncher>,
/// The dependency axis being derived, or the one that was. At most one: the
/// scene only ever looks at the picked row, and a second worker would be work
/// nobody is waiting for.
axis_work: Option<AxisWork>,
/// **The kconfig configurator, as a leaf of ⚒.**
///
/// Built LAZILY, on the first kernel cell an operator picks, and never before:
/// `KernelScene::new` merges the committed fragments and lays out a 3D cloud, and
/// this forge is constructed at shell startup for every operator whether or not
/// they will ever open a kernel row. Seven panes were moved off the UI thread for
/// putting work on the frame that draws them (`warehouse_nav` held **3.246 s**);
/// paying a layout at startup for a tab nobody opened is the same bill in a
/// different pocket. There is exactly one, reused across picks — `set_preset` is
/// idempotent, so a frame that changes nothing rebuilds nothing.
kernel: Option<KernelScene>,
/// **Which built kernel's `kernel.why.toml` the configurator is showing** —
/// `(output dir, the index's mtime)`, and when it was last looked at.
///
/// The kconfig SOLVER (edda `kconfig_graph`) writes its chains beside every
/// kernel it builds; the leaf reads that file so a click can say what else
/// leaves the `.config` by Kconfig's own edges. Read when the picked kernel
/// changes or the file does — never every frame: the mtime is asked at most
/// once per [`KERNEL_WHY_RECHECK`], and a frame that changes nothing reads
/// nothing.
kernel_why: Option<(PathBuf, Option<std::time::SystemTime>, std::time::Instant)>,
/// **The recipe crate's wasm build in flight** (gap #9). Started by
/// [`Self::take_code_road`] on resolve when the cache misses; its lines are the
/// step console's until BYGG runs something else; its landing swaps the resolved
/// repo's set for the CODE's and re-answers the matrix through `Report::menu`.
code_build: Option<code::CodeBuild>,
/// The loaded module for the resolved repo — the instance the tree's
/// `options(path)` / `try_toggle` / `actions(kind)` will be asked of.
code: Option<code::LoadedCode>,
/// The landed wasm build's lines, kept on screen until the next resolve or build.
code_log: Vec<String>,
/// How the last resolve found its repo ("hela receptet", "löst ur namnet", …) —
/// kept so the matrix re-answered on landing wears the same first half.
how: Option<String>,
/// **The chained build in flight** — a `BYGG <kant>` running the edge DAG
/// through the CLI's own `run_build_chain_watched` on a worker thread. At most
/// one: two chains would be two containers compiling one kernel, and the second
/// would file its receipt over the first's.
chain: Option<crate::build_steps::ChainRun>,
/// **The resolved declaration's own refusal at load** (`dwarves-buildthing-4`),
/// when it has one. Set by [`Self::menu_for`] through the CLI door's own gate;
/// every build verb refuses by it, because acting on a declaration the prompt
/// refuses is the drift this field exists to make impossible.
invalid: Option<String>,
}
/// **One row's dependency axis, derived off the frame.**
///
/// MEASURED on this constellation, 2026-08-06, debug build, one repo's lockfile
/// universe of 1557 packages with a three-feature Φ: **19.6 s** before a single
/// minimal cause is hunted (the manifest scan plus the two resolves that settle the
/// ⊥ wall), then roughly **2 s per removable crate** on top — 61.9 s at 20 of that
/// repo's 29 removable crates. There is no cap that makes this a per-frame call, so
/// it is not one: the work runs on its own thread and the scene is told
/// [`Axis::Working`] until it lands.
///
/// One plain thread, not a fan-out: this is a single job, and concurrency in this
/// constellation is `gatling` (LAW 3 — rayon is forbidden).
struct AxisWork {
/// Which SELECTION it belongs to — row, column and toggles. A finished axis
/// for a selection nobody is looking at any more is dropped rather than
/// shown, and that now includes a selection whose opt-outs have changed: the
/// same row with WireGuard unticked is a picture of a different artifact.
pick: AxisPick,
/// `None` once the answer has been taken off it.
rx: Option<std::sync::mpsc::Receiver<Axis>>,
/// The answer, kept so the repeat calls cost nothing.
done: Option<Axis>,
}
/// What one column of one row actually builds.
#[derive(Debug, Clone, PartialEq, Eq)]
struct Cell {
/// The declared handle the engines take (`--variant` / `--image` / `--iso`).
name: String,
/// What it produces.
kind: ArtifactKind,
/// Site values this cell DECLARES (`[[build.*.params]]`). Empty for every
/// cargo axis and every cell that declares none — a product that declares no
/// parameters renders no inputs.
params: Vec<nornir_build_thing::params::ParamSpec>,
/// The fixtures this cell DECLARES for its params (`[build.*.fixtures]`,
/// name → kind). Carried verbatim; the scene offers the affordance only
/// where a name is in here.
fixtures: BTreeMap<String, nornir_build_thing::params::FixtureKind>,
/// The OPT-OUTS this cell DECLARES (`[[build.installer.options]]`) — the
/// capabilities a customer may leave out of the packed binary. Empty for
/// every cargo axis and every cell that declares none, which renders no
/// checkboxes at all.
options: Vec<nornir_build_thing::options::OptionSpec>,
}
impl DeclaredForge {
/// Read the constellation the forge stands in.
///
/// **Says out loud which root it landed on**, once, at startup. MEASURED
/// 2026-08-22: the ui was launched with `cwd = ~/git` and no
/// `$DWARVES_CONSTELLATION`, so `default_root()` took the PARENT —
/// `~` — which declares no build space at all. The roster was
/// empty, `sök` found nothing, and the log said NOTHING about where it had
/// looked. A root this pane resolves silently is a root nobody can debug.
#[must_use]
pub fn discover() -> Self {
let root = ConstellationBuilds::default_root();
let me = Self::at(root);
eprintln!(
"build thing: konstellationsrot {} — {} repo(n) deklarerar ett byggrum{}",
me.builds.root.display(),
me.builds.repos.len(),
if me.builds.repos.is_empty() {
" (sätt $DWARVES_CONSTELLATION)"
} else {
""
}
);
me
}
/// Read a specific constellation root.
#[must_use]
pub fn at(root: PathBuf) -> Self {
// ONE read of the selector, ONE decision: the launcher and the name it is
// reported under come out of the same arm (see [`boot_engine`]).
let (boots, boot_engine_label) =
boot_engine(std::env::var("DWARVES_INSTALLER_ENGINE").ok().as_deref());
Self {
builds: ConstellationBuilds::discover(&root),
repo: None,
cells: BTreeMap::new(),
run: None,
live: None,
last_launch: None,
outcome: None,
note: None,
boots,
boot_engine: boot_engine_label,
containers: Arc::new(crate::boot_run::DraupnirContainerLauncher),
axis_work: None,
kernel: None,
kernel_why: None,
code_build: None,
code: None,
code_log: Vec::new(),
how: None,
chain: None,
invalid: None,
}
}
/// **Which road the resolved repo's matrix came down**, as the pane shows it —
/// [`dwarves::build_thing::BuildRoad::line`], or the in-flight sentence while the
/// recipe crate is still building. A robot reads it off `how`.
#[must_use]
pub fn road_line(&self) -> String {
self.repo
.as_deref()
.map(|r| self.road_line_for(r))
.unwrap_or_default()
}
/// [`Self::road_line`] for a named checkout — `menu_for` asks BEFORE it records the
/// resolve, so the answer cannot come off `self.repo`.
fn road_line_for(&self, repo_name: &str) -> String {
if let Some(b) = self.code_build.as_ref().filter(|b| b.repo() == repo_name) {
return format!(
"deklarationen (toml) tills `{}` byggt till wasm — se konsolen",
b.package()
);
}
self.builds
.repo(repo_name)
.map(|r| r.road.line())
.unwrap_or_default()
}
/// The loaded module for the resolved repo, when the code road answered.
#[must_use]
pub fn code(&self) -> Option<&code::LoadedCode> {
self.code.as_ref()
}
/// **Take the code road for the repo a resolve landed on** (gap #9): the cached
/// module answers now; a cache miss starts the wasm build on a worker and the
/// table answers until it lands; no crate, or the wrong shape, is refused by name
/// and the table answers with the refusal on it. Never blocks on cargo.
fn take_code_road(&mut self, repo_name: &str) {
self.code_build = None;
self.code = None;
self.code_log.clear();
let Some(i) = self.builds.repos.iter().position(|r| r.repo == repo_name) else {
return;
};
let root = self.builds.repos[i].root.clone();
match code::take(&root, repo_name) {
code::Road::Ready(loaded) => {
self.builds.repos[i] = self.builds.repos[i].with_code(&loaded);
self.code = Some(loaded);
}
code::Road::Building(b) => self.code_build = Some(b),
code::Road::Refused(why) => {
self.builds.repos[i] = self.builds.repos[i].refused_code(why);
}
}
}
/// [`Self::take_code_road`] on a fresh resolve — unless the SAME repo's wasm build is
/// already in flight, in which case its landing is the answer being waited for.
fn take_code_road_unless_building(&mut self, repo_name: &str) {
if self
.code_build
.as_ref()
.is_some_and(|b| b.repo() == repo_name)
{
return;
}
self.take_code_road(repo_name);
}
/// **Land the wasm build, if it has.** Swaps the repo's set for the CODE's (or
/// records the refusal by name) and re-answers the matrix for the scene to swap in.
fn settle_code(&mut self) -> Option<Menu> {
let b = self.code_build.as_mut()?;
b.poll();
if !b.is_done() {
return None;
}
let mut b = self.code_build.take()?;
let repo_name = b.repo().to_string();
self.code_log = std::mem::take(&mut b.log);
let i = self.builds.repos.iter().position(|r| r.repo == repo_name)?;
match b.take_result()? {
Ok(loaded) => {
self.builds.repos[i] = self.builds.repos[i].with_code(&loaded);
self.code = Some(loaded);
}
Err(why) => {
self.builds.repos[i] = self.builds.repos[i].refused_code(why.clone());
self.note = Some(why);
}
}
if self.repo.as_deref() != Some(repo_name.as_str()) {
return None;
}
let how = self.how.clone().unwrap_or_default();
Some(self.menu_for(&repo_name, how))
}
/// Which boot engine the Console pane's ▶ KÖR will use.
#[must_use]
pub fn boot_engine(&self) -> &'static str {
self.boot_engine
}
/// Every checkout that declares a build space, in discovery order. The scene never
/// asks for this — it is the roster a robot prints so a failure names what WAS there.
#[must_use]
pub fn declared_repos(&self) -> Vec<String> {
self.builds.repos.iter().map(|r| r.repo.clone()).collect()
}
/// **Every `[[build.kernel]]` this constellation declares** — `(checkout, kernel
/// name, declared preset)`, in discovery order.
///
/// The scene never asks for this either. It exists so a test can run against the
/// ONE live declaration on this disk rather than only against a staged tempdir, and
/// name what it found: a green measured on a fixture says nothing about the
/// declaration an operator will actually pick.
#[must_use]
pub fn kernel_declarations(&self) -> Vec<(String, String, String)> {
self.builds
.repos
.iter()
.flat_map(|r| {
r.set
.kernels
.iter()
.map(|k| (r.repo.clone(), k.name.clone(), k.preset.clone()))
})
.collect()
}
/// **Tear down whatever is running, and return the slot empty.**
///
/// Every path that starts something goes through here first, so there is never a
/// second container alive beside the first — not on a rebuild, not on a re-run, not
/// on a row or column change, not on a fresh resolve.
fn take_live(&mut self) {
if let Some(b) = self.live.take() {
b.cancel();
}
}
/// The checkout the last resolve landed on.
fn current(&self) -> Option<&RepoBuilds> {
self.builds.repo(self.repo.as_deref()?)
}
/// `Some(read)` when the picked kernel's `kernel.why.toml` must be (re)read —
/// another kernel, or the file changed since — and `None` when what the pane
/// holds is still the answer. The file's mtime is asked at most once per
/// [`KERNEL_WHY_RECHECK`].
fn kernel_why_changed(
&mut self,
out: PathBuf,
kernel_name: &str,
) -> Option<Result<Option<KconfigWhy>, String>> {
let now = std::time::Instant::now();
if let Some((dir, _, at)) = &self.kernel_why {
if *dir == out && now.duration_since(*at) < KERNEL_WHY_RECHECK {
return None;
}
}
let mtime = std::fs::metadata(out.join(nornir_build_thing::kernel_requires::WHY_FILE))
.and_then(|m| m.modified())
.ok();
let same = matches!(&self.kernel_why, Some((dir, m, _)) if *dir == out && *m == mtime);
self.kernel_why = Some((out.clone(), mtime, now));
if same {
return None;
}
Some(KconfigWhy::read(&out, kernel_name))
}
/// The cell behind a picked `(row, column)`.
fn cell(&self, row: &str, target: &str) -> Option<&Cell> {
self.cells.get(&key(row, target))
}
// ── resolution ────────────────────────────────────────────────────────────
/// Which checkouts declare something answering to `name` — the repo itself, one of
/// its variants, or a variant's package/bin/member.
///
/// # A checkout named exactly what you typed is not ambiguous
///
/// This machine holds a dozen worktrees of the same repo side by side, and every one
/// of them declares the same variants. Without the exact-name rule, typing the
/// product's own name is ambiguous with its own worktrees and resolves to nothing at
/// all — measured 2026-08-06 on this disk: two of the three products the design names
/// were unreachable that way. So a directory whose name IS the query wins outright;
/// anything else that merely mentions the name stays a candidate, and two of THOSE is
/// still a refusal with both named.
fn candidates(&self, name: &str) -> Vec<&RepoBuilds> {
let exact: Vec<&RepoBuilds> = self
.builds
.repos
.iter()
.filter(|r| r.repo == name)
.collect();
if exact.len() == 1 {
return exact;
}
self.builds
.repos
.iter()
.filter(|r| {
r.repo == name
// What this repo DECLARES — the enumeration over all six
// collections, not just `variants`. A kind declared only
// through its sugar table (`[[build.kernel]]`, and the same
// for the others) lands in its own collection, so matching
// `variants` alone made a valid declaration unresolvable:
// the CLI listed it and the pane answered "no declaration
// names it". MEASURED 2026-08-22 with a `[[build.kernel]]`
// fixture: zero rows.
|| r.set.declared_names().contains(&name)
|| r.set.variants.iter().any(|v| {
v.package.as_deref() == Some(name)
|| v.bin.as_deref() == Some(name)
|| v.member.as_deref() == Some(name)
})
})
.collect()
}
/// Build the [`Menu`] for one checkout, recording the cell behind every column.
///
/// The column strip is the union of the delivery targets the repo's own
/// declarations name, in first-appearance order; a row that does not declare one
/// carries it greyed. Order and spelling both come from the file.
fn menu_for(&mut self, repo_name: &str, how: String) -> Menu {
// The road is part of the answer: a matrix read off the table says so, and
// says why the code did not answer (gap #9 — never a silent second path).
self.how = Some(how.clone());
let mut how = format!("{how} · {}", self.road_line_for(repo_name));
// **THE DOOR VALIDATES WHAT IT IS ABOUT TO ACT ON** — wave 3, lane W7.
//
// ⚒ and the prompt have shared their LOADER since gap #9: both take
// `build_thing::code::take`, both fall back to the toml BY NAME through
// `BuildRoad`. What they did not share was the judgement. `read_through_code`
// has refused a set that will not validate since `35729c2`
// (`dwarves-buildthing-4`); this pane loaded the same set down the same road
// and asked nothing, so a declaration the prompt refuses by name rendered
// here as a matrix — the silent second path law B forbids, arriving through
// agreement about loading rather than disagreement about it.
//
// `refuse_unvalidated` is that door's own gate, lifted out of it. Whichever
// road the set came down is judged here, before a verb is drawn.
self.invalid = None;
if let Some(r) = self.builds.repo(repo_name) {
if let Err(e) = dwarves::buildthing::refuse_unvalidated(r) {
let why = format!("{e:#}");
eprintln!("[dwarves] deklarationen vägrad vid inläsning: {why}");
how.push_str(&format!(" · ⚠ VÄGRAD VID INLÄSNING: {why}"));
self.note = Some(why.clone());
self.invalid = Some(why);
}
}
let repo = self
.builds
.repo(repo_name)
.expect("caller resolved this repo");
let set = repo.set.clone();
let root = repo.root.clone();
// Pass 1 — every column any row declares, in first-appearance order.
let mut columns: Vec<(String, ArtifactKind)> = Vec::new();
let mut per_row: Vec<(String, Vec<(String, Cell)>)> = Vec::new();
for v in &set.variants {
let mut mine: Vec<(String, Cell)> = Vec::new();
let own = (
v.kind.as_str().to_string(),
Cell {
name: v.name.clone(),
kind: v.kind,
params: Vec::new(),
fixtures: BTreeMap::new(),
options: Vec::new(),
},
);
mine.push(own);
for (label, cell) in chained(&set, &v.name) {
mine.push((label, cell));
}
for (label, cell) in &mine {
if !columns.iter().any(|(c, _)| c == label) {
columns.push((label.clone(), cell.kind));
}
}
per_row.push((v.name.clone(), mine));
}
// Pass 1b — the SUGAR rows. `[[build.kernel]]` is documented as
// "permanent sugar" for `kind = "kernel"`, exactly like the iso and
// installer tables, and a sugar table carries its handle in its own NAME
// rather than hanging off a cargo variant (a kernel is built from a
// pinned tarball, so there is no row for it to chain onto). Without this
// the two spellings of ONE declaration disagreed: the long way got a
// clickable cell and the documented way got nothing at all.
for k in &set.kernels {
if per_row.iter().any(|(n, _)| n == &k.name) {
continue;
}
let label = ArtifactKind::Kernel.as_str().to_string();
let cell = Cell {
name: k.name.clone(),
kind: ArtifactKind::Kernel,
params: Vec::new(),
fixtures: BTreeMap::new(),
options: Vec::new(),
};
if !columns.iter().any(|(c, _)| c == &label) {
columns.push((label.clone(), ArtifactKind::Kernel));
}
per_row.push((k.name.clone(), vec![(label, cell)]));
}
// Pass 2 — every row gets every column; the ones it did not declare are greyed.
self.cells.clear();
let mut rows = Vec::new();
for (row_name, declared) in per_row {
// `None` for a sugar row: there is no cargo variant behind it, and
// the badges/settings a variant would have carried come from the
// declaration itself instead.
let v = set.variants.iter().find(|v| v.name == row_name);
let targets = columns
.iter()
.map(|(label, kind)| {
let mine = declared.iter().find(|(l, _)| l == label);
if let Some((_, cell)) = mine {
self.cells.insert(key(&row_name, label), cell.clone());
}
Target {
name: label.clone(),
kind: mine.map_or(*kind, |(_, c)| c.kind),
pane: None,
declared: mine.is_some(),
variant: mine.map(|(_, c)| c.name.clone()).filter(|n| n != &row_name),
settings: mine
.map(|(_, c)| {
let mut out = match v {
Some(v) => settings_of(v, c),
None => {
vec![Setting::fixed("kind", c.kind.as_str(), "deklarerad")]
}
};
out.extend(kernel_settings(&set, c));
out.extend(donor_settings(&set, c));
out
})
.unwrap_or_default(),
}
})
.collect();
let badges = match v {
Some(v) => badges_of(v),
None => Vec::new(),
};
rows.push(Row {
name: row_name,
badges,
targets,
});
}
self.repo = Some(repo_name.to_string());
Menu {
coordinate: coordinate_of(repo_name, &root),
how,
rows,
}
}
// ── the dependency axis ───────────────────────────────────────────────────
/// **The picked row's feature↔dependency axis**, derived off the frame.
///
/// Cheap on every call after the first for a given row: the worker is started
/// once, its answer is kept, and a row change throws the previous one away
/// rather than showing a picture of a different assembly.
///
/// # Φ is the features THIS ROW BUILDS WITH
///
/// Not the repo's space, and not the row's open switches either. The picture
/// answers "what are the features of this build holding up, and what would
/// dropping one cost" — so the clickable universe is exactly the set the build
/// will enable. A feature the row leaves open is OFF until somebody turns it on;
/// putting it on the axis would draw a package as present because of a feature
/// this build does not have, which is a graph that is wrong rather than partial.
fn axis_for(&mut self, pick: &AxisPick) -> Axis {
if self.axis_work.as_ref().is_some_and(|w| &w.pick != pick) {
// A worker for a selection nobody is looking at any more. Dropping
// the receiver does not stop the thread, but nothing waits on it and
// its result is never shown.
self.axis_work = None;
}
if self.axis_work.is_none() {
self.axis_work = Some(self.start_axis(pick));
}
let work = self.axis_work.as_mut().expect("just set");
if let Some(done) = &work.done {
return done.clone();
}
match work.rx.as_ref().map(std::sync::mpsc::Receiver::try_recv) {
Some(Ok(axis)) => {
work.rx = None;
work.done = Some(axis.clone());
axis
}
// The sender was dropped without answering — a panicked worker. Said
// out loud: a silent fall back to "no axis" would read exactly like a
// repo that has none.
Some(Err(std::sync::mpsc::TryRecvError::Disconnected)) => {
let why = "beroendegrafen kunde inte räknas ut: arbetstråden dog".to_string();
work.rx = None;
work.done = Some(Axis::Failed(why.clone()));
Axis::Failed(why)
}
_ => Axis::Working,
}
}
/// Start the worker for one selection, or record outright why there will be
/// no axis.
///
/// **The picture is of the artifact the SELECTION produces**, not of the row
/// label. Two corrections live here, and both were defects:
///
/// * an installer/iso column packs a variant, and it is THAT variant's parts
/// the picture is about. Keyed on the row alone, an installer row (which
/// declares no features of its own) settled on `bygger utan features` and
/// drew nothing, while the app row beside it drew — the intermittency
/// The owner saw as "the picture sometimes flickers in".
/// * the features are the RESOLVED ones. A picture derived from the declared
/// set shows the WireGuard parts of a build that is about to be made
/// without them, which is a picture of an artifact that will not exist.
fn start_axis(&self, pick: &AxisPick) -> AxisWork {
let settled = |a: Axis| AxisWork {
pick: pick.clone(),
rx: None,
done: Some(a),
};
let Some(repo) = self.current() else {
return settled(Axis::Absent);
};
// Which variant this selection actually builds: the column's packed
// appliance when a delivery column is picked, else the row itself.
let cell = pick.target.as_deref().and_then(|t| self.cell(&pick.row, t));
let subject = cell
.and_then(|c| self.packed_variant(&repo.set, c))
.unwrap_or_else(|| pick.row.clone());
let Some(v) = repo.set.variants.iter().find(|v| v.name == subject) else {
// A row with no cargo variant behind it and no column that names
// one. Not a failure: there is genuinely nothing to draw.
return settled(Axis::Absent);
};
let package = v.package.clone().unwrap_or_else(|| repo.repo.clone());
// The RESOLVED feature set — the declaration minus every opt-out the
// operator has unticked, through the same `nornir_build_thing::options`
// resolution the CLI door and the forge use. A second subtraction rule
// here would be a second answer to "what is in this build".
let declared = cell.map(|c| c.options.as_slice()).unwrap_or(&[]);
let params = cell.map(|c| c.params.as_slice()).unwrap_or(&[]);
let features: Vec<String> =
match nornir_build_thing::options::resolve_options(declared, params, &pick.toggles) {
Ok(r) => r.app_features(v),
// A recipe whose options do not validate is the recipe's problem
// and is refused loudly at build time; the picture falls back to
// the declaration rather than inventing a set.
Err(_) => v.features.clone(),
};
let phi: std::collections::BTreeSet<String> = features.into_iter().collect();
if phi.is_empty() {
return settled(Axis::Failed(format!(
"{subject} bygger utan features, så det finns ingen axel att klicka på"
)));
}
let mut req = AxisRequest::new(repo.repo.clone(), repo.root.clone()).package(package);
req.checkout_root = Some(self.builds.root.clone());
req.features = Some(phi);
// ── LAZY ON CLICK ────────────────────────────────────────────────────
//
// The minimal-cause hunt is the entire cost of this picture: MEASURED at
// 10-16 s per crate over 98 removable crates, which at the old default
// budget of 200 is 17-27 MINUTES. The manifest scan everyone suspected is
// 6-30 s of that — under 2%.
//
// So nothing is hunted until it is asked for. The press lands in seconds
// with the whole graph, the ⊥ wall and the clickable features on it, and
// each crate the operator clicks buys exactly its own hunt. `pick.focus`
// is what those clicks accumulate to; a fresh pick has none, and
// `max_analyses` follows it exactly, so the budget can never quietly
// spend itself on a crate nobody asked about.
req.focus = pick.focus.iter().cloned().collect();
req.max_analyses = pick.focus.len();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let answer = match feature_axis(&req, None) {
Ok(a) => Axis::Ready(Box::new(a)),
Err(e) => Axis::Failed(e.to_string()),
};
// The scene may have moved on; nobody is owed an error for that.
let _ = tx.send(answer);
});
AxisWork {
pick: pick.clone(),
rx: Some(rx),
done: None,
}
}
/// The cargo variant a delivery column PACKS — an installer's appliance, an
/// iso's `from_variant`, a container layer's. `None` for a plain cargo
/// column, whose subject is its own row.
fn packed_variant(
&self,
set: &nornir_build_thing::variant::BuildVariantSet,
cell: &Cell,
) -> Option<String> {
match cell.kind {
ArtifactKind::Installer => set
.installers
.iter()
.find(|i| i.name == cell.name)
.map(|i| i.variant.clone())
.filter(|v| !v.is_empty()),
ArtifactKind::Iso => set
.isos
.iter()
.find(|i| i.name == cell.name)
.and_then(|i| i.from_variant.clone()),
ArtifactKind::Container => set
.containers
.iter()
.find(|c| c.name == cell.name)
.and_then(|c| c.layers.iter().find_map(|l| l.from_variant.clone())),
ArtifactKind::Vm => set
.vms
.iter()
.find(|v| v.name == cell.name)
.and_then(|v| v.from_variant.clone()),
_ => None,
}
}
// ── the verbs ─────────────────────────────────────────────────────────────
/// Step 5: hand the picked cell to the one build worker there is.
fn start_build(&mut self, row: &str, target: &str, settings: &BTreeMap<String, String>) {
self.outcome = None;
self.note = None;
self.take_live();
// The load-time refusal is the prompt's; this door does not get to act past it.
if let Some(why) = self.invalid.clone() {
self.note = Some(format!("deklarationen vägras vid inläsning: {why}"));
return;
}
let Some(cell) = self.cell(row, target).cloned() else {
self.note = Some(format!("{row} · {target} är inte deklarerad"));
return;
};
let Some(repo) = self.current().cloned() else {
return;
};
// **THE PICKED PRESET IS THE BUILT PRESET.** The override lands on the
// declaration this build reads, not on an argv beside it, because that is the
// only place a kernel's contents are decided — see `kernel_override`.
let repo = match kernel_override(&repo, &cell, settings) {
Some((overridden, line)) => {
eprintln!("[dwarves] {line}");
overridden
}
None => repo,
};
let params = declared_param_values(&cell, settings);
let mut opts = declared_option_values(&cell, settings);
// The kernel vetoes the operator clicked in the graph ride as the `--opt
// kernel.<symbol>=off` the CLI door speaks — the flag on screen IS the flag the
// build gets (law C). `kernel_override` carries the preset the same way.
opts.extend(
settings
.iter()
.filter(|(k, _)| k.starts_with("kernel."))
.map(|(k, v)| format!("{k}={}", if v == "true" { "on" } else { "off" })),
);
if !params.is_empty() {
eprintln!("[dwarves] {} params: {}", cell.name, params.join(" "));
}
if !opts.is_empty() {
eprintln!("[dwarves] {} opts: {}", cell.name, opts.join(" "));
}
// The OPEN cargo features the operator ticked ride as `--features` — the flag
// the node shows is the flag the build gets (law C), the same choice
// `dwarves buildthing --features` and the forge's BuildThing RPC carry.
let cargo = declared_cargo_features(&repo, &cell.name, settings);
if !cargo.is_empty() {
eprintln!("[dwarves] {} cargo: {}", cell.name, cargo.argv().join(" "));
}
self.last_launch = Some(cell.clone());
self.run = Some(BuildRun::spawn_with_choice(
&repo, &cell.name, params, opts, cargo,
));
}
/// Bring the pane's subject up: a container run, or a guest boot.
fn launch(&mut self) {
let (Some(cell), Some(repo)) = (self.last_launch.clone(), self.current().cloned()) else {
return;
};
let root = repo.root;
self.take_live();
self.live = Some(match cell.kind {
ArtifactKind::Container => {
BootRun::spawn_container(&cell.name, root, self.containers.clone())
}
// **▶ boots what BYGG built.** The medium goes in HERE, from the finished
// build's own outcome, because this is the only place that knows both the
// cell and the artifact. Until 2026-08-16 the installer arm fell through to
// `spawn_hw` below, which takes no artifact, and the launcher re-mastered a
// korp-shaped scaffold into its own temp root — so a holger BYGG that FAILED
// and produced nothing still reached `INSTALL-OK`, and all three products'
// captured framebuffers came back md5-identical.
ArtifactKind::Installer => BootRun::spawn_installer(
&cell.name,
root,
self.boots.clone(),
HwConfig::default(),
self.composed_medium(),
),
// An iso-kind variant boots the medium BYGG composed the same way the
// installer does — read off the SAME `Outcome` the pane shows (gap #7:
// until 2026-08-20 this arm carried no medium, so a composed
// gunnar-appliance-iso was refused as unwired and offered the stand-in).
ArtifactKind::Iso => BootRun::spawn_iso(
&cell.name,
root,
self.boots.clone(),
HwConfig::default(),
self.composed_medium(),
),
_ => BootRun::spawn_hw(&cell.name, root, self.boots.clone(), HwConfig::default()),
});
}
/// **The medium the last successful build composed**, or `None`.
///
/// Read off the same [`Outcome`] the pane SHOWS, so the operator's eye and the boot
/// cannot disagree about which file this is. Three things must hold and each is
/// checked, because each has been false in a real run: the build succeeded, it named
/// an artifact, and the artifact is on disk. A failed build's outcome carries
/// `ok=false` and an empty artifact — and a ▶ that boots anything after that is not
/// booting this product.
fn composed_medium(&self) -> Option<PathBuf> {
let o = self.outcome.as_ref()?;
if !o.ok || o.artifact.trim().is_empty() {
return None;
}
let p = PathBuf::from(&o.artifact);
p.is_file().then_some(p)
}
/// **Run the built artifact INSIDE A CONTAINER**, streaming its output into the pane.
///
/// Not a child process against the host. The artifact is bind-mounted read-only into
/// a fixed base image with no network, and the container runs the in-container path —
/// so the same build behaves the same way on every machine and cannot write over
/// anything outside itself.
///
/// It goes through [`BootRun::spawn_container`] because that is already the one
/// console contract there is: one worker, one [`crate::boot_run::pump`], one log
/// window, one kill path. An Exec run is a container run with a different launcher,
/// not a second mechanism.
fn run_artifact(&mut self, argv: &str) {
let Some(path) = self.outcome.as_ref().map(|o| PathBuf::from(&o.artifact)) else {
return;
};
if !path.is_file() {
self.note = Some(format!("{} finns inte på disk", path.display()));
return;
}
let Some(repo) = self.current().cloned() else {
return;
};
let sandbox = ExecSandbox::new(path, argv.split_whitespace().map(str::to_string).collect());
// The sandbox is SHOWN, not implied: an operator reading stdout has to be able to
// see that it came out of a container and which one.
self.note = Some(sandbox.banner());
let label = self
.last_launch
.as_ref()
.map_or("artefakt", |c| c.name.as_str())
.to_string();
self.take_live();
self.live = Some(BootRun::spawn_container(
&label,
repo.root,
Arc::new(ArtifactExecLauncher { sandbox }),
));
}
/// Fold the finished build into an [`Outcome`]. Reads only what the report holds:
/// a build that produced nothing shows nothing, never a plausible path.
fn settle(&mut self) {
let Some(run) = self.run.as_mut() else { return };
run.poll();
let Some(report) = run.report.clone() else {
return;
};
if self.outcome.is_some() {
return;
}
let ports = self
.last_launch
.as_ref()
.and_then(|c| self.current().map(|r| (c.name.clone(), r.set.clone())))
.map(|(name, set)| {
set.containers
.iter()
.find(|c| c.name == name)
.map(|c| c.expose.clone())
.unwrap_or_default()
})
.unwrap_or_default();
// **Read the receipt back out of what was just built, and check it.** One
// `audit_artifact` does both: it lifts `.nordisk.buildthing` out with the
// object parser and measures every claim in it against the binary's own
// symbol table. Nothing is executed — which is why this works on the musl
// cross build this host would not run, on a stripped one, and on a hostile
// one.
let paths: Vec<&Path> = report.artifacts.iter().map(|(p, _)| p.as_path()).collect();
let (receipt, proof) = match audit_first_readable(&paths) {
Some((r, p)) => (r, Some(p)),
None => (None, None),
};
self.outcome = Some(Outcome::from_report(&report, ports, receipt, proof));
if !report.errors.is_empty() {
self.note = Some(report.errors.join("\n"));
}
}
}
/// **Which of a build's artifacts the receipt audit is ABOUT.**
///
/// For every cargo cell there is one artifact and this is the identity function.
/// It exists for the kinds that produce a MEDIUM.
///
/// An installer ISO is not an object file and never will be. Pointing
/// [`audit_artifact`] at `gunnar-installer.iso` answers `NOT MEASURED — Unknown
/// file magic` — MEASURED on oden 2026-08-16, and the sentence is *true*. It is
/// simply about the wrong subject: an ISO carries no `.nordisk.buildthing` section
/// and no symbol table, so there is nothing there to agree or disagree with a
/// receipt, and no amount of work on the audit will ever change that.
///
/// The receipt that matters for an installer is the one compiled into the program
/// it LAYS DOWN. `assemble_cell::compose_installer_row` lists that binary as this
/// cell's second artifact for exactly this reason, so the subject is simply **the
/// first listed artifact the audit can read**, and the artifact the operator is
/// shown stays the medium.
///
/// # Why this is not a way of making an ISO pass
///
/// It never suppresses a verdict. A list whose artifacts are all unreadable still
/// answers on the FIRST one, so a medium shipped without its program still says
/// NOT MEASURED — which is what a `[[build.iso]]` live-appliance cell (one
/// artifact, the medium) still answers today. Only a build that genuinely produced
/// a readable program gets a verdict, and that verdict is free to come out
/// `Disproven`.
fn audit_first_readable(artifacts: &[&Path]) -> Option<(Option<Receipt>, Proof)> {
let mut fallback = None;
for p in artifacts {
let (receipt, proof) = audit(p);
if proof.verdict != ReceiptVerdict::NotMeasured {
return Some((receipt, proof));
}
fallback.get_or_insert((receipt, proof));
}
fallback
}
/// Read an artifact's receipt and measure its claims against the artifact.
///
/// A free function rather than a method because it touches nothing but the path:
/// that is what lets [`the_audit_reports_every_outcome_it_has_a_name_for`] point it
/// at files it built itself and watch every verdict come out, including the ones
/// that must never read as a pass.
///
/// [`the_audit_reports_every_outcome_it_has_a_name_for`]: tests::the_audit_reports_every_outcome_it_has_a_name_for
fn audit(artifact: &Path) -> (Option<Receipt>, Proof) {
let a = audit_artifact(artifact);
let proof = Proof {
verdict: a.verdict(),
lines: a.lines(),
};
(a.receipt.clone(), proof)
}
/// What [`DeclaredForge::plan_chain`] hands back: the repo's builds, the planned
/// steps, the `--opt` line, the params and the kernel decision.
type PlannedChain = (
RepoBuilds,
Vec<dwarves::build_steps::BuildStep>,
Vec<String>,
Vec<String>,
Option<nornir_build_thing::kernel_features::KernelDecision>,
);
impl DeclaredForge {
/// **ONE derivation for both readers** — `tree()` and `try_toggle()` must look at the
/// same tree, or the judge refuses a node the pane drew (measured 2026-08-26 on the
/// real declaration: the pane derived the donor's kernel across the constellation,
/// the judge derived it without the donor, and `kernel.pci` was "not a setting").
/// The picked preset lands on the declaration, exactly as `kernel_override` puts it
/// on the build's; the donor of an edge is looked up in ITS checkout.
fn derived_tree(
&self,
variant: &str,
preset: Option<Preset>,
) -> Option<nornir_build_thing::tree::SettingsTree> {
use nornir_build_thing::tree::SettingsTree;
let mut set = self.current()?.set.clone();
if let Some(p) = preset {
for k in &mut set.kernels {
k.preset = p.as_str().to_string();
}
}
if let Some(inst) = set.installers.iter().find(|i| i.name == variant) {
if let Some(d) = &inst.kernel_donor {
let mut donor = self
.builds
.repo(&d.repo)
.and_then(|r| r.set.kernels.iter().find(|k| k.name == d.kernel).cloned());
if let (Some(k), Some(p)) = (donor.as_mut(), preset) {
k.preset = p.as_str().to_string();
}
return Some(SettingsTree::of_installer(inst, donor.as_ref()));
}
}
SettingsTree::of_set(&set, variant)
}
/// **The chain a `BYGG <kant>` means, planned through the CLI door's own
/// functions and nothing else.**
///
/// Four calls, in the order `build_cells` makes them, because a second ordering
/// is a second product:
///
/// 1. `refuse_build_flags` — the grammar's judge. A target that is no edge is
/// refused with the same words on both doors (law B).
/// 2. `expand_opts_for` — the `--opt` line, expanded and spelling-checked.
/// 3. `resolve_kernel_axis` — the kernel half of the line judged against the
/// donor BEFORE anything spawns; the decision the pin is asked about.
/// 4. `plan_build_steps` — the edge DAG (a consumer of edda's `BuildDag::derive`
/// since `7881fd7`, so the donor's donor is a step), then `describe_plan`,
/// whose sentences ARE the prompt's dry pass.
///
/// The `--opt` line comes off the TREE's flags, not off a second reading of the
/// settings: the flag on screen is the flag the build gets (law C), and the CLI
/// line the node menu shows is spelled from the same list.
fn plan_chain(&mut self, plan: &crate::build_steps::StepPlan) -> Result<PlannedChain, String> {
if let Some(why) = &self.invalid {
return Err(format!("deklarationen vägras vid inläsning: {why}"));
}
let Some(repo) = self.current().cloned() else {
return Err("inget recept upplöst — sök en rot först".to_string());
};
let variant = plan.variant.as_str();
let builds = std::slice::from_ref(&plan.edge_path);
dwarves::buildthing::refuse_build_flags(&repo.set, &repo.repo, Some(variant), builds)
.map_err(|e| format!("{e:#}"))?;
// Every `--opt` the tree carries, donor's and consumer's alike: routing onto
// the steps is the planner's job (the path prefix is the brace) and doing it
// here as well would be the second implementation of one rule.
//
// **IN THE TREE'S ORDER, and deduped by first appearance.** The consuming
// step's flags are the whole line in declaration order, so they lead. Sorting
// them — which a `BTreeSet` here did for one commit — reorders the line the
// kernel grammar applies IN ORDER, cascade and contract refusals included:
// the same set of flags in another order is not guaranteed to be the same
// decision, and the prompt would have been handed the declaration's order
// while ⚒ handed an alphabetical one. Caught by
// `robot_two_doors::the_chain_the_pane_plans_is_the_chain_the_prompt_plans`,
// which compares the two doors' plan lines verbatim.
// `StepPlan::routed_opts` is that rule's ONE writer — it is also what spells the
// CLI line printed under the button, which is the only way the two can be
// guaranteed to name the same build.
let line = plan.routed_opts();
// The consumer's `--param` line, off the same declared-parameter reader
// `BYGG` and the printed line both use. A donor step carries none; its routed
// `--opt` line is all it takes from this screen.
let params = Forge::declared_params(self, variant, &plan.settings);
let opts = dwarves::buildthing::expand_opts_for(&repo.set, Some(variant), &line, ¶ms)
.map_err(|e| format!("{e:#}"))?;
let (decision, _) =
dwarves::buildthing::resolve_kernel_axis(&repo.set, &repo.root, Some(variant), &opts)
.map_err(|e| format!("{e:#}"))?;
let steps = dwarves::build_steps::plan_build_steps(
&repo.set,
&repo.root,
Some(variant),
builds,
&opts,
)
.map_err(|e| format!("{e:#}"))?;
Ok((repo, steps, opts, params, decision))
}
/// The planned chain as the console's own plan — the steps with their
/// coordinates, typs and routed flags, each wearing the planner's own line.
fn staged(
plan: &crate::build_steps::StepPlan,
steps: &[dwarves::build_steps::BuildStep],
lines: &[String],
) -> crate::build_steps::StepPlan {
let planned: Vec<(String, ArtifactKind, Vec<String>)> = steps
.iter()
.map(|s| {
(
s.coordinate().to_string(),
s.edge.typ,
s.opts.iter().map(|o| format!("--opt {o}")).collect(),
)
})
.collect();
let consumer_typ = plan.steps.last().map_or(ArtifactKind::Exe, |s| s.typ);
let consumer_flags = plan
.steps
.last()
.map(|s| s.flags.clone())
.unwrap_or_default();
crate::build_steps::StepPlan::from_planned(
plan.product.as_deref(),
&plan.variant,
consumer_typ,
&plan.edge_path,
&plan.coordinate,
consumer_flags,
&planned,
lines,
plan.cli.clone(),
plan.params.clone(),
)
}
}
impl Forge for DeclaredForge {
fn axis(&mut self, pick: &AxisPick) -> Axis {
self.axis_for(pick)
}
fn resolve(&mut self, query: &Query) -> Result<Menu, String> {
self.run = None;
self.outcome = None;
self.note = None;
self.axis_work = None;
self.take_live();
match query {
Query::Artifact(name) => {
let hits: Vec<String> = self
.candidates(name)
.iter()
.map(|r| r.repo.clone())
.collect();
match hits.as_slice() {
// ── AN EMPTY ROSTER IS ITS OWN ANSWER, NOT A MISS ────────
//
// "ingen deklaration nämner gunnar" is true of a root that
// holds 40 checkouts and true of a root that holds none,
// and only one of those is the operator's fault. MEASURED
// 2026-08-22: launched from ~/git with no
// $DWARVES_CONSTELLATION, `default_root()` returns the
// PARENT (~), the roster is empty, and every
// search reads as "that artifact does not exist here".
[] if self.builds.repos.is_empty() => Err(format!(
"0 repon deklarerar ett byggrum under {} — sätt \
$DWARVES_CONSTELLATION till katalogen som håller \
utcheckningarna sida vid sida",
self.builds.root.display()
)),
[] => Err(format!(
"ingen deklaration under {} nämner {name:?} ({} repo(n) \
deklarerar ett byggrum där)",
self.builds.root.display(),
self.builds.repos.len()
)),
[one] => {
let how = if one == name {
"hela receptet".to_string()
} else {
"löst ur namnet".to_string()
};
let one = one.clone();
self.take_code_road_unless_building(&one);
Ok(self.menu_for(&one, how))
}
many => Err(format!(
"{name:?} deklareras av flera utcheckningar: {} — skriv namnet på \
den du menar",
many.join(", ")
)),
}
}
Query::At(c) => {
let repo = c
.krate
.strip_suffix(RECIPE_SUFFIX)
.unwrap_or(&c.krate)
.to_string();
if self.builds.repo(&repo).is_none() {
if self.builds.repos.is_empty() {
return Err(format!(
"0 repon deklarerar ett byggrum under {} — sätt \
$DWARVES_CONSTELLATION till katalogen som håller \
utcheckningarna sida vid sida",
self.builds.root.display()
));
}
return Err(format!("ingen utcheckning heter {repo:?}"));
}
self.take_code_road_unless_building(&repo);
Ok(self.menu_for(&repo, format!("angiven koordinat {c}")))
}
}
}
/// **The kconfig configurator, hosted by ⚒.**
///
/// Three answers, and every one of them is a sentence:
///
/// * a kernel cell whose `[[build.kernel]]` is there ⇒ the graph, its preset
/// mirrored from the settings map so the leaf and the picker cannot disagree;
/// * a kernel cell whose recipe is MISSING ⇒ the same named refusal
/// [`kernel_settings`] gives, for the same reason: a kernel is two declarations
/// that must agree by name, and a blank is indistinguishable from a typo;
/// * anything else ⇒ a refusal that says what this constellation actually
/// declares. **Exactly one repo declares a `[[build.kernel]]` today**, so a
/// leaf that rendered an empty pane on the other fifteen would be a blank for
/// almost every operator who opens the tab. The names are read off the
/// discovered checkouts rather than typed here, so the sentence cannot rot the
/// day a second repo declares one.
fn leaf(&mut self, row: &str, target: &str, settings: &BTreeMap<String, String>) -> Leaf<'_> {
let Some(cell) = self.cell(row, target).cloned() else {
return Leaf::None;
};
if cell.kind != ArtifactKind::Kernel {
// An INSTALLER that consumes a donor kernel gets the sharper sentence:
// the cell HAS a kernel — a declared, pin-verified one — it just is not
// configured here. Naming the donor turns "this is not a kernel" into
// "the choice is made in the donor's configurator", which is the actual
// answer to the question the operator opened this leaf with.
if cell.kind == ArtifactKind::Installer {
if let Some(d) = self
.current()
.and_then(|r| r.set.installers.iter().find(|i| i.name == cell.name))
.and_then(|i| i.kernel_donor.clone())
{
return Leaf::Refused(format!(
"`{row} · {target}` bygger ingen kärna SJÄLV — den KONSUMERAR \
donatorkärnan `{}/{}` ([build.installer.kernel_donor]). Valet \
görs i `{}`s konfigurator; BYGG här verifierar den byggda \
kärnan mot sin pin och vägrar en obyggd eller avvikande",
d.repo, d.kernel, d.repo
));
}
}
let declaring: Vec<&str> = self
.builds
.repos
.iter()
.filter(|r| !r.set.kernels.is_empty())
.map(|r| r.repo.as_str())
.collect();
let who = if declaring.is_empty() {
"ingen utcheckning i konstellationen deklarerar en".to_string()
} else {
format!("deklareras av: {}", declaring.join(", "))
};
return Leaf::Refused(format!(
"`{row} · {target}` bygger ingen kärna — det här bladet konfigurerar en \
[[build.kernel]], och {who}"
));
}
let Some(set) = self.current().map(|r| r.set.clone()) else {
return Leaf::None;
};
let Some(k) = set.kernels.iter().find(|k| k.name == cell.name).cloned() else {
let declared = if set.kernels.is_empty() {
"inga alls".to_string()
} else {
set.kernels
.iter()
.map(|k| k.name.as_str())
.collect::<Vec<_>>()
.join(", ")
};
return Leaf::Refused(format!(
"INGEN DEKLARATION för `{}` — en [[build.kernel]] med det namnet saknas; \
deklarerade: {declared}",
cell.name
));
};
// The kconfig solver's answer for THIS kernel, if a build left one: decided
// before the pane is borrowed, applied after.
let why = self
.current()
.map(|r| nornir_build_thing::kernel::kernel_output_dir(&r.root, &k))
.and_then(|out| self.kernel_why_changed(out, &k.name));
// Lazy: the first kernel cell an operator picks pays for the layout, and a
// shell nobody opens this tab in pays nothing.
let pane = self.kernel.get_or_insert_with(KernelScene::embedded);
if let Some(read) = why {
pane.set_why_read(read);
}
// ── ONE VALUE, ONE CONTROL ───────────────────────────────────────────
// The settings map is the source of truth (it is what rides into
// `Act::Build`), so the leaf is re-pointed from it every frame; the
// embedded pane draws no preset strip of its own. `set_preset` and
// `set_profile` both return early on no change, so a settled frame does no
// work — the graph must never be relaid on the frame that draws it.
let want = settings
.get(PRESET_KEY)
.and_then(|v| Preset::parse(v.trim()))
.or_else(|| Preset::parse(&k.preset));
if let Some(p) = want {
pane.set_preset(p);
}
if let Some(p) = HardeningProfile::parse(&k.profile) {
pane.set_profile(p);
}
// …and because the settings map IS the control, the pane's own strip stays off.
pane.set_chrome(false);
Leaf::Pane(pane)
}
/// **The configurator with nothing picked** — the question the retired ⚙ Kernel tab
/// was the only home for: *"what is in a `slim` kernel?"*, asked before an operator
/// knows which row they want.
///
/// The SAME instance [`leaf`](Forge::leaf) hands back, with its own preset strip
/// turned ON because no host control is pointing at it this frame. Two instances
/// would be two presets and the operator would pick in one and build the other; one
/// instance in two mutually exclusive states is one preset with one control on it.
///
/// ── BUT IT IS THE RECIPE THAT DECIDES, NOT THE TAB ──────────────────────────
///
/// This used to hand back the configurator UNCONDITIONALLY, and that is wrong for
/// the same reason the ⚙ tab was wrong: *"varför skall kernel dyka upp jämt? det
/// beror ju på receptet"* (owner, 2026-08-26). A recipe that builds a library
/// declares no kernel and must not offer to configure one — the standing question
/// *"what is in a `slim` kernel"* is only standing for a recipe that HAS one.
///
/// MEASURED across the constellation on the day this was written: of 16 checkouts
/// declaring a build space, exactly **one** (`tunnr`) declares a `[[build.kernel]]`.
/// So the unconditional pane was the wrong answer for 15 recipes out of 16, and the
/// one it was right for was not the one being looked at.
///
/// Three answers, because there are three states and the seam already has the words
/// for them — collapsing any two is what put a blank pane on screen:
///
/// * **no recipe picked** → [`Leaf::None`]. The scene says "välj en rad och en
/// kolumn"; a configurator here would be pointing at nothing.
/// * **a recipe that declares no kernel** → [`Leaf::Refused`], NAMING the recipe.
/// Not `None`: "this library has no kernel" and "you have picked nothing" are
/// different facts, and a leaf that renders identically for both is the defect.
/// * **a recipe that declares one** → the configurator, chrome on, as before.
fn browse(&mut self) -> Leaf<'_> {
// Answer from the DECLARATION before borrowing the pane: `self.kernel` is
// borrowed mutably for the rest of the function, and the roster read has to
// finish first.
let Some(repo) = self.repo.clone() else {
return Leaf::None;
};
let declared: Vec<String> = match self.current() {
Some(r) => r.set.kernels.iter().map(|k| k.name.clone()).collect(),
None => return Leaf::None,
};
if declared.is_empty() {
// A donor-consuming installer is the third state: the recipe builds no
// kernel, but one of its media BOOTS a declared one. The refusal names
// it, because "this repo has no kernel" is a false answer for a repo
// whose installer ships tunnr's.
if let Some((iname, d)) = self.current().and_then(|r| {
r.set
.installers
.iter()
.find_map(|i| i.kernel_donor.clone().map(|d| (i.name.clone(), d)))
}) {
return Leaf::Refused(format!(
"`{repo}` deklarerar ingen [[build.kernel]] — men `{iname}` \
KONSUMERAR donatorkärnan `{}/{}`. Valet görs i `{}`s \
konfigurator; BYGG på `{iname}` verifierar den byggda kärnan \
mot sin pin",
d.repo, d.kernel, d.repo
));
}
return Leaf::Refused(format!(
"`{repo}` deklarerar ingen [[build.kernel]] — det här bladet \
konfigurerar en kärna, och det här receptet bygger ingen"
));
}
let pane = self.kernel.get_or_insert_with(KernelScene::embedded);
// ONE control on the preset, browsing or not: the settings tree's `kernel.preset`
// val is it (gap #6 — the canvas is the `kärna` node's DETAIL, and a strip of
// its own beside the tree's would be two widgets with one accessible name).
pane.set_chrome(false);
Leaf::Pane(pane)
}
/// The kernel leaf's PRESENCE: does the resolved recipe have a KERNEL ANSWER —
/// a `[[build.kernel]]` of its own, or an installer that binds a donor's kernel
/// (`[build.installer.kernel_donor]`, the gunnar → tunnr/tunnr-kernel road)?
///
/// The same declaration read [`browse`](Forge::browse) opens with, but
/// side-effect-free: `browse` lazily builds the configurator and flips its chrome,
/// which a per-frame presence ask must never do. The donor case answers `true`
/// because the leaf's surface for it — the donor, NAMED, and where the choice is
/// made — IS that recipe's kernel answer: hiding it would tell a gunnar operator
/// their medium boots no kernel, which is false. A recipe with neither (a plain
/// library) answers `false`, and no recipe answers `false` — those two are the
/// states with no kernel answer at all, and the leaf is ABSENT rather than a frame
/// full of refusal.
fn kernel_declared(&self) -> bool {
self.repo.is_some()
&& self.current().is_some_and(|r| {
!r.set.kernels.is_empty()
|| r.set.installers.iter().any(|i| i.kernel_donor.is_some())
})
}
/// **The settings tree for this selection** — edda's `SettingsTree::of_set` over the
/// resolved declaration (the CODE's set when the wasm answered, the table's else),
/// with the operator's preset folded into the kernel declaration first, because the
/// preset decides which `kernel.<symbol>` features exist at all.
///
/// The donor of an installer's edge is looked up ACROSS the constellation (`tunnr`'s
/// `tunnr-kernel` for gunnar's installer): the edge's coordinate is `repo/variant`,
/// and the repo is another checkout. `of_set` alone looks only in its own set and
/// would derive the donor's symbols from a default preset.
///
/// Nothing picked ⇒ the standing question's tree: the recipe's own kernel, else the
/// installer that carries the edge, else an empty tree. No recipe ⇒ `None`.
fn tree(
&mut self,
row: Option<&str>,
target: Option<&str>,
settings: &BTreeMap<String, String>,
) -> Option<nornir_build_thing::tree::SettingsTree> {
use nornir_build_thing::tree::SettingsTree;
let repo = self.current()?.clone();
// ── THE CODE ANSWERS FIRST (stage D): `options(variant, "")` over wasm ──────
// The loaded build thing's own tree, when the code road is open and the
// operator has not moved the preset off the declaration — `OptionsQuery`
// carries no values yet, so a moved preset (which changes WHICH symbols
// exist) is derived locally from the same declaration until edda's query
// grows a `vals` map. Said here so the two roads cannot be confused.
let preset_moved = settings.get(PRESET_KEY).is_some_and(|v| {
let want = v.trim();
!repo.set.kernels.iter().any(|k| k.preset == want) && Preset::parse(want).is_some()
});
if let (Some(r), Some(t), false) = (row, target, preset_moved) {
if let Some(name) = self.cell(r, t).map(|c| c.name.clone()) {
if let Some(code) = self.code.as_mut() {
match code.handle.options(&name, "") {
Ok(Some(tree)) => return Some(tree),
Ok(None) => {}
Err(e) => eprintln!(
"[dwarves] kod · options({name}): {e:#} — härleder ur deklarationen"
),
}
}
}
}
let set = repo.set.clone();
let preset = settings
.get(PRESET_KEY)
.and_then(|v| Preset::parse(v.trim()));
let variant: String = match (row, target) {
(Some(r), Some(t)) => self.cell(r, t)?.name.clone(),
_ => {
// The standing question: the variant that carries the `kärna` feature.
if let Some(k) = set.kernels.first() {
k.name.clone()
} else if let Some(i) = set.installers.iter().find(|i| i.kernel_donor.is_some()) {
i.name.clone()
} else {
return Some(SettingsTree {
variant: String::new(),
typ: ArtifactKind::Exe,
roots: Vec::new(),
});
}
}
};
self.derived_tree(&variant, preset)
}
/// **The judge** — the loaded build thing's `try_toggle` over wasm when the code
/// road is open (a donor's build thing must answer for its own kernel), the derived
/// tree's otherwise. One judge per click, never two.
fn try_toggle(
&mut self,
req: &nornir_build_thing::tree::ToggleRequest,
) -> nornir_build_thing::tree::ToggleDecision {
if let Some(code) = self.code.as_mut() {
match code.handle.try_toggle(req) {
Ok(d) => return d,
Err(e) => eprintln!(
"[dwarves] kod · try_toggle({}): {e:#} — dömer ur deklarationen",
req.path
),
}
}
// The request's variant, derived the SAME way `tree()` derives it — donor and
// preset included (the preset rides in `req.vals`).
let preset = req
.vals
.get("kernel.preset")
.or_else(|| req.vals.get("preset"))
.and_then(|v| Preset::parse(v.trim()));
let derived = self.derived_tree(&req.variant, preset);
match derived {
Some(t) => t.try_toggle(req),
None => nornir_build_thing::tree::ToggleDecision {
path: req.path.clone(),
on: req.on,
verdict: nornir_build_thing::plugin::Verdict::refuse(format!(
"ingen variant `{}` i det upplösta receptet",
req.variant
)),
cascade: Vec::new(),
refuses: Vec::new(),
activates: Vec::new(),
},
}
}
/// **The verbs** — the loaded build thing's `actions(kind)` over wasm when the code
/// road is open (an impl may refine: UNPACK), edda's table otherwise.
fn actions(&mut self, kind: ArtifactKind) -> Vec<nornir_build_thing::verbs::Action> {
if let Some(code) = self.code.as_mut() {
match code.handle.actions(kind) {
Ok(a) => return a,
Err(e) => eprintln!(
"[dwarves] kod · actions({}): {e:#} — tabellen svarar",
kind.as_str()
),
}
}
nornir_build_thing::verbs::actions_for(kind)
}
/// The product word: the resolved repo's name — `dwarves buildthing <repo>`.
fn product(&self) -> Option<String> {
self.current().map(|r| r.repo.clone())
}
/// The DECLARED parameters among the site values, off the cell's own declaration —
/// the ONE reader both the sent plan and the printed CLI line take their `--param`
/// line from.
fn declared_params(
&mut self,
variant: &str,
settings: &BTreeMap<String, String>,
) -> Vec<String> {
self.cells
.values()
.find(|c| c.name == variant)
.map(|c| declared_param_values(c, settings))
.unwrap_or_default()
}
/// **The dry pass** — the chain planned, the pin asked, nothing spawned.
fn plan_edge(
&mut self,
plan: &crate::build_steps::StepPlan,
) -> Result<crate::build_steps::StepPlan, String> {
let (_repo, steps, _opts, _params, decision) = self.plan_chain(plan)?;
let lines = dwarves::build_steps::describe_plan(&steps, decision.as_ref())
.map_err(|e| format!("{e:#}"))?;
for l in &lines {
eprintln!("[dwarves] plan · {l}");
}
Ok(Self::staged(plan, &steps, &lines))
}
/// **`BYGG <kant>` — the chain, run.**
///
/// Planned by [`plan_edge`](Forge::plan_edge)'s own road (one planner), then
/// handed to `dwarves::build_steps::run_build_chain_watched` on a worker thread:
/// the same registry, the same runa, the same `assemble_cell` the prompt's
/// `--build` reaches. This door is a second READER of that run, never a second
/// runner — which is the whole of law B here, and the reason the wave-3 stub
/// (`missing_runner()` on both arms, naming a `BuildRun::spawn_chain` that has
/// never existed) could not simply be replaced with a build of its own.
///
/// Answers the plan it started — the DAG, not the click's two steps.
fn build_edge(
&mut self,
plan: &crate::build_steps::StepPlan,
) -> Result<crate::build_steps::StepPlan, String> {
let (repo, steps, opts, params, decision) = self.plan_chain(plan)?;
let lines = dwarves::build_steps::describe_plan(&steps, decision.as_ref())
.map_err(|e| format!("{e:#}"))?;
let staged = Self::staged(plan, &steps, &lines);
// The run's rows go to the same warehouse a `dwarves run` files under, so the
// pipeline DAG viewer draws this chain like any other run.
let warehouse = dwarves::store::warehouse_from(
std::env::var("DWARVES_WAREHOUSE").ok().as_deref(),
std::env::var("HOME").ok().as_deref(),
dwarves::store::is_system_scope(),
);
let variant = plan.variant.clone();
let refusal_on = steps
.first()
.map_or_else(|| plan.coordinate.clone(), |s| s.coordinate().to_string());
self.chain = Some(crate::build_steps::ChainRun::spawn(
refusal_on,
move |watch| {
dwarves::build_steps::run_build_chain_watched(
&repo,
&repo.repo,
&variant,
steps,
decision.as_ref(),
&opts,
¶ms,
Some(&warehouse),
Some(watch),
)
.map(|_| ())
.map_err(|e| format!("{e:#}"))
},
));
Ok(staged)
}
/// **The chain's steps as the runner has reported them so far** — drained once a
/// frame, never blocking. A chain that refused before its first step comes back
/// as that step refused by name, which is [`ChainRun::poll`]'s own doing.
fn steps(&mut self) -> Option<Vec<crate::build_steps::StepReport>> {
let reports = self.chain.as_mut()?.poll();
if let Some(why) = self
.chain
.as_mut()
.and_then(crate::build_steps::ChainRun::take_note)
{
self.note = Some(format!("BYGG <kant>: {why}"));
}
Some(reports)
}
fn act(&mut self, act: Act) {
match act {
Act::Build {
row,
target,
settings,
} => self.start_build(&row, &target, &settings),
Act::Run(argv) => self.run_artifact(&argv),
Act::Start => self.launch(),
Act::Stop => self.take_live(),
Act::Reboot => {
self.take_live();
self.launch();
}
}
}
fn poll(&mut self) -> Report {
self.settle();
let menu = self.settle_code();
let (phase, elapsed) = match (&self.run, &self.outcome) {
(None, _) => (None, 0),
(Some(r), None) => (Some(Phase::Building), r.elapsed_ms()),
(Some(r), Some(o)) => (
Some(if o.ok {
Phase::Done
} else {
Phase::Failed(
r.report
.as_ref()
.map_or_else(|| "bygget misslyckades".to_string(), |x| x.headline()),
)
}),
r.elapsed_ms(),
),
};
let (mut live, mut screen) = (false, None);
let lines = if let Some(b) = &self.live {
live = b.is_running();
screen = b.frame().map(|f| Screen {
width: f.width,
height: f.height,
rgba: f.rgba,
});
b.log()
} else if let Some(r) = &self.run {
r.log.clone()
} else if let Some(b) = &self.code_build {
// The recipe crate's wasm build IS a build; the console shows it.
b.log.clone()
} else {
self.code_log.clone()
};
Report {
phase,
elapsed_ms: elapsed,
outcome: self.outcome.clone(),
live,
lines,
screen,
note: self.note.clone(),
menu,
}
}
}
// ── declaration → data ────────────────────────────────────────────────────────
/// **Which boot engine a Console pane drives**, from `$DWARVES_INSTALLER_ENGINE` —
/// the launcher AND the name it goes by, out of ONE match arm.
///
/// Carried over from the pane this scene replaced, because without it the real RAID
/// installer engine is not merely unrecorded but UNREACHABLE from the UI — the scene
/// would always take the default single-disk path and no operator could ask for the
/// other one. Unset, or any other value, is the default: the shipping UI never changes
/// engine silently.
///
/// **Why one function and not two (L5, and the guard that could not go red).** This
/// used to be a `boot_launcher` returning the `Arc` and a separate `boot_engine_name`
/// returning the string — two independent copies of the same three-arm match, and the
/// test watched the STRING one. MEASURED 2026-08-23: renaming only `boot_launcher`'s
/// arm to `Some("edda_raid")` makes `DWARVES_INSTALLER_ENGINE=edda-raid` fall through
/// to the default launcher — the RAID engine unreachable from the UI, which is the one
/// defect the test's own doc says it exists to catch — and the test still reported
/// `ok`. A name that mirrors a decision is not the decision. Now the name is produced
/// BY the arm that produces the launcher, so asserting the name asserts the arm, and
/// the two can no longer disagree. The `$DWARVES_INSTALLER_ENGINE` read is also done
/// once at the call site rather than twice, which is the same fault one level up.
fn boot_engine(engine: Option<&str>) -> (Arc<dyn BootLauncher>, &'static str) {
match engine {
Some("edda-raid") => (
Arc::new(crate::boot_run::EddaRaidInstallerLauncher::default()),
"edda-raid",
),
_ => (Arc::new(crate::boot_run::TunnrBootLauncher), "tunnr"),
}
}
/// The map key for one cell of the matrix. A unit separator, so a column label
/// containing a colon cannot collide with a row name.
fn key(row: &str, target: &str) -> String {
format!("{row}\u{1f}{target}")
}
/// The delivery cells that chain off `variant`, labelled `<kind>:<cell>`.
///
/// A repo may declare two containers off one atom, so the cell name has to be part of
/// the label; the kind leads so the column strip reads as an axis.
fn chained(
set: &nornir_build_thing::variant::BuildVariantSet,
variant: &str,
) -> Vec<(String, Cell)> {
let mut out = Vec::new();
for c in &set.containers {
if c.layers
.iter()
.any(|l| l.from_variant.as_deref() == Some(variant))
{
out.push((
format!("container:{}", c.name),
Cell {
name: c.name.clone(),
kind: ArtifactKind::Container,
params: Vec::new(),
fixtures: BTreeMap::new(),
options: Vec::new(),
},
));
}
}
for v in &set.vms {
if v.from_variant.as_deref() == Some(variant) {
out.push((
format!("vm:{}", v.name),
Cell {
name: v.name.clone(),
kind: ArtifactKind::Vm,
params: Vec::new(),
fixtures: BTreeMap::new(),
options: Vec::new(),
},
));
}
}
for i in &set.isos {
if i.from_variant.as_deref() == Some(variant) {
out.push((
format!("iso:{}", i.name),
Cell {
name: i.name.clone(),
kind: ArtifactKind::Iso,
params: i.params.clone(),
fixtures: i.fixtures.clone(),
options: i.options.clone(),
},
));
}
}
for i in &set.installers {
// WHICH ROW AN INSTALLER HANGS OFF. `installer_variant` is the cargo build that
// produces the PID-1 riding the ISO; `variant` is the appliance the installer
// LAYS DOWN. Matching only the former made the column unreachable for every
// installer actually declared in this constellation — gunnar's and holger's both
// name `variant` and leave `installer_variant` unset (it is `#[serde(default)]`,
// so it is the empty string, which equals no row), so the installer was declared,
// listed by the CLI, and had no clickable column in the matrix at all. MEASURED
// 2026-08-16: `installer:gunnar-installer` resolved to no widget.
//
// A declared `installer_variant` still wins outright — korp's shape is unchanged
// — and the fallback is taken ONLY when the field is absent, so one installer never
// lands on two rows.
let hangs_off = if i.installer_variant.is_empty() {
i.variant == variant
} else {
i.installer_variant == variant
};
if hangs_off {
out.push((
format!("installer:{}", i.name),
Cell {
name: i.name.clone(),
// `Installer`, NOT `Iso`. edda added the variant precisely because
// "until this variant existed the attestation spelled both `iso`, so
// an auditor could not tell them apart" — a live ISO *is* the
// appliance, an installer ISO *produces* one. Spelling it `Iso` here
// re-created in the pane the exact conflation the kind was added to end.
kind: ArtifactKind::Installer,
// The site values this cell DECLARES. Carried verbatim so the scene
// renders whatever the recipe says and nothing it invented.
params: i.params.clone(),
// …and the fixtures it declares for them, the same way.
fixtures: i.fixtures.clone(),
// …and the OPT-OUTS. These decide the rest of the block:
// which of the parameters above are asked for at all, and
// which cargo features the packed app binary is built with.
options: i.options.clone(),
},
));
}
}
out
}
/// The marks a row carries, straight out of the declaration and in its order:
/// the suites it ships in, its role, whether it is the default for that role, and how
/// it links to its siblings. Nothing is derived and nothing is added.
fn badges_of(v: &BuildVariant) -> Vec<String> {
let mut out: Vec<String> = v.suites.clone();
if let Some(role) = &v.role {
out.push(format!("{role:?}").to_lowercase());
}
if v.is_default {
out.push("default".to_string());
}
out
}
/// The settings one cell resolves: the cargo axes the declaration fixed, plus the
/// switches **that row** declares open.
///
/// Every value shown is the one the build will use. A setting the declaration pinned
/// is shown fixed, with the reason; a feature the row declares `open` is shown as a
/// toggle the operator may flip.
///
/// # Why the row's `open`, and not the repo's `offers`
///
/// Every row of this matrix is an ASSEMBLY, and the two are not interchangeable:
/// `offers` is the SPACE the repo's declarations may draw from — the set the
/// declaration validator rejects a stray feature against — while `open` is what one
/// particular assembly leaves switchable. Reading the space as a switch list put a
/// toggle for every feature ANY row could use on EVERY row: a GPU checkbox on a
/// server that has only ever had a CPU path.
///
/// Greying it there instead of dropping it is not the fix. A greyed column is honest
/// because the column strip is a MAP — every delivery target the repo has, so an
/// operator can see the one this row lacks. A setting is not a map of anything; it is
/// the argv this build will run with. A control the recipe never declared has no
/// business in that list in either state, which is the same reason no row grows an
/// `rpm` column nobody declared.
/// The declared kernel facts a `kernel` cell carries — the ones that decide what
/// comes out. `nuked` and `slim` differ by ~86 KiB and by what an appliance can
/// do at all, so a cell that showed the KIND and hid the PRESET would be showing
/// the half that decides nothing. Empty for every other kind, and empty for a
/// `kind = "kernel"` row with no matching declaration (which is legal: the
/// variant row is a leaf, and the sugar may live in another suite's file).
/// **The kernel a donor-consuming installer cell shows for itself** — the fact
/// row that makes the cell's kernel VISIBLE in ⚒.
///
/// An installer with a `[build.installer.kernel_donor]` boots a kernel some
/// OTHER checkout declares, and until this row existed the pane said nothing
/// about it: the operator saw dhcp/wireguard toggles and site params while the
/// medium's kernel — the one thing the cell cannot toggle — was decided off
/// screen. One fixed row names the donor and where the choice is made; the
/// choice itself is the DONOR's configurator (the same two-step the CLI walks:
/// build the kernel where it is declared, then BYGG here consumes the built,
/// pin-verified artifact — `assemble_cell::installer_kernel` refuses a stale or
/// missing one by name).
fn donor_settings(set: &nornir_build_thing::variant::BuildVariantSet, cell: &Cell) -> Vec<Setting> {
if cell.kind != ArtifactKind::Installer {
return Vec::new();
}
let Some(d) = set
.installers
.iter()
.find(|i| i.name == cell.name)
.and_then(|i| i.kernel_donor.as_ref())
else {
return Vec::new();
};
vec![Setting::fixed(
"kärna",
format!("{}/{}", d.repo, d.kernel),
format!(
"donator — valet görs i `{}`s konfigurator; BYGG verifierar mot dess pin",
d.repo
),
)]
}
fn kernel_settings(
set: &nornir_build_thing::variant::BuildVariantSet,
cell: &Cell,
) -> Vec<Setting> {
if cell.kind != ArtifactKind::Kernel {
return Vec::new();
}
let Some(k) = set.kernels.iter().find(|k| k.name == cell.name) else {
// **A KERNEL CELL WITH NO KERNEL DECLARATION SAYS SO.** Returning an empty Vec
// here rendered a cell headed "Kernel" with nothing under it and no reason —
// indistinguishable from a kernel that simply declares no settings, and from a
// pane that failed to load.
//
// It is reachable by a plain typo, because a kernel recipe is TWO declarations
// that must agree by name: a `[[build.kernel]]` row and a
// `[[build.variant]] kind = "kernel"` row pointing at it. Misspell either and
// the matrix still shows the column — the variant is what puts it there — while
// the recipe it names is never found.
//
// Naming the miss costs one row and turns an unexplained blank into the actual
// answer. The declared names are listed because "not found" without "here is
// what IS declared" sends the reader to grep for something they cannot spell.
let declared = if set.kernels.is_empty() {
"inga alls".to_string()
} else {
set.kernels
.iter()
.map(|k| k.name.as_str())
.collect::<Vec<_>>()
.join(", ")
};
return vec![Setting::fixed(
"kernel",
format!("INGEN DEKLARATION för `{}`", cell.name),
format!("en [[build.kernel]] med det namnet saknas — deklarerade: {declared}"),
)];
};
// ── THE PRESET IS A CONTROL NOW, NOT A LABEL ─────────────────────────────
//
// It was `Setting::fixed(…, "deklarerad")` — four true words the operator could
// read and not act on, with the machine that produces them eleven tabs away in ⚙
// Kernel. The owner went looking for the configurator in ⚒ three separate times;
// three times the answer was "it is in the Kernel tab, by design". Build Thing is
// where you choose what gets built, so the choice is here.
//
// Open, therefore it lands in `Act::Build`'s settings map like every other open
// setting — and `kernel_override` below turns that into the declaration the build
// actually runs from. A picker that changed the picture and not the build would be
// strictly worse than the label it replaced.
//
// The BASE rides in the chosen entry's note rather than in a row of its own,
// because it is derived from the preset and a separate row would freeze at the
// DECLARED preset's base the moment the operator picked another one. Unless the
// declaration pins `base` explicitly — then the preset does NOT decide it, every
// note says so, and the pinned row is kept below.
let choices: Vec<Choice> = PRESETS
.iter()
.map(|p| {
let note = match &k.base {
Some(b) => format!("bas {b} (pinnad i deklarationen — följer inte preset)"),
None => format!("bas {}", p.base()),
};
Choice::new(p.as_str(), note)
})
.collect();
let mut out = vec![
Setting::choice(PRESET_KEY, k.preset.clone(), choices),
Setting::fixed("version", k.version.clone(), "deklarerad"),
];
if let Some(b) = &k.base {
out.push(Setting::fixed("base", b.clone(), "pinnad i deklarationen"));
}
if !k.profile.is_empty() {
out.push(Setting::fixed("profile", k.profile.clone(), "deklarerad"));
}
out
}
/// **The declaration the build ACTUALLY runs from**, once the operator's preset
/// pick is folded into it — or `None` when nothing was picked that changes it.
///
/// This is the whole point of moving the configurator into ⚒. `BuildRun::spawn_*`
/// clones `repo.set` and builds from that clone, and a kernel row is assembled from
/// its `[[build.kernel]]` declaration — so overriding the preset ON THE SET is
/// exactly, and only, overriding what gets built. Nothing downstream needs a new
/// parameter: the change is in the declaration the worker reads.
///
/// Returns `None` — leaving the declared build untouched — when the cell is not a
/// kernel, when nothing was picked, when the pick equals the declaration, or when
/// the pick is not a preset this kind knows. That last one is a refusal by
/// omission on purpose: `Preset::parse` is the same gate `KernelBuild::validate`
/// uses, and inventing a preset here would produce a build that fails deep inside
/// a toolchain container instead of a picker that never offered the word.
fn kernel_override(
repo: &RepoBuilds,
cell: &Cell,
settings: &BTreeMap<String, String>,
) -> Option<(RepoBuilds, String)> {
if cell.kind != ArtifactKind::Kernel {
return None;
}
let want = Preset::parse(settings.get(PRESET_KEY)?.trim())?;
let i = repo.set.kernels.iter().position(|k| k.name == cell.name)?;
let had = repo.set.kernels[i].preset.clone();
if had == want.as_str() {
return None;
}
let mut repo = repo.clone();
repo.set.kernels[i].preset = want.as_str().to_string();
let line = format!(
"{} · preset {had} → {} (vald i ⚒, bas {})",
cell.name,
want.as_str(),
repo.set.kernels[i].base(),
);
Some((repo, line))
}
fn settings_of(v: &BuildVariant, cell: &Cell) -> Vec<Setting> {
let mut out = vec![Setting::fixed("kind", cell.kind.as_str(), "deklarerad")];
out.push(Setting::fixed(
"profile",
format!("{:?}", v.profile).to_lowercase(),
"deklarerad",
));
if let Some(t) = &v.target {
out.push(Setting::fixed("target", t.clone(), "deklarerad"));
}
if let Some(m) = &v.member {
out.push(Setting::fixed("member", m.clone(), "deklarerad"));
}
for f in &v.features {
out.push(Setting::fixed(f.clone(), "true", "i atomen"));
}
for f in &v.open {
// A feature the row both enables and opens is refused by the declaration
// validator; the guard here is so a set built in code rather than parsed
// cannot render one key twice.
if !v.features.contains(f) {
out.push(Setting::open(f.clone(), "false"));
}
}
// ── THE OPT-OUTS, AT THE TOP OF THE INPUTS BLOCK ─────────────────────────
//
// Before the values, because they decide which values are asked for at all:
// untick DHCP and the static address appears; untick WireGuard and the hub
// declaration goes away. An operator who typed a hub.conf and then unticked
// WireGuard would otherwise be looking at a field whose value the door
// refuses BY NAME.
//
// These are NOT the row's `open` switches above. An `open` feature flips a
// cargo feature and nothing else; an option ALSO governs parameters and
// bakes a cmdline selector, and its default is the recipe's (ON, for an
// opt-out) rather than the `false` an open switch starts at.
for o in &cell.options {
out.push(Setting::option(o));
}
// ── THE SITE VALUES THIS CELL DECLARES ───────────────────────────────────
//
// In the declaration's own order, carrying its own label/kind/required. This is
// the whole of the pane's per-site input: the scene knows no product by name, so
// holger's four (site · hub_conf · holger_ron · underlay) and korp's two appear
// because those cells DECLARE them, and a product that declares none renders no
// inputs at all.
//
// The same `ParamSpec` list the CLI door reads for `--param`, so the pane and
// `dwarves buildthing --installer` cannot drift into asking for different things.
// The sharper SHAPE an option gives its conditional parameters. The rows
// themselves stay at a spelling every published loader knows (`kind =
// "text"`), because `ParamSpec` is deny_unknown_fields and `ParamKind` is a
// closed enum in the 0.1.7 reader a parity mirror loads through; the real
// shape (`ipv4`, `ipv4-cidr`) is declared inside the options table, which
// that reader skips whole. Applying it here is what makes the pane refuse
// exactly what the door refuses — `ResolvedOptions::effective_specs` does
// the same to the same map on the other side.
let sharpened: BTreeMap<&str, nornir_build_thing::params::ParamKind> = cell
.options
.iter()
.flat_map(|o| o.param_kinds.iter().map(|(n, k)| (n.as_str(), *k)))
.collect();
for p in &cell.params {
let p = match sharpened.get(p.name.as_str()) {
Some(k) => {
let mut p = p.clone();
p.kind = *k;
p
}
None => p.clone(),
};
let p = &p;
let mut s = Setting::param(p, cell.fixtures.get(&p.name).copied());
// Which toggle, if any, this parameter answers to. The rule is the
// recipe's — `params_when_on` / `params_when_off` — read once, HERE, so
// the pane cannot drift from the CLI door and the forge, which resolve
// the same declaration through `nornir_build_thing::options`.
s.governed = governing_option(&cell.options, &p.name);
out.push(s);
}
out
}
/// The option that governs `param`, and the state in which it applies.
///
/// `None` for a parameter no option names, which is every parameter of every
/// recipe written before the opt-out axis existed — those are always shown.
/// A parameter named by TWO options is refused at declaration time by
/// `nornir_build_thing::options::validate_options`, so the first hit is the
/// only hit.
fn governing_option(
options: &[nornir_build_thing::options::OptionSpec],
param: &str,
) -> Option<Governed> {
for o in options {
if o.params_when_on.iter().any(|p| p == param) {
return Some(Governed {
option: o.name.clone(),
shown_when: true,
});
}
if o.params_when_off.iter().any(|p| p == param) {
return Some(Governed {
option: o.name.clone(),
shown_when: false,
});
}
}
None
}
/// **What the operator typed into the cell's DECLARED parameter inputs**, as the
/// `name=value` argv `run_installer` already speaks.
///
/// Only the cell's own `[[build.*.params]]` names are read, in the declaration's order:
/// a settings map also carries the cargo axis (`kind`, `profile`, `target`, every open
/// feature), and forwarding those as parameters would offer a cell values it never
/// declared. An empty value is DROPPED rather than sent as `name=`, so a required
/// parameter left blank still produces the declaration's own refusal — naming the
/// parameter — instead of a validation error about an empty string.
///
/// Until 2026-08-16 this function did not exist and `Act::Build`'s `settings` were
/// discarded at the door: holger's ⚒ cell declares a REQUIRED `holger_ron`, the pane
/// rendered the input, and BYGG failed with `required parameter 'holger_ron' … was not
/// supplied` no matter what was typed into it.
fn declared_param_values(cell: &Cell, settings: &BTreeMap<String, String>) -> Vec<String> {
let toggles = declared_option_states(cell, settings);
cell.params
.iter()
.filter_map(|p| {
// A parameter the toggles REFUSE is not forwarded, whatever is left
// in the map. The operator may have typed a hub.conf and then
// unticked WireGuard; sending it would be refused BY NAME at the
// door, and the refusal would name a field that is no longer on the
// screen.
let g = governing_option(&cell.options, &p.name);
if let Some(g) = &g {
if toggles.get(&g.option).copied().unwrap_or(true) != g.shown_when {
return None;
}
}
let v = settings.get(&p.name)?.trim();
(!v.is_empty()).then(|| format!("{}={v}", p.name))
})
.collect()
}
/// The state of every opt-out this cell declares, as the operator left it —
/// falling back to the DECLARATION's default for a toggle nobody touched, never
/// to `false`. An opt-out defaulted to off would silently ship an appliance
/// stripped of capabilities nobody asked to remove.
fn declared_option_states(
cell: &Cell,
settings: &BTreeMap<String, String>,
) -> BTreeMap<String, bool> {
cell.options
.iter()
.map(|o| {
let on = match settings.get(&o.name).map(String::as_str) {
Some("true") => true,
Some("false") => false,
_ => o.default,
};
(o.name.clone(), on)
})
.collect()
}
/// **The toggles as the `--opt name=on|off` argv the door already speaks.**
///
/// EVERY declared option is sent, including the ones still at their default:
/// the door would resolve an omitted toggle to the same default, but sending it
/// makes the build RECORD say the operator was shown the choice. `from_default`
/// on the applied record keeps the two apart.
/// **The cargo features ⚒ sends** — the cargo variant's OPEN features whose box is
/// ticked in `settings` (declared-on ones are in the variant already). Empty for a
/// row that is no cargo variant.
pub fn declared_cargo_features(
repo: &RepoBuilds,
variant: &str,
settings: &BTreeMap<String, String>,
) -> nornir_build_thing::CargoFeatures {
let mut cf = nornir_build_thing::CargoFeatures::default();
if let Some(v) = repo.set.get(variant).filter(|v| v.kind.is_cargo()) {
cf.add_features(v.open.iter().filter(|f| settings.get(f.as_str()).is_some_and(|x| x == "true")));
}
cf
}
fn declared_option_values(cell: &Cell, settings: &BTreeMap<String, String>) -> Vec<String> {
declared_option_states(cell, settings)
.into_iter()
.map(|(name, on)| format!("{name}={}", if on { "on" } else { "off" }))
.collect()
}
/// The coordinate a checkout's recipe would carry.
///
/// `hash` stays `None` on purpose: it is the hash of the recipe that ACTUALLY RAN, and
/// nothing has run yet. A number here would be a number in the receipt that no rebuild
/// could ever match.
fn coordinate_of(repo: &str, root: &Path) -> Coordinate {
Coordinate {
krate: format!("{repo}{RECIPE_SUFFIX}"),
// `?` when the checkout declares no version: a virtual workspace with no
// `[workspace.package]` has none to read, and `0.0.0` there would be a number a
// rebuild could try to match. `?` cannot be mistaken for one.
version: manifest_version(root).unwrap_or_else(|| "?".to_string()),
hash: None,
}
}
/// The `version` a checkout's root manifest declares, from `[workspace.package]` or
/// `[package]` — the FIRST `version = "…"` inside either table.
///
/// A line scan rather than a TOML parse, because the only question is which of two
/// named tables a key sits in, and a dependency on a parser to answer it would be the
/// larger change. `None` when there is no manifest, no such table, or no key: never a
/// guess.
fn manifest_version(root: &Path) -> Option<String> {
let text = std::fs::read_to_string(root.join("Cargo.toml")).ok()?;
let mut inside = false;
for line in text.lines() {
let l = line.trim();
if l.starts_with('[') {
inside = matches!(l, "[workspace.package]" | "[package]");
continue;
}
if !inside {
continue;
}
if let Some(rest) = l.strip_prefix("version") {
let rest = rest.trim_start().strip_prefix('=')?.trim();
let v = rest.trim_matches(|c| c == '"' || c == '\'');
if !v.is_empty() && !v.starts_with('{') {
return Some(v.to_string());
}
}
}
None
}
/// The shipped ⚒ Build Thing scene, over the constellation this forge stands in.
#[must_use]
pub fn build_scene_pane() -> BuildScene {
BuildScene::new(Box::new(DeclaredForge::discover()))
}
#[cfg(test)]
mod tests {
use super::*;
/// The constellation on this disk, if there is one — the tests below are about
/// REAL declarations, so they skip rather than lie when there is nothing to read.
fn forge() -> Option<DeclaredForge> {
let f = DeclaredForge::discover();
(!f.builds.repos.is_empty()).then_some(f)
}
/// **RED-when-broken: every outcome the audit has a name for comes out of a REAL
/// file, and a MEDIUM is audited through the program it carries.**
///
/// `audit`'s own doc has named this test since the receipt work landed and the
/// test did not exist — a doc-level phantom guard, found on oden 2026-08-16 while
/// answering why `gunnar-installer.iso` reports `EJ MÄTT`. It is written now
/// because that question has an answer that must not rot: the ISO arm below is
/// the exact shape the real medium has, and the two-artifact arm is the exact
/// shape `assemble_cell::compose_installer_row` now produces.
#[test]
fn the_audit_reports_every_outcome_it_has_a_name_for() {
let dir = std::env::temp_dir().join(format!("dwarves-audit-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("a scratch dir for files this test writes itself");
// ── NOT MEASURED — a MEDIUM. ─────────────────────────────────────────────
// Not a hypothetical file: ISO-9660 puts `CD001` at 0x8001 and carries no
// object-file magic anywhere, which is why `gunnar-installer.iso` answers
// `Unknown file magic` and why no work on the audit will ever change that.
let iso = dir.join("medium.iso");
let mut image = vec![0u8; 0x8800];
image[0x8001..0x8006].copy_from_slice(b"CD001");
std::fs::write(&iso, &image).expect("write the medium");
let (_, medium) = audit(&iso);
assert_eq!(
medium.verdict,
ReceiptVerdict::NotMeasured,
"an ISO has no symbol table, so it can only be NOT MEASURED: {:?}",
medium.lines,
);
assert!(!medium.holds(), "and NOT MEASURED is never a pass");
// A path that is not there at all lands in the same non-passing outcome
// rather than in an early return that looks like silence.
let (_, absent) = audit(&dir.join("was-never-built"));
assert_eq!(absent.verdict, ReceiptVerdict::NotMeasured);
// ── the PROGRAM — a real object file this box built. ─────────────────────
// The running test binary: `object` parses it, it has a symbol table, and
// `build.rs` put a `.nordisk.buildthing` section in it. Whatever verdict that
// receipt earns, it is a MEASURED one — which is the whole difference.
let me = std::env::current_exe().expect("the running test binary");
let (_, program) = audit(&me);
assert_ne!(
program.verdict,
ReceiptVerdict::NotMeasured,
"the test binary must be readable as an object file: {:?}",
program.lines,
);
// ── the SUBJECT. ─────────────────────────────────────────────────────────
// A medium ALONE is all the audit has, and it still says so — this is the
// arm that keeps the change below from being a way of making an ISO pass.
let (_, alone) = audit_first_readable(&[iso.as_path()]).expect("one artifact, one answer");
assert_eq!(alone.verdict, ReceiptVerdict::NotMeasured);
// Listed BESIDE the program it carries, the answer is about the program.
let (_, carried) = audit_first_readable(&[iso.as_path(), me.as_path()])
.expect("two artifacts, one answer");
assert_eq!(
carried.verdict, program.verdict,
"an installer is audited through the appliance it lays down, not through \
the medium: {:?}",
carried.lines,
);
let subject = me
.file_name()
.expect("a file name")
.to_string_lossy()
.into_owned();
assert!(
carried.lines.first().is_some_and(|l| l.contains(&subject)),
"and the audit names the subject it actually read, so nobody reads the \
verdict as being about the ISO: {:?}",
carried.lines,
);
// Nothing produced, nothing claimed — never a verdict over an empty list.
assert!(audit_first_readable(&[]).is_none());
let _ = std::fs::remove_dir_all(&dir);
}
/// **RED-when-broken: an installer cell's DECLARED parameters become the pane's
/// inputs — label, kind and required-ness straight out of the declaration.**
///
/// This is the founding rule made testable. A hardcoded "WireGuard hub key" text
/// box would pass any test that only asked "is there an input"; what must hold is
/// that the inputs ARE the declaration, so a cell declaring different parameters
/// renders different fields and a cell declaring none renders nothing.
///
/// Driven through the private `settings_of`/`chained` against a synthetic set, so
/// it needs no checkout and no 64 GiB ISO.
/// **RED-when-broken: gunnar's REAL declaration must put BOTH delivery columns
/// on the row that declares them — the live ISO and the installer.**
///
/// Reads the actual `gunnar/.nornir/build-variants.toml` off this box rather
/// than a fixture, because the defect being guarded is a mismatch between what
/// the file says and what the matrix resolves. Skips by name (never silently)
/// when the checkout is absent, so it cannot go quietly green elsewhere.
#[test]
fn gunnars_installer_and_iso_are_both_reachable_columns() {
let root_buf = std::path::PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
.join("git/gunnar");
let root = root_buf.as_path();
if !root.join(".nornir/build-variants.toml").exists() {
eprintln!(
"SKIP: no gunnar checkout at {} — this guard needs the real file",
root.display()
);
return;
}
let set = nornir_build_thing::variant::BuildVariantSet::discover(root)
.expect("gunnar's declaration parses")
.expect("gunnar declares a build space");
// Every column any row declares — the same union pass 1 builds.
let mut seen: Vec<String> = Vec::new();
for v in &set.variants {
seen.push(v.kind.as_str().to_string());
for (label, _) in chained(&set, &v.name) {
seen.push(label);
}
}
seen.sort();
seen.dedup();
assert!(
seen.iter().any(|c| c == "iso:gunnar-appliance-iso"),
"the live-ISO column must exist; got {seen:?}"
);
assert!(
seen.iter().any(|c| c == "installer:gunnar-installer"),
"THE INSTALLER COLUMN MUST EXIST — an installer that is declared, listed by \
the CLI and has no column is a capability the operator cannot reach; got {seen:?}"
);
// And they must be DISTINCT columns: conflating them re-creates the exact
// ambiguity ArtifactKind::Installer was added to end.
let iso_row: Vec<&str> = set
.variants
.iter()
.filter(|v| {
chained(&set, &v.name)
.iter()
.any(|(l, _)| l == "iso:gunnar-appliance-iso")
})
.map(|v| v.name.as_str())
.collect();
let inst_row: Vec<&str> = set
.variants
.iter()
.filter(|v| {
chained(&set, &v.name)
.iter()
.any(|(l, _)| l == "installer:gunnar-installer")
})
.map(|v| v.name.as_str())
.collect();
assert!(!iso_row.is_empty(), "the live ISO must hang off some row");
assert!(!inst_row.is_empty(), "the installer must hang off some row");
eprintln!("live ISO hangs off {iso_row:?}; installer hangs off {inst_row:?}");
}
/// Prints the matrix EXACTLY as the pane resolves it for gunnar — every column
/// of every row with its declared/greyed state. A diagnostic, not a gate.
#[test]
fn print_gunnar_matrix() {
let root =
std::path::PathBuf::from(std::env::var_os("HOME").unwrap_or_default()).join("git");
if !root.join("gunnar/.nornir/build-variants.toml").exists() {
eprintln!("SKIP: no gunnar checkout");
return;
}
let mut f = DeclaredForge::at(root);
let menu = f.menu_for("gunnar", "probe".to_string());
for row in &menu.rows {
let lit: Vec<&str> = row
.targets
.iter()
.filter(|t| t.declared)
.map(|t| t.name.as_str())
.collect();
eprintln!("ROW {:<24} LIT: {:?}", row.name, lit);
}
let all: Vec<&str> = menu.rows[0]
.targets
.iter()
.map(|t| t.name.as_str())
.collect();
eprintln!("ALL COLUMNS ({}): {:?}", all.len(), all);
}
#[test]
fn a_cells_declared_params_become_the_panes_inputs() {
use nornir_build_thing::params::{ParamKind, ParamSpec};
let spec = |name: &str, label: &str, kind, required| ParamSpec {
name: name.into(),
label: label.into(),
kind,
required,
boot_param: format!("korp.{name}"),
staged_file: String::new(),
..Default::default()
};
let cell = Cell {
name: "korp-installer".into(),
kind: ArtifactKind::Installer,
params: vec![
spec("site", "Site name", ParamKind::Text, false),
spec("hub_pubkey", "Hub public key", ParamKind::Base64Key, true),
],
fixtures: BTreeMap::new(),
options: vec![],
};
let v = BuildVariant {
name: "korp-server".into(),
..Default::default()
};
let out = settings_of(&v, &cell);
let site = out
.iter()
.find(|s| s.key == "site")
.expect("the declared `site` renders");
let hub = out
.iter()
.find(|s| s.key == "hub_pubkey")
.expect("the declared `hub_pubkey` renders");
// The DECLARATION's words, not the UI's.
let d = site
.declared
.as_ref()
.expect("a declared param carries its shape");
assert_eq!(d.label, "Site name");
assert!(!d.required);
let d = hub
.declared
.as_ref()
.expect("a declared param carries its shape");
assert_eq!(d.label, "Hub public key");
assert!(
d.required,
"the declaration said required, so the pane must refuse without it"
);
assert_eq!(d.kind, ParamKind::Base64Key);
// Open by construction — a site value the operator types.
assert!(
!site.fixed && !hub.fixed,
"declared params are the operator's to fill"
);
assert!(
hub.value.is_empty(),
"and start empty, never pre-filled with a guess"
);
// A cell that declares NOTHING renders no inputs at all.
let bare = Cell {
name: "korp-server".into(),
kind: ArtifactKind::Exe,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
};
let none = settings_of(&v, &bare);
assert!(
none.iter().all(|s| s.declared.is_none()),
"a product declaring no parameters renders no inputs: {none:?}",
);
}
/// **The fixture affordance is DATA-DRIVEN**: a param whose cell declares a
/// fixture for it carries the fixture's label into the scene; one that does
/// not carries `None` — and `@fixture` typed there is refused by label (the
/// red). A staged text file renders multi-line; a cmdline token does not.
#[test]
fn a_declared_fixture_reaches_the_panes_setting_and_only_there() {
use nornir_build_thing::params::{FixtureKind, ParamKind, ParamSpec, FIXTURE_VALUE};
let hub_conf = ParamSpec {
name: "hub_conf".into(),
label: "Hub declaration".into(),
kind: ParamKind::Text,
required: false,
boot_param: String::new(),
staged_file: "hub.conf".into(),
..Default::default()
};
let site = ParamSpec {
name: "site".into(),
label: "Site name".into(),
kind: ParamKind::Text,
required: false,
boot_param: "korp.site".into(),
staged_file: String::new(),
..Default::default()
};
let cell = Cell {
name: "x-installer".into(),
kind: ArtifactKind::Installer,
params: vec![site, hub_conf],
fixtures: BTreeMap::from([("hub_conf".to_string(), FixtureKind::WgHub)]),
options: vec![],
};
let v = BuildVariant {
name: "x-server".into(),
..Default::default()
};
let out = settings_of(&v, &cell);
let hub = out
.iter()
.find(|s| s.key == "hub_conf")
.unwrap()
.declared
.clone()
.unwrap();
let site = out
.iter()
.find(|s| s.key == "site")
.unwrap()
.declared
.clone()
.unwrap();
assert_eq!(
hub.fixture.as_deref(),
Some("wg-hub"),
"the declared fixture, by its label"
);
assert!(hub.multiline, "a staged text file is a multi-line value");
assert_eq!(site.fixture, None, "no fixture was declared for site");
assert!(!site.multiline, "a cmdline token is one line");
hub.check(FIXTURE_VALUE)
.expect("@fixture is legal where a fixture is declared");
let red = site.check(FIXTURE_VALUE).unwrap_err();
assert!(
red.contains("ingen fixtur") && red.contains("Site name"),
"{red}"
);
// And the argv the build is given carries the same word the CLI speaks.
let chosen = BTreeMap::from([
("hub_conf".to_string(), FIXTURE_VALUE.to_string()),
("site".to_string(), "stockholm-7".to_string()),
]);
assert_eq!(
declared_param_values(&cell, &chosen),
vec![
"site=stockholm-7".to_string(),
format!("hub_conf={FIXTURE_VALUE}")
]
);
}
/// **RED-when-broken: the declared KIND is what validates, and the refusal names
/// the field.** A `Base64Key` that accepted any text would let a mistyped hub key
/// reach a 64 GiB ISO build and fail at boot instead of at the keystroke.
#[test]
fn a_declared_kind_validates_and_refuses_by_label() {
use crate::build_scene::DeclaredParam;
use nornir_build_thing::params::ParamKind;
let key = DeclaredParam {
label: "Hub public key".into(),
kind: ParamKind::Base64Key,
required: true,
fixture: None,
multiline: false,
};
assert!(key
.check("HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=")
.is_ok());
for bad in [
"not-a-key",
"HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw",
"",
" ",
] {
let err = key.check(bad).expect_err(bad);
assert!(
err.contains("Hub public key"),
"the refusal names the field: {err}"
);
}
// Empty is "required", never "malformed" — different sentences for different faults.
assert_eq!(key.check("").unwrap_err(), "Hub public key is required");
let opt = DeclaredParam {
label: "Site".into(),
kind: ParamKind::Text,
required: false,
fixture: None,
multiline: false,
};
assert!(opt.check("").is_ok(), "an optional blank is fine");
let ep = DeclaredParam {
label: "Endpoint".into(),
kind: ParamKind::Endpoint,
required: false,
fixture: None,
multiline: false,
};
assert!(ep.check("192.0.2.7:51820").is_ok());
assert!(ep.check("192.0.2.7").is_err());
let cidr = DeclaredParam {
label: "Allowed".into(),
kind: ParamKind::CidrList,
required: false,
fixture: None,
multiline: false,
};
assert!(cidr.check("10.77.0.0/16, 10.99.0.1/32").is_ok());
assert!(cidr.check("10.77.0.0/33").is_err());
assert!(cidr.check("10.77.0.0").is_err());
}
/// **A KERNEL COLUMN WITH NO KERNEL RECIPE RENDERED A BLANK AND SAID NOTHING.**
///
/// A kernel is TWO declarations that must agree by name: a `[[build.kernel]]` row
/// carrying preset/version/profile, and a `[[build.variant]] kind = "kernel"` row
/// that puts a column in the matrix. The variant is what makes the column appear, so
/// misspell the pairing and the operator gets a column headed with the kernel's name
/// and NOTHING under it — indistinguishable from a kernel that declares no settings,
/// and from a pane that failed to load.
///
/// That is not a hypothetical typo: `tunnr` is the only repo in the constellation
/// that declares a kernel at all, so this pairing has exactly one live example and
/// nothing has ever exercised getting it wrong.
///
/// RED WHEN REVERTED: put the bare `return Vec::new();` back and the assertion below
/// gets an empty vec.
#[test]
fn a_kernel_column_whose_recipe_is_missing_names_the_miss() {
let dir = tempfile::tempdir().expect("tmp");
// The variant declares a kernel column called `orten-kernel`; the recipe below
// is called `orten-krnel`. One transposition, and they no longer meet.
std::fs::write(
dir.path().join("build-variants.toml"),
r#"
[build]
[[build.kernel]]
name = "orten-krnel"
version = "6.12.104"
preset = "nuked"
[[build.variant]]
name = "orten-kernel"
suites = ["ORIGINAL"]
member = "orten"
package = "orten"
kind = "kernel"
linkage = "not-applicable"
"#,
)
.expect("write");
let set = nornir_build_thing::variant::BuildVariantSet::load_file(
&dir.path().join("build-variants.toml"),
)
.expect("load");
let cell = Cell {
name: "orten-kernel".into(),
kind: ArtifactKind::Kernel,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
};
let out = kernel_settings(&set, &cell);
assert!(
!out.is_empty(),
"a kernel column whose recipe is missing rendered NOTHING — the operator sees \
a column named `orten-kernel` with no settings and no reason, which reads as \
'this kernel has nothing to configure' rather than 'the recipe is misspelt'"
);
let text = format!("{out:?}");
assert!(
text.contains("orten-kernel"),
"the miss must name the column that is missing its recipe: {text}"
);
assert!(
text.contains("orten-krnel"),
"and it must list what IS declared — 'not found' without the real names sends \
the reader off to grep for a string they cannot spell: {text}"
);
}
/// **THE GUARD ON THE GUARD.** A kernel column whose recipe IS present must render
/// the recipe, not the refusal — otherwise the test above is satisfied by a function
/// that always complains.
#[test]
fn a_kernel_column_with_its_recipe_renders_the_recipe() {
let dir = tempfile::tempdir().expect("tmp");
std::fs::write(
dir.path().join("build-variants.toml"),
r#"
[build]
[[build.kernel]]
name = "orten-kernel"
version = "6.12.104"
preset = "nuked"
[[build.variant]]
name = "orten-kernel"
suites = ["ORIGINAL"]
member = "orten"
package = "orten"
kind = "kernel"
linkage = "not-applicable"
"#,
)
.expect("write");
let set = nornir_build_thing::variant::BuildVariantSet::load_file(
&dir.path().join("build-variants.toml"),
)
.expect("load");
let cell = Cell {
name: "orten-kernel".into(),
kind: ArtifactKind::Kernel,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
};
let text = format!("{:?}", kernel_settings(&set, &cell));
assert!(
text.contains("6.12.104"),
"the declared version reaches the pane: {text}"
);
assert!(text.contains("nuked"), "and the declared preset: {text}");
assert!(
!text.contains("INGEN DEKLARATION"),
"a recipe that IS declared must not be reported as missing: {text}"
);
}
/// **The donor-consuming installer is VISIBLE, and both kernel leaves name the
/// donor.** RED-FIRST 2026-08-26, the acceptance question in user terms:
/// *"I search gunnar in ⚒ — do I get a buildthing with a KERNEL choice?"*
/// Before this, the answer was a bare refusal ("`gunnar` deklarerar ingen
/// [[build.kernel]]") and an installer settings block that said dhcp/wireguard
/// and NOTHING about the one thing the medium boots. Now the cell's settings
/// carry the donor as a fact row, and both the repo-level browse and the
/// cell-level leaf answer with WHERE the choice is made instead of "no".
#[test]
fn a_donor_consuming_installer_shows_its_kernel_and_both_leaves_name_the_donor() {
let dir = tempfile::tempdir().expect("tmp");
let root = dir.path().join("consumer");
std::fs::create_dir_all(root.join(".nornir")).expect("mkdir");
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"consumer\"\nversion = \"1.0.0\"\nedition = \"2021\"\n",
)
.expect("manifest");
std::fs::write(
root.join(".nornir").join("build-variants.toml"),
r#"
[build]
[[build.variant]]
name = "c-installer"
suites = ["ORIGINAL"]
kind = "installer"
role = "server"
default = false
[[build.installer]]
name = "c-installer"
variant = "c-app"
[build.installer.kernel_donor]
repo = "tunnr"
kernel = "tunnr-kernel"
[[build.variant]]
name = "c-app"
suites = ["ORIGINAL"]
kind = "exe"
role = "server"
default = true
"#,
)
.expect("declaration");
let mut forge = DeclaredForge::at(dir.path().to_path_buf());
assert!(
forge.builds.repos.iter().any(|r| r.repo == "consumer"),
"the fixture repo must be discovered: {:?}",
forge.declared_repos()
);
forge.repo = Some("consumer".to_string());
// 1. THE FACT ROW: the installer cell's settings say which kernel it boots.
let set = forge.current().expect("picked").set.clone();
let cell = Cell {
name: "c-installer".into(),
kind: ArtifactKind::Installer,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
};
let rows = donor_settings(&set, &cell);
assert_eq!(rows.len(), 1, "one donor row for a donor-declaring cell");
// …and a cell with no donor renders NO row — the fact never appears undeclared.
let plain = Cell {
name: "not-declared".into(),
..cell.clone()
};
assert!(donor_settings(&set, &plain).is_empty());
// 2. THE REPO-LEVEL LEAF (browse): the refusal names the donor, not just "no".
match forge.browse() {
Leaf::Refused(t) => {
assert!(t.contains("tunnr/tunnr-kernel"), "{t}");
assert!(
t.contains("c-installer"),
"the consuming cell is named: {t}"
);
}
_ => panic!("a kernel-less repo answers Refused from browse()"),
}
// 3. THE CELL-LEVEL LEAF: same donor, sharper sentence.
forge.cells.insert(key("c-installer", "installer"), cell);
match forge.leaf("c-installer", "installer", &BTreeMap::new()) {
Leaf::Refused(t) => {
assert!(t.contains("tunnr/tunnr-kernel"), "{t}");
assert!(
t.contains("pin"),
"the pin verification is what BYGG buys: {t}"
);
}
_ => panic!("an installer cell answers Refused from leaf()"),
}
}
/// A staged checkout with ONE `[[build.kernel]]`, read the way the forge reads it.
/// `RepoBuilds::read` (not a hand-built struct) because the subject of the two
/// tests below is the declaration the BUILD WORKER receives, and the worker
/// receives `repo.set`.
fn staged_kernel_repo(preset: &str) -> (tempfile::TempDir, RepoBuilds) {
let dir = tempfile::tempdir().expect("tmp");
let root = dir.path().join("orten");
std::fs::create_dir_all(root.join(".nornir")).expect("mkdir");
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"orten\"\nversion = \"1.0.0\"\nedition = \"2021\"\n",
)
.expect("manifest");
std::fs::write(
root.join(".nornir").join("build-variants.toml"),
format!(
r#"
[build]
[[build.kernel]]
name = "orten-kernel"
version = "6.12.104"
tarball_sha256 = "949d166a1263bd0cf0c5aab44eeb57869615099e7c48bf88d0e4adb77379b999"
preset = "{preset}"
[[build.variant]]
name = "orten-kernel"
suites = ["ORIGINAL"]
member = "orten"
package = "orten"
kind = "kernel"
linkage = "not-applicable"
"#
),
)
.expect("declaration");
let repo = RepoBuilds::read("orten", &root);
assert!(repo.declared, "the fixture must parse: {:?}", repo.error);
(dir, repo)
}
fn kernel_cell() -> Cell {
Cell {
name: "orten-kernel".into(),
kind: ArtifactKind::Kernel,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
}
}
/// **A PRESET PICKED IN ⚒ CHANGES WHAT ⚒ WOULD BUILD.** This is the claim the
/// whole move rests on, measured rather than asserted.
///
/// It is measured on the DECLARATION the build worker receives, because that is
/// where a kernel's contents are decided: `BuildRun::spawn_with_selection` clones
/// `repo.set` and hands the clone to the assembler, which resolves the
/// `[[build.kernel]]` row by name. So the honest question is not "did a string
/// reach a UI field" but "is the declaration the worker gets a different kernel",
/// and the kind answers that itself — `KernelBuild::content_id` is the identity a
/// rebuild has to match, and `base()` is the make target the preset resolves to.
///
/// RED BEFORE GREEN: with `preset` still `Setting::fixed` and no `kernel_override`,
/// the operator's pick was discarded at the door and both halves below are the
/// declared `nuked` — the picker was a knob that turned nothing.
#[test]
fn a_preset_picked_in_the_forge_is_the_preset_the_build_runs() {
let (_dir, repo) = staged_kernel_repo("nuked");
let cell = kernel_cell();
let declared = repo.set.kernels[0].clone();
assert_eq!(declared.preset, "nuked");
assert_eq!(
declared.base(),
"tinyconfig",
"the declared preset's own base"
);
// THE PICK, exactly as `Act::Build` carries it off the settings block.
let mut chose = BTreeMap::new();
chose.insert(PRESET_KEY.to_string(), "full".to_string());
let (built, line) = kernel_override(&repo, &cell, &chose)
.expect("a pick that differs from the declaration must reach the build");
let after = &built.set.kernels[0];
assert_eq!(
after.preset, "full",
"the WORKER's declaration carries the pick"
);
assert_eq!(
after.base(),
"defconfig",
"and the derived base follows it — `full` starts from defconfig, `nuked` from \
tinyconfig, which is the difference between the two machines"
);
assert_ne!(
after.content_id(),
declared.content_id(),
"the kind's OWN identity for this build must differ; an override that produced \
the same content id would be a rebuild of the same kernel"
);
assert!(
line.contains("nuked") && line.contains("full"),
"the log names both: {line}"
);
// …and the declaration ON DISK is untouched. The override is this run's, not
// an edit to the repo — a pane that rewrote a committed declaration to build
// something once would be a far worse bug than the one it fixed.
assert_eq!(
repo.set.kernels[0].preset, "nuked",
"the source declaration is not edited"
);
}
/// **THE GUARD ON THE GUARD.** The override must be silent about everything it is
/// not: a pick equal to the declaration, a word that is not a preset, and any cell
/// that is not a kernel. Without these, the test above is satisfied by a function
/// that clones the repo unconditionally and calls that a change.
#[test]
fn the_override_stays_out_of_the_way_of_everything_it_does_not_decide() {
let (_dir, repo) = staged_kernel_repo("nuked");
let cell = kernel_cell();
assert!(
kernel_override(&repo, &cell, &BTreeMap::new()).is_none(),
"nothing picked ⇒ the declaration stands"
);
let same: BTreeMap<String, String> = [(PRESET_KEY.to_string(), "nuked".to_string())]
.into_iter()
.collect();
assert!(
kernel_override(&repo, &cell, &same).is_none(),
"a pick equal to the declaration is not an override, and must not be logged as one"
);
let junk: BTreeMap<String, String> = [(PRESET_KEY.to_string(), "slimm".to_string())]
.into_iter()
.collect();
assert!(
kernel_override(&repo, &cell, &junk).is_none(),
"an unknown preset is refused HERE, by the same `Preset::parse` gate the kind's \
own validate uses — forwarding it would fail deep inside a toolchain container"
);
let exe = Cell {
kind: ArtifactKind::Exe,
..kernel_cell()
};
let pick: BTreeMap<String, String> = [(PRESET_KEY.to_string(), "full".to_string())]
.into_iter()
.collect();
assert!(
kernel_override(&repo, &exe, &pick).is_none(),
"a cargo cell has no kernel declaration to override, whatever is in the map"
);
}
/// **The picker offers every preset the kind has, and no others.**
///
/// The list is the kind's own [`crate::kernel_scene::PRESETS`], not a list spelled
/// here — a picker with its own copy of the vocabulary is a picker that offers a
/// word the door refuses the day the kind grows a fifth preset.
#[test]
fn the_preset_setting_is_a_control_with_the_kinds_whole_vocabulary() {
let (_dir, repo) = staged_kernel_repo("nuked");
let out = kernel_settings(&repo.set, &kernel_cell());
let preset = out
.iter()
.find(|s| s.key == PRESET_KEY)
.expect("the kernel cell resolves a `preset` setting");
assert!(
!preset.fixed,
"the preset was `Setting::fixed` — a label. It is the choice the tab exists to \
offer, and a read-only one is why the configurator had to be looked for in \
another tab three times"
);
assert_eq!(
preset.value, "nuked",
"seeded from the declaration, not from a default"
);
let words: Vec<&str> = preset.choices.iter().map(|c| c.value.as_str()).collect();
assert_eq!(
words,
PRESETS
.iter()
.map(nornir_build_thing::kernel::Preset::as_str)
.collect::<Vec<_>>(),
"the vocabulary is the kind's"
);
// Every choice carries the base it resolves to — the consequence beside the
// pick, so a `base` row that would freeze at the declared preset is not needed.
assert!(
preset.choices.iter().all(|c| c.note.contains("bas ")),
"each choice must say what it resolves to: {:?}",
preset.choices
);
assert!(
!out.iter().any(|s| s.key == "base"),
"with no pinned `base` in the declaration there must be no `base` ROW — it would \
read as the declared preset's base forever while the operator picked another"
);
}
/// **The dependency picture's SUBJECT: an installer column is about the
/// appliance it packs, not about itself.**
///
/// An installer variant declares no cargo features — the thing it builds is
/// the appliance. Derived from its own row, the picture settled on
/// `bygger utan features` and drew nothing, while the app row beside it drew
/// fine; that intermittency is what read as "the picture sometimes flickers
/// in". Both arms here, because a mapping that returned `None` for
/// everything would look identical on the plain-exe arm.
///
/// ⚠ THIS TEST DID NOT RUN. Its doc block and its `#[test]` were separated from
/// it by a whole other test that landed in between (the doc comment above went in
/// UNDER the attribute), so the attribute attached to the kernel test — which then
/// carried two of them — and this function was a plain private fn inside
/// `mod tests` that nothing ever called. Found 2026-08-26 while moving the kernel
/// configurator into ⚒; the attribute is back where it belongs.
#[test]
fn a_delivery_column_pictures_the_variant_it_packs() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("build-variants.toml"),
r#"
[[build.variant]]
name = "orten-server"
package = "orten"
bin = "orten"
kind = "exe"
features = ["server"]
[[build.installer]]
name = "orten-installer"
variant = "orten-server"
[[build.iso]]
name = "orten-live"
from_variant = "orten-server"
staging = "iso-root"
"#,
)
.expect("write");
let set = nornir_build_thing::variant::BuildVariantSet::load_file(
&dir.path().join("build-variants.toml"),
)
.expect("load");
let forge = DeclaredForge::at(dir.path().to_path_buf());
let installer = Cell {
name: "orten-installer".into(),
kind: ArtifactKind::Installer,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
};
assert_eq!(
forge.packed_variant(&set, &installer),
Some("orten-server".to_string()),
"an installer column does not know which appliance it packs, so its picture is derived from a row that declares no features and draws nothing"
);
let iso = Cell {
name: "orten-live".into(),
kind: ArtifactKind::Iso,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
};
assert_eq!(
forge.packed_variant(&set, &iso),
Some("orten-server".to_string())
);
// The arm that keeps the two above honest: a plain cargo column packs
// nothing, and its picture is of its own row.
let exe = Cell {
name: "orten-server".into(),
kind: ArtifactKind::Exe,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
};
assert_eq!(
forge.packed_variant(&set, &exe),
None,
"a cargo column claimed to pack something, so every picture would be redirected"
);
}
/// **RED-when-broken: an installer column is `Installer`, never `Iso`.**
///
/// edda added the kind precisely because "until this variant existed the
/// attestation spelled both `iso`, so an auditor could not tell them apart" — a
/// live ISO IS the appliance, an installer ISO PRODUCES one.
#[test]
fn an_installer_column_is_not_spelled_iso() {
let cell = Cell {
name: "korp-installer".into(),
kind: ArtifactKind::Installer,
params: vec![],
fixtures: BTreeMap::new(),
options: vec![],
};
assert_ne!(
cell.kind,
ArtifactKind::Iso,
"the two kinds must stay distinguishable"
);
assert_eq!(
settings_of(
&BuildVariant {
name: "x".into(),
..Default::default()
},
&cell
)
.iter()
.find(|s| s.key == "kind")
.map(|s| s.value.as_str()),
Some(ArtifactKind::Installer.as_str()),
"and the pane's own `kind` setting says so",
);
}
/// Every declared checkout resolves to a matrix, and the matrix is the file.
///
/// Watched red: `assert!(rows > 0)` alone would pass on a resolver that returned
/// one row for everything, so the row NAMES are compared against the declaration
/// itself, in order.
#[test]
fn every_declared_checkout_renders_its_own_matrix_in_declaration_order() {
let Some(mut f) = forge() else { return };
let names: Vec<String> = f.builds.repos.iter().map(|r| r.repo.clone()).collect();
let mut checked = 0;
for repo in names {
let declared: Vec<String> = f
.builds
.repo(&repo)
.map(|r| r.set.variants.iter().map(|v| v.name.clone()).collect())
.unwrap_or_default();
if declared.is_empty() {
continue;
}
let menu = f
.resolve(&Query::Artifact(repo.clone()))
.expect("a declared repo");
let rows: Vec<String> = menu.rows.iter().map(|r| r.name.clone()).collect();
assert_eq!(
rows, declared,
"{repo}: the scene reordered or dropped rows the declaration lists"
);
assert_eq!(menu.coordinate.krate, format!("{repo}{RECIPE_SUFFIX}"));
assert!(
menu.coordinate.hash.is_none(),
"{repo}: a hash was invented"
);
checked += 1;
}
assert!(checked > 0, "no checkout on this disk declares a variant");
}
/// Suite badges are copied, not computed.
///
/// Red probe: the badge list is compared to `suites` as a PREFIX, so a resolver
/// that dropped, sorted or invented a suite fails — including one that helpfully
/// added the base suite to a row that does not declare it.
#[test]
fn a_rows_suite_badges_are_exactly_what_the_declaration_says() {
let Some(mut f) = forge() else { return };
let names: Vec<String> = f.builds.repos.iter().map(|r| r.repo.clone()).collect();
let mut seen_any = 0;
for repo in names {
let declared: Vec<(String, Vec<String>)> = f
.builds
.repo(&repo)
.map(|r| {
r.set
.variants
.iter()
.map(|v| (v.name.clone(), v.suites.clone()))
.collect()
})
.unwrap_or_default();
if declared.is_empty() {
continue;
}
let menu = f
.resolve(&Query::Artifact(repo.clone()))
.expect("a declared repo");
for (row, suites) in declared {
let badges = &menu
.rows
.iter()
.find(|r| r.name == row)
.unwrap_or_else(|| panic!("{repo}: row {row} vanished"))
.badges;
assert!(
badges.starts_with(&suites),
"{repo}/{row}: badges {badges:?} are not the declared suites {suites:?}"
);
seen_any += usize::from(!suites.is_empty());
}
}
assert!(seen_any > 0, "no checkout on this disk declares a suite");
}
/// A column exists exactly when a declaration produced it — and a row that does
/// not declare one carries it greyed rather than dropped.
#[test]
fn the_column_strip_is_the_same_on_every_row_and_only_some_cells_are_declared() {
let Some(mut f) = forge() else { return };
let names: Vec<String> = f.builds.repos.iter().map(|r| r.repo.clone()).collect();
let mut wide = 0;
for repo in names {
let Ok(menu) = f.resolve(&Query::Artifact(repo.clone())) else {
continue;
};
let Some(first) = menu.rows.first() else {
continue;
};
let strip: Vec<&str> = first.targets.iter().map(|t| t.name.as_str()).collect();
for r in &menu.rows {
let mine: Vec<&str> = r.targets.iter().map(|t| t.name.as_str()).collect();
assert_eq!(
mine, strip,
"{repo}/{}: a row has its own column strip",
r.name
);
assert!(
r.targets.iter().any(|t| t.declared),
"{repo}/{}: every column greyed — the row builds nothing",
r.name
);
}
if strip.len() > 1 {
wide += 1;
// RED — a strip where EVERY cell is declared would mean the greying is
// never exercised. At least one repo must have a genuinely sparse row.
let sparse = menu
.rows
.iter()
.any(|r| r.targets.iter().any(|t| !t.declared));
assert!(
sparse,
"{repo}: a multi-column matrix with no undeclared cell"
);
}
}
assert!(
wide > 0,
"no checkout on this disk declares more than one column"
);
}
/// A name two recipes both declare is refused, with both named. Silently picking
/// one is how a build comes out of the wrong checkout.
#[test]
fn an_ambiguous_name_is_refused_and_names_the_candidates() {
let Some(mut f) = forge() else { return };
// Find a name more than one checkout declares, if the disk has one.
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for r in &f.builds.repos {
for v in &r.set.variants {
*counts.entry(v.name.clone()).or_default() += 1;
}
}
let Some((name, _)) = counts.iter().find(|(_, n)| **n > 1) else {
return;
};
let name = name.clone();
let err = f
.resolve(&Query::Artifact(name.clone()))
.expect_err("ambiguous");
assert!(
err.contains(&name),
"the refusal does not name what was asked: {err}"
);
assert!(
err.contains(','),
"the refusal does not list the candidates: {err}"
);
}
/// A coordinate addresses the same matrix the name does — the second door onto one
/// state, proven against a real declaration rather than a fixture.
#[test]
fn a_coordinate_reaches_the_same_matrix_the_name_does() {
let Some(mut f) = forge() else { return };
let Some(repo) = f
.builds
.repos
.iter()
.find(|r| !r.set.variants.is_empty())
.map(|r| r.repo.clone())
else {
return;
};
let by_name = f.resolve(&Query::Artifact(repo.clone())).expect("by name");
let coord = Coordinate {
krate: format!("{repo}{RECIPE_SUFFIX}"),
version: by_name.coordinate.version.clone(),
hash: None,
};
let by_coord = f.resolve(&Query::At(coord)).expect("by coordinate");
assert_eq!(
by_name.rows, by_coord.rows,
"the two doors disagree about {repo}"
);
assert_ne!(by_name.how, by_coord.how, "the doors are indistinguishable");
// RED — a coordinate naming no checkout is refused, not defaulted.
let bad = Coordinate {
krate: "ingen-sadan-utcheckning".to_string(),
version: "0.1.0".to_string(),
hash: None,
};
assert!(
f.resolve(&Query::At(bad)).is_err(),
"an unknown coordinate resolved"
);
}
}
#[cfg(test)]
mod engine_tests {
use super::*;
/// **The RAID installer engine is still reachable.**
///
/// It was wired into the retired pane's constructor and would have been lost with it;
/// unreachable is worse than unrecorded, because no operator could ask for it at all.
///
/// Watched red in both directions: the default must NOT be the RAID engine and the
/// hook must NOT be the default, so a selector stuck on either answer fails one of
/// the two.
///
/// **This test was BLIND until 2026-08-23** and this note is the reason it is written
/// the way it is. It asserted a `boot_engine_name` that held a SECOND copy of the
/// selector match; renaming only the launcher's arm to `Some("edda_raid")` made
/// `DWARVES_INSTALLER_ENGINE=edda-raid` fall through to the default launcher — the
/// exact "unreachable from the UI" defect above — and the test still reported `ok`.
/// It now reads the name out of the SAME call that builds the launcher
/// ([`boot_engine`]), so the string under assertion is the arm that ran. That
/// sabotage is now RED, measured.
#[test]
fn the_installer_engine_hook_is_opt_in_and_the_default_is_untouched() {
// The name asserted is the one the picked arm handed back WITH its launcher —
// never a mirror computed somewhere else.
let name = |e: Option<&str>| boot_engine(e).1;
assert_eq!(name(None), "tunnr", "the default engine changed silently");
assert_eq!(name(Some("")), "tunnr");
assert_eq!(name(Some("something-else")), "tunnr");
assert_eq!(
name(Some("edda-raid")),
"edda-raid",
"the RAID engine cannot be asked for, so it is unreachable from the UI"
);
// RED — the two answers must differ, or neither assertion above discriminates.
assert_ne!(name(None), name(Some("edda-raid")));
// …and the SCENE really carries the arm's answer: the field the pane reports is
// fed by the same call, so a selector that stopped reaching the launcher shows up
// here too rather than only in a helper nobody dispatches through.
let scene = DeclaredForge::at(std::env::temp_dir().join("dwarves-engine-probe"));
assert_eq!(
scene.boot_engine(),
boot_engine(std::env::var("DWARVES_INSTALLER_ENGINE").ok().as_deref()).1,
"the pane reports an engine the constructor did not pick"
);
}
}