xtask/src/build_flow_svg.rs
//! `cargo xtask build-flow-svg` — the `Build{variant}` flow drawn from a **data
//! model of audited steps**, so the picture cannot claim a step is
//! declaration-driven when the code decides it.
//!
//! # Why a generator and not a drawing
//!
//! The whole point of this diagram is one distinction — which steps the
//! DECLARATION orders (variant → features → settings/sub-settings → typ) and
//! which are fixed in Rust. A hand-drawn picture of that distinction rots
//! silently: the boxes keep rendering after the code moves. So every node below
//! is a `const` carrying an [`Origin`] and, when it is hard-coded, the
//! `file:line` where that was read on 2026-09-10. There is no way to draw a step
//! as declaration-driven without changing the datum.
//!
//! `xtask/tests/build_flow_svg.rs` asserts the two verdicts actually render
//! differently — a legend that says "coral means hard-coded" while both halves
//! render cyan is exactly the silent green this constellation keeps finding.
//!
//! # Style
//!
//! The `skade` circuit-board house style (`../skade/.nornir/assets/*.svg`):
//! `#0b1c2e`→`#06111d` board gradient, `chip`/`rail` gradients, DejaVu Sans,
//! cyan `#6fd3f5` for the declared signal path, coral `#f0a9a0` for the
//! hard-coded half. Those files are hand-written; this one is not, which is the
//! only intentional departure — the same departure `gunnar/xtask/src/cycle_svg.rs`
//! makes, and this file is that file's idiom applied to the build flow.
//!
//! Rendered to `.nornir/assets/build-flow.svg`; `std` only.
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
/// Where a step's decision comes from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Origin {
/// The DECLARATION orders it: the variant's `kind`, its features, its
/// settings tree or the BUILT-edge DAG decides, and a recipe change changes
/// the build with no code change.
Declared,
/// FIXED IN RUST: the order, the arm or the path is a literal. `at` is where
/// it was read.
HardCoded { at: Cite },
/// Typed, transported over the wasm ABI — and never executed. The plan half
/// of "the build thing returns a plan and the forge executes it" exists;
/// the execute half does not.
TypedNeverRun { at: Cite },
}
/// A repo-relative citation, so the claim can be OPENED rather than merely read.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cite {
/// Repo-relative path, `edda:` prefixed when it is the sibling checkout.
pub file: &'static str,
/// The line the claim rests on, as read 2026-09-10.
pub line: u32,
}
impl Cite {
const fn new(file: &'static str, line: u32) -> Self {
Cite { file, line }
}
fn render(&self) -> String {
format!("{}:{}", self.file, self.line)
}
}
/// One box on the flow.
#[derive(Debug, Clone, Copy)]
pub struct Step {
/// The short name on the box.
pub name: &'static str,
/// One line of what it decides.
pub what: &'static str,
/// Which lane (column) it sits in: 0 = the doors, 1 = the forge, 2 = the
/// evaluator, 3 = the isolation layer.
pub lane: u8,
/// Row within the lane.
pub row: u8,
/// Where the decision comes from.
pub origin: Origin,
}
const fn declared(name: &'static str, what: &'static str, lane: u8, row: u8) -> Step {
Step {
name,
what,
lane,
row,
origin: Origin::Declared,
}
}
const fn fixed(
name: &'static str,
what: &'static str,
lane: u8,
row: u8,
file: &'static str,
line: u32,
) -> Step {
Step {
name,
what,
lane,
row,
origin: Origin::HardCoded {
at: Cite::new(file, line),
},
}
}
/// **The audited flow**, 2026-09-10. Three doors, one funnel, one dispatch
/// chain, one isolation layer.
pub const STEPS: &[Step] = &[
// ── lane 0: the three doors ─────────────────────────────────────────────
declared("CLI", "dwarves buildthing <p> --variant V", 0, 0),
declared("rpc", "dwarf.v1.BuildThing (the --server door)", 0, 1),
declared("⚒ pane", "Act::Build{row,target,settings}", 0, 2),
// ── lane 1: the forge ───────────────────────────────────────────────────
fixed(
"prologue",
"resolve → stale-reader → refuse-build-flags → recipe line",
1,
0,
"crates/dwarves/src/buildthing.rs",
988,
),
fixed(
"phase order",
"code road → opts → kernel axis → plan → resolve → loop → GATE",
1,
1,
"crates/dwarves/src/buildthing.rs",
1306,
),
declared(
"selection",
"suite_cli::resolve — WHICH variants, from the set",
1,
2,
),
fixed(
"the funnel",
"BuildRun::spawn_recording_with_selection — every door lands here",
1,
3,
"crates/dwarves/src/build_thing.rs",
1813,
),
fixed(
"typ default",
"kind_of(v).unwrap_or(Exe) — an undeclared row silently becomes cargo",
1,
4,
"crates/dwarves/src/build_thing.rs",
1841,
),
// ── lane 2: the evaluator ───────────────────────────────────────────────
declared(
"is_cargo(typ)",
"Exe|Lib|Wasm|Service → cargo; else assemble",
2,
0,
),
fixed(
"dispatch(kind)",
"the 7-arm builder match — one arm per typ, in Rust",
2,
1,
"edda:nornir-build-thing/src/assemble.rs",
955,
),
fixed(
"assemble_cell",
"dwarves' SECOND full kind chain over the same typs",
2,
2,
"crates/dwarves/src/assemble_cell.rs",
395,
),
declared(
"settings tree",
"--opt / --param / presets resolve against the declaration",
2,
3,
),
declared(
"BUILT edges",
"BuildDag::derive — the chain's order IS the DAG",
2,
4,
),
Step {
name: "BuildPlan",
what: "typed, ABI-transported, NEVER executed — dwarves never calls plan()",
lane: 2,
row: 5,
origin: Origin::TypedNeverRun {
at: Cite::new("edda:nornir-build-thing/src/plugin.rs", 990),
},
},
// ── lane 3: isolation + output ──────────────────────────────────────────
declared(
"container?",
"[build.toolchain] present → container; absent → HOST",
3,
0,
),
fixed(
"host fallback",
"no toolchain ⇒ build on the host — undeclarable, unrefusable",
3,
1,
"crates/dwarves/src/build_thing.rs",
1917,
),
fixed(
"kernel runner",
"DraupnirKernelRunner + ContainerKernelBuilder, behind a cfg!",
3,
2,
"crates/dwarves/src/assemble_cell.rs",
553,
),
fixed(
"kernel script",
"the in-container step list is a String builder: make → merge → bzImage",
3,
3,
"edda:nornir-build-thing/src/kernel.rs",
1677,
),
fixed(
"output dirs",
".iso-out · .kernel-out/<name> · target/ · /usr/local/bin",
3,
4,
"edda:nornir-build-thing/src/image_build.rs",
760,
),
];
/// The palette, named once.
mod paint {
/// The declared signal path.
pub const CYAN: &str = "#6fd3f5";
/// The hard-coded half.
pub const CORAL: &str = "#f0a9a0";
/// Typed but never executed.
pub const AMBER: &str = "#e8c46a";
pub const INK: &str = "#eaf6ff";
pub const DIM: &str = "#9fc9e0";
pub const RULE: &str = "#143a52";
}
const W: u32 = 1180;
const LANE_X: [u32; 4] = [40, 320, 620, 920];
const BOX_W: u32 = 250;
const BOX_H: u32 = 52;
const ROW_H: u32 = 68;
const TOP: u32 = 118;
fn box_xy(s: &Step) -> (u32, u32) {
(LANE_X[s.lane as usize], TOP + u32::from(s.row) * ROW_H)
}
/// The stroke a step is drawn with — THE distinction the whole diagram exists
/// for, and the only place the two verdicts may differ.
fn stroke_of(o: &Origin) -> &'static str {
match o {
Origin::Declared => paint::CYAN,
Origin::HardCoded { .. } => paint::CORAL,
Origin::TypedNeverRun { .. } => paint::AMBER,
}
}
/// Hard-coded steps are drawn DASHED as well as coral, so the distinction
/// survives a greyscale print and a colour-blind reader.
fn dash_of(o: &Origin) -> &'static str {
match o {
Origin::Declared => "",
Origin::HardCoded { .. } => " stroke-dasharray=\"6 4\"",
Origin::TypedNeverRun { .. } => " stroke-dasharray=\"2 5\"",
}
}
fn esc(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
/// Render the whole diagram.
#[must_use]
pub fn svg() -> String {
let rows = STEPS.iter().map(|s| u32::from(s.row)).max().unwrap_or(0);
let height = TOP + (rows + 1) * ROW_H + 96;
let mut o = String::with_capacity(16_000);
let _ = write!(
o,
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {height}" font-family="'DejaVu Sans', 'Segoe UI', sans-serif">
<defs>
<linearGradient id="board" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0b1c2e"/><stop offset="1" stop-color="#06111d"/>
</linearGradient>
<linearGradient id="chip" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#14384f"/><stop offset="1" stop-color="#0e2a3e"/>
</linearGradient>
<linearGradient id="rail" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#173d57"/><stop offset="1" stop-color="#0c2438"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="{cyan}"/>
</marker>
</defs>
<rect x="0" y="0" width="{W}" height="{height}" fill="url(#board)"/>
<g stroke="{rule}" stroke-width="1"><line x1="0" y1="96" x2="{W}" y2="96"/></g>
<text x="40" y="40" fill="{ink}" font-size="22" font-weight="700">build thing — where the flow is DECLARED and where it is CODE</text>
<text x="40" y="62" fill="{dim}" font-size="14">every box carries the file:line it was audited at, 2026-09-10 — generated by <tspan fill="#bfe6ff">cargo xtask build-flow-svg</tspan>, never drawn</text>
"##,
cyan = paint::CYAN,
rule = paint::RULE,
ink = paint::INK,
dim = paint::DIM,
);
// Lane headings.
for (i, label) in [
"the DOORS",
"the FORGE",
"the EVALUATOR",
"ISOLATION + OUTPUT",
]
.iter()
.enumerate()
{
let _ = writeln!(
o,
" <text x=\"{}\" y=\"{}\" fill=\"{}\" font-size=\"13\" font-weight=\"700\" letter-spacing=\"1\">{}</text>",
LANE_X[i],
TOP - 20,
paint::DIM,
label
);
}
// The lane-to-lane rails, drawn first so boxes sit on top.
for i in 0..3 {
let x1 = LANE_X[i] + BOX_W;
let x2 = LANE_X[i + 1];
let _ = writeln!(
o,
" <line x1=\"{x1}\" y1=\"{y}\" x2=\"{x2}\" y2=\"{y}\" stroke=\"{c}\" stroke-width=\"2\" marker-end=\"url(#arrow)\" opacity=\"0.55\"/>",
y = TOP + BOX_H / 2,
c = paint::CYAN,
);
}
// The boxes.
for s in STEPS {
let (x, y) = box_xy(s);
let stroke = stroke_of(&s.origin);
let _ = writeln!(
o,
" <rect x=\"{x}\" y=\"{y}\" width=\"{BOX_W}\" height=\"{BOX_H}\" rx=\"6\" fill=\"url(#chip)\" stroke=\"{stroke}\" stroke-width=\"1.6\"{dash}/>",
dash = dash_of(&s.origin)
);
let _ = writeln!(
o,
" <text x=\"{tx}\" y=\"{ty}\" fill=\"{ink}\" font-size=\"13\" font-weight=\"700\">{n}</text>",
tx = x + 10,
ty = y + 19,
ink = paint::INK,
n = esc(s.name)
);
let _ = writeln!(
o,
" <text x=\"{tx}\" y=\"{ty}\" fill=\"{dim}\" font-size=\"10.5\">{w}</text>",
tx = x + 10,
ty = y + 33,
dim = paint::DIM,
w = esc(s.what)
);
// The citation IS the claim: a hard-coded box that could not name where
// it was read would be a drawing again.
let cite = match &s.origin {
Origin::Declared => "from the declaration".to_string(),
Origin::HardCoded { at } | Origin::TypedNeverRun { at } => at.render(),
};
let _ = writeln!(
o,
" <text x=\"{tx}\" y=\"{ty}\" fill=\"{c}\" font-size=\"9.5\" font-style=\"italic\">{cite}</text>",
tx = x + 10,
ty = y + 46,
c = stroke,
cite = esc(&cite)
);
}
// The legend + the count, which is the diagram's verdict in two numbers.
let hard = STEPS
.iter()
.filter(|s| matches!(s.origin, Origin::HardCoded { .. }))
.count();
let decl = STEPS
.iter()
.filter(|s| matches!(s.origin, Origin::Declared))
.count();
let ly = height - 56;
let _ = write!(
o,
r##" <rect x="40" y="{ly}" width="{lw}" height="40" rx="6" fill="url(#rail)" stroke="{rule}"/>
<line x1="56" y1="{lty}" x2="88" y2="{lty}" stroke="{cyan}" stroke-width="2.4"/>
<text x="96" y="{lt}" fill="{ink}" font-size="12">DECLARED · {decl} steps the recipe orders</text>
<line x1="356" y1="{lty}" x2="388" y2="{lty}" stroke="{coral}" stroke-width="2.4" stroke-dasharray="6 4"/>
<text x="396" y="{lt}" fill="{ink}" font-size="12">HARD-CODED · {hard} steps fixed in Rust</text>
<line x1="686" y1="{lty}" x2="718" y2="{lty}" stroke="{amber}" stroke-width="2.4" stroke-dasharray="2 5"/>
<text x="726" y="{lt}" fill="{ink}" font-size="12">TYPED, NEVER EXECUTED · BuildPlan/PlanStep</text>
</svg>
"##,
lw = W - 80,
lty = ly + 20,
lt = ly + 24,
cyan = paint::CYAN,
coral = paint::CORAL,
amber = paint::AMBER,
ink = paint::INK,
rule = paint::RULE,
);
o
}
/// Write the diagram to `<repo>/.nornir/assets/build-flow.svg`.
///
/// # Errors
/// Any I/O failure creating the directory or writing the file.
pub fn run(workspace_root: &Path) -> std::io::Result<PathBuf> {
let dir = workspace_root.join(".nornir").join("assets");
std::fs::create_dir_all(&dir)?;
let dst = dir.join("build-flow.svg");
std::fs::write(&dst, svg())?;
Ok(dst)
}