crates/nornir-build-thing/src/laws.rs
//! **THE BIJECTION LAWS — conformance every build thing owes.**
//!
//! The owner, 2026-08-26: *"viktigt att bijektion används för buildthings — det
//! måste vara i traitsen till och med."* One declaration, many surfaces — the
//! TOML text, the typed struct, the menu a pane renders, the flags a CLI takes,
//! the fragment a kernel decision commits, the receipt an artifact carries —
//! and every surface must be **mutually recoverable**, because a surface you
//! cannot map back is a surface that can drift and nothing will say so. The
//! inverses already existed one by one (`option_rows` ↔ `symbol_for_option`,
//! the decision-fragment header, gunnar's `toml_parity`, the pane/CLI shared
//! renderer); this module is where they stop being per-impl virtues and become
//! the [`BuildThing`] contract, checked by one function every implementation's
//! own test suite calls.
//!
//! # The laws
//!
//! | law | round trip | status |
//! |---|---|---|
//! | **A** declaration ↔ struct · **rows ↔ tree** | serialize the [`BuildVariantSet`] back to `[build]` TOML, reparse, compare; derive each variant's [`SettingsTree`] from its rows and render the rows back from the tree — equal | bijective on the STRUCT side |
//! | **B** menu ↔ declaration · **tree node ↔ declaration row** | the [`BuildMenu`] yields back the variant rows, their roles, and the rev; every declared row (option, param, kernel donor, kernel symbol, cargo feature) is a node of the impl's [`options`](BuildThing::options) tree with the same data, and every node outside a delegated (edge-bearing) subtree is a declared row | bijective |
//! | **C** path ↔ flag | every flag the tree can produce (`--opt <path>=on\|off`, `--opt <val>=<choice>`, `--param <path>=<value>`, `--build <path\|coordinate>`) renders, parses back, and resolves to the node it came from; a short alias resolves to its node or is a named refusal; kernel kebab handle ↔ `CONFIG_` symbol both ways | bijective |
//! | **D** decision ↔ fragment | render a `try_toggle` decision as a fragment, [`parse_decision_fragment`] it back; the tree's `kernel.*` judge cascades exactly as the model does | bijective |
//! | **E** receipt → coordinate → recipe | an artifact's receipt RESOLVES to the recipe that built it | recovery only — see below |
//!
//! ## Law A's one-way half, stated exactly
//!
//! What IS bijective: `parse(render(set)) == set` — the struct survives being
//! written back as a declaration and re-read, field for field. What is
//! deliberately NOT claimed: `render(parse(text)) == text` byte-for-byte. Two
//! spellings of one row are legal FOREVER (`[[build.installer]]` vs
//! `[[build.variant]] kind = "installer"` — the crate root rules the sugar
//! sections permanent), so distinct texts parse to one struct and a byte-level
//! text inverse cannot exist. The render half emits the canonical spelling;
//! the canonical text is a fixpoint (`render(parse(render(set))) ==
//! render(set)` follows from the struct law). One more honest exclusion: a
//! [`KernelBuild`](crate::kernel::KernelBuild) carrying an IN-MEMORY
//! `decision_fragment` is not a declaration (the field is `#[serde(skip)]` by
//! design — an operator's decision is not the repo's default), and the law
//! applies to declared sets; the negative test below watches the render half
//! forget that field so the exclusion stays visible.
//!
//! ## Why law E is not a bijection, and what it asserts instead
//!
//! `artifact → receipt` is injective by construction (two different builds get
//! different receipts — the resolved settings and inputs differ), but
//! `receipt → recipe` FORGETS on purpose: many receipts (different settings,
//! targets, toolchains) name one recipe, exactly as many lockfiles name one
//! registry crate. An inverse `recipe → receipt` would have to invent the
//! forgotten settings, so none is claimed. What the law asserts is that the
//! RECOVERY works: every declared variant's recipe is resolvable
//! ([`BuildThing::rebuild_recipe`] is `Some`, names the same variant and repo,
//! and materializes the declaration's own argv), and the coordinate's string
//! form — the one leg that IS a bijection — round-trips through
//! [`Coordinate::parse`].
//!
//! # The tree is the model; the rows are its spelling
//!
//! The owner, 2026-08-26, *"kaka på kaka"*: the settings tree
//! ([`crate::tree::SettingsTree`]) is the ONE in-memory model of a variant's
//! settings — what [`BuildThing::options`] answers, what
//! [`BuildThing::try_toggle`] judges, and what laws B, C and D quantify over.
//! `OptionSpec` / `ParamSpec` / `kernel_donor` survive only as toml rows, and
//! law A proves rows → tree → rows is the identity. No law walks the flat lists.
//!
//! # Using it
//!
//! Every impl's own suite calls [`assert_bijective`] on its build thing (the
//! same shape as `role_linkage_contract` / `vocabulary_compat`: the contract
//! guard lives with the contract, the call lives with the implementor):
//!
//! ```
//! # use nornir_build_thing::plugin::{BuildThing, Manifest};
//! # use nornir_build_thing::variant::BuildVariantSet;
//! # struct My;
//! # impl BuildThing for My {
//! # fn manifest(&self) -> Manifest {
//! # Manifest { abi: nornir_build_thing::ABI_VERSION, repo: "my".into(),
//! # source_crate: "my-build".into(), summary: "…".into() }
//! # }
//! # fn variants(&self) -> BuildVariantSet { BuildVariantSet::default() }
//! # }
//! nornir_build_thing::laws::assert_bijective(&My);
//! ```
//!
//! A failure names the LAW and the inverse that broke — "LAW B: variant 'x'
//! appears in no role section" — never a bare `assert_eq` dump, because the
//! whole point of a law is that its violation is legible.
use crate::build_dag::{BuildLine, resolve_build_target};
use crate::contract::Coordinate;
use crate::flags::{Flag, flags_of};
use crate::kernel::{HardeningProfile, Preset};
use crate::kernel_features::{
KernelFeatureModel, ToggleOutcome, option_name, parse_decision_fragment, split_kernel_opts,
};
use crate::plugin::{BuildMenu, BuildThing};
use crate::tree::{NodeKind, SettingsIndex, SettingsTree, ToggleRequest};
use crate::variant::{ArtifactKind, BuildConfig, BuildVariant, BuildVariantSet};
/// **The conformance gate.** Runs every law over `thing` and panics with the
/// full list of broken inverses, each naming its law. Call this from the
/// implementing crate's own test suite — conformance to these laws is part of
/// the [`BuildThing`] contract (see [`crate::plugin`]'s module docs).
pub fn assert_bijective<T: BuildThing>(thing: &T) {
let problems = check_bijective(thing);
assert!(
problems.is_empty(),
"the build thing for '{}' breaks {} bijection law(s):\n {}",
thing.manifest().repo,
problems.len(),
problems.join("\n ")
);
}
/// Every law, as a report: one line per broken inverse, empty = the laws hold.
/// [`assert_bijective`] is this plus the panic; this form exists for a caller
/// that wants to render the problems itself (a pane, a doctor verb).
pub fn check_bijective<T: BuildThing>(thing: &T) -> Vec<String> {
let set = thing.variants();
let mut out = Vec::new();
out.extend(law_declaration_struct(&set));
out.extend(law_rows_tree(&set));
out.extend(law_menu_declaration(&thing.menu(), &set, &thing.provenance().rev));
out.extend(law_tree_declaration(thing));
out.extend(law_path_flag(thing));
out.extend(law_decision_tree(thing));
out.extend(law_receipt_recipe(thing));
out
}
/// **LAW A, the tree half — rows ↔ tree.** Every variant's rows derive a
/// tree ([`SettingsTree::of_set`]) and the tree renders the rows back
/// ([`SettingsTree::rows`], [`kernel_pick`](SettingsTree::kernel_pick),
/// [`open_features`](SettingsTree::open_features)) — equal, field for field.
/// A kernel pick that does not parse is named here: it derives no tree.
pub fn law_rows_tree(set: &BuildVariantSet) -> Vec<String> {
const LAW: &str = "LAW A (rows ↔ tree)";
let mut out = Vec::new();
let sorted = |p: &[crate::params::ParamSpec]| {
let mut v = p.to_vec();
v.sort_by(|a, b| a.name.cmp(&b.name));
v
};
for inst in &set.installers {
let Some(tree) = SettingsTree::of_set(set, &inst.name) else { continue };
let rows = tree.rows();
if rows.options != inst.options {
out.push(format!("{LAW}: installer '{}': the tree renders different option rows than declared", inst.name));
}
if rows.params != sorted(&inst.params) {
out.push(format!("{LAW}: installer '{}': the tree renders different param rows than declared", inst.name));
}
if rows.fixtures != inst.fixtures {
out.push(format!("{LAW}: installer '{}': the tree renders different fixtures than declared", inst.name));
}
if rows.kernel_donor != inst.kernel_donor {
out.push(format!("{LAW}: installer '{}': the tree's kernel edge does not render back to the declared donor", inst.name));
}
if rows.edges != inst.edges {
out.push(format!("{LAW}: installer '{}': the tree's edges do not render back to the declared [edges] rows", inst.name));
}
if rows.presets != inst.presets {
out.push(format!("{LAW}: installer '{}': the tree's preset val does not render back to the declared [presets]", inst.name));
}
}
for iso in &set.isos {
let Some(tree) = SettingsTree::of_set(set, &iso.name) else { continue };
let rows = tree.rows();
if rows.options != iso.options || rows.params != sorted(&iso.params) || rows.fixtures != iso.fixtures {
out.push(format!("{LAW}: iso '{}': the tree renders different rows than declared", iso.name));
}
if rows.kernel_donor != iso.kernel_donor {
out.push(format!("{LAW}: iso '{}': the tree's kernel edge does not render back to the declared donor", iso.name));
}
if rows.edges != iso.edges {
out.push(format!("{LAW}: iso '{}': the tree's edges do not render back to the declared [edges] rows", iso.name));
}
if rows.presets != iso.presets {
out.push(format!("{LAW}: iso '{}': the tree's preset val does not render back to the declared [presets]", iso.name));
}
}
for k in &set.kernels {
let (Some(preset), Some(profile)) = (Preset::parse(&k.preset), HardeningProfile::parse(&k.profile)) else {
out.push(format!(
"{LAW}: kernel '{}' declares preset '{}' / profile '{}', which does not parse — no tree can be \
derived, so no rows can come back",
k.name, k.preset, k.profile
));
continue;
};
let Some(tree) = SettingsTree::of_set(set, &k.name) else { continue };
// Under the `kernel` scope — the tree of a declared kernel row spells
// its vals `kernel.preset` / `kernel.profile`, the spelling every door
// accepts (`tree::SettingsTree::of_kernel`, lane AK).
if tree.kernel_pick(crate::tree::KERNEL_PATH) != Some((preset, profile)) {
out.push(format!("{LAW}: kernel '{}': the tree's preset/profile vals do not render back to the declared pick", k.name));
}
// The symbol rows ARE the model's rows, one node each.
let model = KernelFeatureModel::derive(preset, profile);
let want: Vec<String> = model
.option_rows()
.iter()
.map(|r| format!("{}{}{}", crate::tree::KERNEL_PATH, crate::tree::PATH_SEP, r.option))
.collect();
let got: Vec<String> = tree.walk().into_iter().filter(|n| matches!(&n.kind, NodeKind::Feature(f) if f.feature.starts_with("CONFIG_"))).map(|n| n.path.clone()).collect();
if got != want {
out.push(format!("{LAW}: kernel '{}': the tree's symbol nodes are not the pick's rows", k.name));
}
}
for v in &set.variants {
// A `kind = kernel` variant's tree IS the kernel model's symbol rows
// (judged above, against the pick) — it has no cargo `open` axis to
// render back to. Measured on tunnr's `tunnr-kernel` row (2026-08-29):
// the nuked model's clicked-off symbols read as an "open" set nobody
// declared.
if v.kind == ArtifactKind::Kernel {
continue;
}
let Some(tree) = SettingsTree::of_set(set, &v.name) else { continue };
if tree.open_features() != v.open {
out.push(format!("{LAW}: variant '{}': the tree's features do not render back to the declared `open` axis", v.name));
}
let vrows = tree.rows();
if vrows.edges != v.edges {
out.push(format!("{LAW}: variant '{}': the tree's edges do not render back to the declared [edges] rows", v.name));
}
if vrows.presets != v.presets {
out.push(format!("{LAW}: variant '{}': the tree's preset val does not render back to the declared [presets]", v.name));
}
}
// Rows rendered from a tree derive the same tree again (minus the edge, which is the donor row).
for inst in &set.installers {
let Some(tree) = SettingsTree::of_set(set, &inst.name) else { continue };
let rows = tree.rows();
let again = SettingsTree::of_rows(&inst.name, ArtifactKind::Installer, &rows.options, &rows.params, &rows.fixtures);
// The rendered spelling is a fixpoint, and the re-derived tree holds
// the same nodes (root-setting ORDER is not a decision: the rows sort
// params by name, the declaration may interleave them).
let nodes = |t: &SettingsTree| {
let mut v: Vec<(String, NodeKind, Option<bool>, Option<crate::tree::Edge>)> = t
.walk()
.into_iter()
.filter(|n| {
n.edge.is_none()
&& n.path != crate::tree::KERNEL_PATH
&& !n.path.starts_with("kernel.")
&& n.path != crate::tree::PRESET_PATH
// The `data` group is its own table, like the preset val
// and the edge rows: `of_rows` builds a tree out of the
// OPTION and PARAM rows alone, so it cannot carry a group
// node — and a group is compared as a row below.
&& n.path != crate::tree::DATA_FS_PATH
&& n.path != crate::tree::DATA_IO_PATH
})
.map(|n| (n.path.clone(), n.kind.clone(), n.when, n.edge.clone()))
.collect();
v.sort_by(|a, b| a.0.cmp(&b.0));
v
};
let mut again_rows = again.rows();
again_rows.kernel_donor = rows.kernel_donor.clone(); // the donor row is the edge, not a bare row
again_rows.edges = rows.edges.clone(); // …and so is every other edge row
again_rows.presets = rows.presets.clone(); // …and the preset val is its own table
// The `data` group is compared where it is DECLARED, not through the
// re-derivation: `of_rows` has only the option/param rows to build from.
// This is the row assertion the identity leg cannot make, and it is the
// one that matters — the tree must render the declared group BACK.
if rows.data != inst.data {
out.push(format!("{LAW}: installer '{}': the tree's data vals do not render back to the declared [data] group", inst.name));
}
again_rows.data = rows.data.clone();
if again_rows != rows || nodes(&again) != nodes(&tree) {
out.push(format!("{LAW}: installer '{}': rows → tree → rows → tree is not the identity", inst.name));
}
}
out
}
/// The trees an impl answers, one per declared variant, in declaration order —
/// paired with the reference derivation from the same declaration.
fn forest_of<T: BuildThing>(thing: &T, set: &BuildVariantSet) -> Vec<(SettingsTree, Option<SettingsTree>)> {
SettingsTree::forest(set)
.into_iter()
.map(|derived| {
let answered = thing.options(&derived.variant, "");
(derived, answered)
})
.collect()
}
/// **LAW B, the tree half — tree node ↔ declaration row.** Every row the
/// declaration makes (an installer option, a param, a kernel donor, a kernel
/// symbol of the pick, a cargo feature) is a node of the impl's
/// [`options`](BuildThing::options) tree at its path, with the same kind and
/// data; and every node the impl answers is either such a row or sits under a
/// DELEGATED subtree (one whose root carries an [`Edge`](crate::tree::Edge) —
/// a donor kernel read from fetched source may say more than the declaration).
pub fn law_tree_declaration<T: BuildThing>(thing: &T) -> Vec<String> {
const LAW: &str = "LAW B (tree node ↔ declaration row)";
let set = thing.variants();
let mut out = Vec::new();
for (derived, answered) in forest_of(thing, &set) {
let Some(answered) = answered else {
out.push(format!(
"{LAW}: variant '{}' is declared but options() answers no tree for it",
derived.variant
));
continue;
};
if answered.variant != derived.variant || answered.typ != derived.typ {
out.push(format!(
"{LAW}: variant '{}': the tree says ('{}', {:?}), the declaration says ('{}', {:?})",
derived.variant, answered.variant, answered.typ, derived.variant, derived.typ
));
}
// row → node: every derived node is answered, equal.
for row in derived.walk() {
match answered.get(&row.path) {
None => out.push(format!(
"{LAW}: variant '{}': declared row '{}' ({}) has no node in the answered tree",
derived.variant,
row.path,
row.kind.word()
)),
Some(node) => {
if node.kind != row.kind || node.when != row.when || node.edge != row.edge {
out.push(format!(
"{LAW}: variant '{}': node '{}' does not carry its row — kind/when/edge \
differ from the declaration",
derived.variant, row.path
));
}
}
}
}
// node → row: an answered node is a row, or lives under an edge.
let delegated: Vec<&str> = answered
.walk()
.into_iter()
.filter(|n| n.edge.is_some())
.map(|n| n.path.as_str())
.collect();
for node in answered.walk() {
if derived.get(&node.path).is_some() {
continue;
}
let under_edge = delegated
.iter()
.any(|d| node.path.starts_with(d) && node.path[d.len()..].starts_with(crate::tree::PATH_SEP));
if !under_edge {
out.push(format!(
"{LAW}: variant '{}': node '{}' ({}) answers to no declared row and sits under no \
edge — it is not recoverable to the declaration",
derived.variant,
node.path,
node.kind.word()
));
}
}
}
out
}
/// **LAW C — path ↔ flag**, over the whole tree. Every flag the answered
/// tree can produce ([`flags_of`]) renders and parses back to itself, and its
/// path resolves to the node it was rendered from (`--build <coordinate>` to a
/// node carrying that edge). A node's short name resolves to that node or is
/// an AMBIGUOUS refusal by name — never to a different node.
pub fn law_path_flag<T: BuildThing>(thing: &T) -> Vec<String> {
const LAW: &str = "LAW C (path ↔ flag)";
let set = thing.variants();
let mut out = Vec::new();
for (derived, answered) in forest_of(thing, &set) {
let Some(tree) = answered else { continue }; // LAW B names the missing tree.
let v = &derived.variant;
let index = SettingsIndex::of(&tree);
for flag in flags_of(&tree) {
let rendered = flag.render();
match Flag::parse(&rendered) {
Err(e) => {
out.push(format!("{LAW}: variant '{v}': '{rendered}' does not PARSE back: {e:#}"));
continue;
}
Ok(back) if back != flag => {
out.push(format!(
"{LAW}: variant '{v}': '{rendered}' parsed back as '{}', not the flag it rendered from",
back.render()
));
continue;
}
Ok(_) => {}
}
let resolves = match &flag {
Flag::Build { target } if index.get(target).is_none() => tree
.walk()
.into_iter()
.any(|n| n.edge.as_ref().is_some_and(|e| &e.coordinate == target)),
other => index.resolve(other.path()).is_ok(),
};
if !resolves {
out.push(format!(
"{LAW}: variant '{v}': '{rendered}' resolves to no node of the tree it was rendered from"
));
}
}
// SCOPED flags (design f3): for every edge, `--build <edge> { --opt
// <child>=<v> }` and `--build <edge> --opt <edge>.<child>=<v>` are the
// SAME statement — both parse to one canonical BuildLine, and each of
// its two renderings parses back to it. The child is the first setting
// node under the edge when there is one (`kernel.preset`), else a
// synthetic `x`; a coordinate target must resolve to the same edge.
for node in tree.walk() {
let Some(edge) = &node.edge else { continue };
let (child, value) = match node.children.first() {
Some(c) => match &c.kind {
NodeKind::Feature(_) => (c.name().to_string(), "off".to_string()),
NodeKind::Val(val) => (c.name().to_string(), val.choices.first().cloned().unwrap_or_else(|| val.default.clone())),
NodeKind::Setting(_) => (c.name().to_string(), "1".to_string()),
},
None => ("x".to_string(), "on".to_string()),
};
let scoped = format!("--build {} {{ --opt {child}={value} }}", node.path);
let flat = format!("--build {} --opt {}{}{child}={value}", node.path, node.path, crate::tree::PATH_SEP);
match (BuildLine::parse(&scoped), BuildLine::parse(&flat)) {
(Ok(a), Ok(b)) if a == b => {
for rendered in [a.render(), a.render_scoped()] {
match BuildLine::parse(&rendered) {
Ok(back) if back == a => {}
other => out.push(format!(
"{LAW}: variant '{v}': '{rendered}' does not parse back to the line it rendered from: {other:?}"
)),
}
}
match a.canonical_in(&tree) {
Ok(c) if c == a => {}
Ok(c) => out.push(format!(
"{LAW}: variant '{v}': '{scoped}' canonicalises to '{}' — a full path changed under resolution",
c.render()
)),
Err(e) => out.push(format!("{LAW}: variant '{v}': '{scoped}' is refused by the tree it was rendered from: {e:#}")),
}
}
(Ok(a), Ok(b)) => out.push(format!(
"{LAW}: variant '{v}': '{scoped}' and '{flat}' parse to DIFFERENT lines ('{}' vs '{}') — the brace is not the path prefix",
a.render(),
b.render()
)),
(a, b) => out.push(format!("{LAW}: variant '{v}': scoped/flat parse failed: {a:?} / {b:?}")),
}
match resolve_build_target(&tree, &edge.coordinate) {
Ok((p, _)) if p == node.path => {}
Ok((p, _)) => out.push(format!(
"{LAW}: variant '{v}': `--build {}` resolves to '{p}', not to the edge '{}' that carries it",
edge.coordinate, node.path
)),
Err(e) => out.push(format!("{LAW}: variant '{v}': `--build {}` resolves to no edge: {e:#}", edge.coordinate)),
}
}
// The kernel handle ↔ `CONFIG_` symbol mapping inverts both ways over
// every symbol node, and `--opt kernel.<handle>=off` splits back to it.
for node in tree.walk() {
let NodeKind::Feature(f) = &node.kind else { continue };
if !f.feature.starts_with("CONFIG_") {
continue;
}
if option_name(&f.feature) != node.name() {
out.push(format!(
"{LAW}: variant '{v}': symbol {} renders handle '{}' but the node is '{}' — the kebab \
direction broke",
f.feature,
option_name(&f.feature),
node.path
));
}
let prefix = node.path.rsplit_once(crate::tree::PATH_SEP).map(|(p, _)| p).unwrap_or("");
if let Some((preset, profile)) = tree.kernel_pick(prefix) {
let model = KernelFeatureModel::derive(preset, profile);
if model.symbol_for_option(node.name()) != Some(f.feature.as_str()) {
out.push(format!(
"{LAW}: variant '{v}': handle '{}' resolves to {:?}, not back to {} — the symbol \
direction broke",
node.path,
model.symbol_for_option(node.name()),
f.feature
));
}
}
if prefix == crate::tree::KERNEL_PATH {
let flag = format!("{}=off", node.path);
match split_kernel_opts(&[flag.clone()]) {
Ok((pairs, rest)) if rest.is_empty() && pairs == [(node.name().to_string(), "off".to_string())] => {}
other => out.push(format!("{LAW}: variant '{v}': '--opt {flag}' did not split back to its row: {other:?}")),
}
}
}
for node in tree.walk() {
match index.resolve(node.name()) {
Ok(r) if r.node.path == node.path => {}
Ok(r) => out.push(format!(
"{LAW}: variant '{v}': the short name '{}' of '{}' resolves to '{}' — an alias that \
lands on a DIFFERENT node",
node.name(),
node.path,
r.node.path
)),
Err(e) if format!("{e:#}").contains("AMBIGUOUS") => {}
Err(e) => out.push(format!(
"{LAW}: variant '{v}': the short name '{}' of '{}' neither resolves nor is refused as \
ambiguous: {e:#}",
node.name(),
node.path
)),
}
}
}
out
}
/// **LAW D — decision ↔ fragment, over the kernel subtree.** For each
/// `[[build.kernel]]`, a sample decision made through [`BuildThing::try_toggle`]
/// (the door both the pane and the CLI use) drops exactly the symbols the
/// model's cascade drops beyond the click; the tree renders that state as a
/// fragment ([`SettingsTree::kernel_decision`]) and [`parse_decision_fragment`]
/// reads back the same `(preset, profile, clicks)` with the veto list being
/// exactly click + cascade — so the file states the whole consequence and
/// nothing else.
pub fn law_decision_tree<T: BuildThing>(thing: &T) -> Vec<String> {
const LAW: &str = "LAW D (decision ↔ fragment)";
let set = thing.variants();
let mut out = Vec::new();
for k in &set.kernels {
let (Some(preset), Some(profile)) = (Preset::parse(&k.preset), HardeningProfile::parse(&k.profile)) else {
continue; // LAW C names the unparseable pick.
};
let model = KernelFeatureModel::derive(preset, profile);
let Some(clicked) = model.symbols.iter().filter(|s| s.enabled).find_map(|s| {
let mut trial = Vec::new();
(model.try_toggle(&mut trial, &s.name) == Some(ToggleOutcome::Off)).then_some(s.name.clone())
}) else {
continue;
};
let dropped = model.cascade_for(std::slice::from_ref(&clicked));
let mut want: Vec<String> = model
.symbols
.iter()
.enumerate()
.filter(|&(i, s)| dropped[i] && s.name != clicked && s.enabled)
// Under the `kernel` scope, the spelling the tree uses (lane AK).
.map(|(_, s)| format!("{}{}{}", crate::tree::KERNEL_PATH, crate::tree::PATH_SEP, option_name(&s.name)))
.collect();
want.sort();
let d = thing.try_toggle(&ToggleRequest::new(&k.name, option_name(&clicked), false));
if !d.verdict.ok {
out.push(format!(
"{LAW}: kernel '{}': the model lets go of {clicked} but the tree's judge refuses: {}",
k.name,
d.verdict.line()
));
continue;
}
let mut got = d.cascade.clone();
got.sort();
if got != want {
out.push(format!(
"{LAW}: kernel '{}': dropping {clicked} cascades to {got:?} over the tree but {want:?} in \
the model — the two doors would commit different fragments",
k.name
));
}
// The fragment: rendered from the tree's state, parsed back.
let Some(tree) = thing.options(&k.name, "") else { continue }; // LAW B names it
let state = [(d.path.clone(), false)].into_iter().collect();
let fragment = match tree.kernel_decision(crate::tree::KERNEL_PATH, &state, &Default::default()) {
Ok(kd) => kd.fragment,
Err(e) => {
out.push(format!("{LAW}: kernel '{}': the tree cannot render the decision it accepted: {e:#}", k.name));
continue;
}
};
match parse_decision_fragment(&fragment) {
Err(e) => out.push(format!("{LAW}: kernel '{}': the rendered fragment does not parse back: {e:#}", k.name)),
Ok(p) => {
if p.preset != preset || p.profile != profile {
out.push(format!(
"{LAW}: kernel '{}': the fragment came back as ({}, {}), not the ({}, {}) that rendered it",
k.name, p.preset.as_str(), p.profile.as_str(), preset.as_str(), profile.as_str()
));
}
if p.clicked_off != vec![clicked.clone()] {
out.push(format!("{LAW}: kernel '{}': the clicks came back as {:?}, not [{clicked}]", k.name, p.clicked_off));
}
let mut vetoed = p.vetoed.clone();
vetoed.sort();
let mut whole: Vec<String> = d.cascade.iter().map(|path| model.symbol_for_option(path).unwrap_or(path).to_string()).collect();
whole.push(clicked.clone());
whole.sort();
if vetoed != whole {
out.push(format!(
"{LAW}: kernel '{}': the fragment vetoes {vetoed:?} but the decision says {whole:?} — the file \
does not state the whole consequence",
k.name
));
}
}
}
}
out
}
/// Which fields of two sets disagree, by name — so a law failure says WHERE the
/// inverse broke instead of dumping two structs.
fn set_diff(a: &BuildVariantSet, b: &BuildVariantSet) -> Vec<String> {
let mut out = Vec::new();
macro_rules! field {
($f:ident) => {
if a.$f != b.$f {
out.push(stringify!($f).to_string());
}
};
}
field!(offers);
field!(exclusive);
field!(required);
field!(allow_unbuilt);
field!(allow_unbuilt_bins);
field!(allow_dark_test_files);
field!(downstream);
field!(base_suite);
field!(containers);
field!(vms);
field!(isos);
field!(installers);
field!(kernels);
field!(toolchain);
if a.variants != b.variants {
if a.variants.len() != b.variants.len() {
out.push(format!(
"variants (count {} vs {})",
a.variants.len(),
b.variants.len()
));
} else {
for (x, y) in a.variants.iter().zip(&b.variants) {
if x != y {
out.push(format!("variant '{}'", x.name));
}
}
}
}
out
}
/// **LAW A — declaration ↔ struct.** The set writes back as `[build]` TOML and
/// reparses to the same struct. See the module docs for the exact one-way half
/// (byte-level text identity is NOT claimed; the sugar spellings are permanent).
pub fn law_declaration_struct(set: &BuildVariantSet) -> Vec<String> {
const LAW: &str = "LAW A (declaration ↔ struct)";
// `emitting`, not a struct literal: LAW A is the round trip an EMISSION
// takes, and an emission carries the schema header R1 computes for it. A
// literal here would test a file no writer produces.
let text = match toml::to_string(&BuildConfig::emitting(set)) {
Ok(t) => t,
Err(e) => {
return vec![format!(
"{LAW}: the render half failed — the struct cannot be written back \
as a declaration at all: {e}"
)];
}
};
let reparsed = match BuildVariantSet::from_toml_str(&text) {
Ok(s) => s,
Err(e) => {
return vec![format!(
"{LAW}: the render half produced text the parse half refuses: {e:#}"
)];
}
};
let diff = set_diff(set, &reparsed);
if diff.is_empty() {
Vec::new()
} else {
vec![format!(
"{LAW}: render → reparse did not come back equal; the inverse broke \
in: {}",
diff.join(", ")
)]
}
}
/// **LAW B — menu ↔ declaration.** The [`BuildMenu`] is recoverable to the
/// variant rows, their roles, and the revision that produced it: `cargo_set`
/// carries the declaration verbatim, the entries name exactly the declared
/// cargo rows in declaration order, every row is reachable from some role
/// section and no section names an undeclared row, and a selector that speaks
/// the cargo dialect deserializes back to the row it was rendered from.
pub fn law_menu_declaration(menu: &BuildMenu, set: &BuildVariantSet, rev: &str) -> Vec<String> {
const LAW: &str = "LAW B (menu ↔ declaration)";
let mut out = Vec::new();
match &menu.cargo_set {
None => out.push(format!(
"{LAW}: the menu carries no cargo_set — the declaration is not \
recoverable from the menu (a non-cargo build thing would need its \
own recoverability story before dropping the field)"
)),
Some(cs) => {
let diff = set_diff(set, cs);
if !diff.is_empty() {
out.push(format!(
"{LAW}: menu.cargo_set ≠ variants(); the declaration recovered \
from the menu differs in: {}",
diff.join(", ")
));
}
}
}
let declared: Vec<&str> = set.names();
let entry_ids: Vec<&str> = menu.entries.iter().map(|e| e.id.as_str()).collect();
if entry_ids != declared {
let missing: Vec<&str> =
declared.iter().filter(|n| !entry_ids.contains(n)).copied().collect();
let extra: Vec<&str> =
entry_ids.iter().filter(|n| !declared.contains(n)).copied().collect();
out.push(format!(
"{LAW}: the menu entries are not the declared rows in declaration \
order (missing: [{}], undeclared: [{}])",
missing.join(", "),
extra.join(", ")
));
}
for &name in &declared {
if menu.role_of(name).is_none() {
out.push(format!(
"{LAW}: variant '{name}' appears in no role section — its role is \
not recoverable from the menu"
));
}
}
for g in &menu.roles {
for v in &g.variants {
if set.get(v).is_none() {
out.push(format!(
"{LAW}: role section '{}' names '{v}', which the declaration \
does not declare — the section is not recoverable to rows",
g.role.as_str()
));
}
}
}
// A selector is opaque to the forge; but when it speaks the cargo dialect
// (deserializes as a BuildVariant) it must be the row it came from — a
// selector that parses to a DIFFERENT row is drift wearing opacity.
for e in &menu.entries {
if let Ok(v) = serde_json::from_value::<BuildVariant>(e.selector.clone()) {
if set.get(&e.id) != Some(&v) {
out.push(format!(
"{LAW}: entry '{}' carries a cargo-shaped selector that does \
not parse back to the declared row",
e.id
));
}
}
}
if menu.rev != rev {
out.push(format!(
"{LAW}: the menu pins rev '{}' but provenance says '{}' — the menu is \
not recoverable to the revision that produced it",
menu.rev, rev
));
}
out
}
/// **LAW E — receipt → coordinate → recipe.** Recovery, not bijection (see the
/// module docs for why no inverse is claimed): every declared variant's recipe
/// resolves, names the same variant and repo, and materializes the
/// declaration's argv — so a receipt's `atom` field is always enough to get
/// back to the recipe that built the artifact. The coordinate's string form,
/// the one leg that IS bijective, round-trips through [`Coordinate::parse`].
pub fn law_receipt_recipe<T: BuildThing>(thing: &T) -> Vec<String> {
const LAW: &str = "LAW E (receipt → coordinate → recipe)";
let mut out = Vec::new();
let m = thing.manifest();
let set = thing.variants();
let coord = Coordinate {
krate: m.source_crate.clone(),
version: "0.0.0".to_string(),
hash: Some("deadbeefdeadbeef".to_string()),
};
let spelled = coord.to_string();
match Coordinate::parse(&spelled) {
Ok(back) if back == coord => {}
Ok(back) => out.push(format!(
"{LAW}: coordinate '{spelled}' parsed back as '{back}' — the string \
form drifted from itself"
)),
Err(e) => out.push(format!(
"{LAW}: coordinate '{spelled}' does not parse back at all: {e:#}"
)),
}
for v in &set.variants {
match thing.rebuild_recipe(&v.name) {
None => out.push(format!(
"{LAW}: a receipt naming atom '{}' cannot RECOVER its recipe — \
rebuild_recipe returned None for a declared variant",
v.name
)),
Some(r) => {
if r.variant != v.name {
out.push(format!(
"{LAW}: the recipe recovered for atom '{}' says it builds \
'{}' — the recovery landed on a different variant",
v.name, r.variant
));
}
if r.repo != m.repo {
out.push(format!(
"{LAW}: the recipe for '{}' names repo '{}', not '{}' — \
the recovery leaves the repo",
v.name, r.repo, m.repo
));
}
if r.cargo_args != v.cargo_args() {
out.push(format!(
"{LAW}: the recipe for '{}' materializes a different argv \
than the declaration — the recovered recipe would rebuild \
something else",
v.name
));
}
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::{Manifest, RebuildRecipe, RoleGroup};
/// A small but NON-VACUOUS declaration: cargo rows, an installer with
/// options, a kernel row — so every law has a subject even when a real
/// impl declares no kernel. Built through the real parser, because the
/// laws are about what a declaration round-trips to.
fn lawful_set() -> BuildVariantSet {
BuildVariantSet::from_toml_str(
r#"
[build]
offers = ["gui", "wg"]
[[build.variant]]
name = "toy-server"
kind = "service"
role = "server"
default = true
[[build.variant]]
name = "toy-cli"
features = ["gui"]
role = "client"
default = true
[[build.installer]]
name = "toy-installer"
[build.installer.kernel_donor]
repo = "toy"
kernel = "toy-kernel"
[[build.installer.params]]
name = "hub_conf"
staged_file = "hub.conf"
[[build.installer.options]]
name = "wireguard"
feature = "wg"
params_when_on = ["hub_conf"]
[[build.kernel]]
name = "toy-kernel"
version = "6.12.4"
tarball_sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
preset = "slim"
"#,
)
.expect("the lawful declaration parses")
}
struct Lawful;
impl BuildThing for Lawful {
fn manifest(&self) -> Manifest {
Manifest {
abi: crate::plugin::ABI_VERSION,
repo: "toy".to_string(),
source_crate: "toy-build-thing".to_string(),
summary: "the laws' own reference impl".to_string(),
}
}
fn variants(&self) -> BuildVariantSet {
lawful_set()
}
}
/// The reference impl passes every law — the floor under every red proof
/// below: a gate that cannot go green is as useless as one that cannot
/// go red.
#[test]
fn a_lawful_build_thing_passes_every_law() {
assert_bijective(&Lawful);
}
/// ★ RED PROOF, LAW A — the render half FORGETS an in-memory kernel
/// decision (`#[serde(skip)]` by design), and the law says so naming the
/// field. This is the documented exclusion made visible: the law holds for
/// DECLARED sets, and a set carrying operator state is not one.
#[test]
fn law_a_names_the_field_the_render_half_forgets() {
let mut set = lawful_set();
set.kernels[0].decision_fragment = Some("# CONFIG_FOO is not set\n".to_string());
let p = law_declaration_struct(&set);
assert_eq!(p.len(), 1, "{p:?}");
assert!(p[0].contains("LAW A") && p[0].contains("kernels"), "{}", p[0]);
}
/// ★ RED PROOF, LAW B — a menu that drops the declaration, renames a row's
/// section, or loses the rev is caught by name, not by a diff dump.
#[test]
fn law_b_names_the_menu_that_cannot_be_recovered() {
struct MenuLiar;
impl BuildThing for MenuLiar {
fn manifest(&self) -> Manifest {
Lawful.manifest()
}
fn variants(&self) -> BuildVariantSet {
lawful_set()
}
fn menu(&self) -> BuildMenu {
BuildMenu {
repo: "toy".to_string(),
rev: "not-the-provenance-rev".to_string(),
roles: Vec::<RoleGroup>::new(),
entries: Vec::new(),
cargo_set: None,
}
}
}
let p = check_bijective(&MenuLiar);
let b: Vec<&String> = p.iter().filter(|l| l.contains("LAW B")).collect();
assert!(
b.iter().any(|l| l.contains("no cargo_set")),
"the dropped declaration is named: {b:?}"
);
assert!(
b.iter().any(|l| l.contains("no role section")),
"the unreachable rows are named: {b:?}"
);
assert!(
b.iter().any(|l| l.contains("rev")),
"the lost rev is named: {b:?}"
);
}
/// ★ RED PROOF, LAW C — an option whose name cannot survive its own flag
/// spelling (`=` inside the handle) is caught over the tree, before any
/// CLI ever mangles it; and LAW A names a kernel pick that does not parse
/// (no tree derives from it, so nothing is invertible).
#[test]
fn law_c_names_the_flag_that_does_not_parse_back() {
struct Renamed;
impl BuildThing for Renamed {
fn manifest(&self) -> Manifest {
Lawful.manifest()
}
fn variants(&self) -> BuildVariantSet {
let mut set = lawful_set();
set.installers[0].options[0].name = "wg=hub".to_string();
set
}
}
let p = law_path_flag(&Renamed);
assert!(p.iter().any(|l| l.contains("LAW C") && l.contains("wg=hub")), "{p:?}");
let mut set = lawful_set();
set.kernels[0].preset = "slin".to_string();
let p = law_rows_tree(&set);
assert!(p.iter().any(|l| l.contains("LAW A") && l.contains("slin")), "{p:?}");
}
/// ★ RED PROOF, LAW A (rows ↔ tree) — a tree that does not render its rows
/// back is named per installer; the lawful set passes non-vacuously.
#[test]
fn law_a_rows_and_tree_are_each_others_spelling() {
let set = lawful_set();
assert_eq!(law_rows_tree(&set), Vec::<String>::new());
assert!(!set.installers[0].options.is_empty() && set.installers[0].kernel_donor.is_some());
let t = SettingsTree::of_set(&set, "toy-installer").unwrap();
let rows = t.rows();
assert_eq!(rows.options, set.installers[0].options);
assert_eq!(rows.kernel_donor, set.installers[0].kernel_donor);
}
/// ★ RED PROOF, LAW E — a build thing that forgets how to rebuild a
/// declared variant is named, per variant.
#[test]
fn law_e_names_the_recipe_that_cannot_be_recovered() {
struct Amnesiac;
impl BuildThing for Amnesiac {
fn manifest(&self) -> Manifest {
Lawful.manifest()
}
fn variants(&self) -> BuildVariantSet {
lawful_set()
}
fn rebuild_recipe(&self, _variant: &str) -> Option<RebuildRecipe> {
None
}
}
let p = law_receipt_recipe(&Amnesiac);
assert!(
p.iter().any(|l| l.contains("LAW E") && l.contains("toy-server")),
"{p:?}"
);
assert!(
p.iter().any(|l| l.contains("toy-cli")),
"every declared variant is checked: {p:?}"
);
}
/// The reference derivation IS a lawful tree: B's tree half and C's path
/// half hold over it, non-vacuously (features, settings, a val, an edge).
#[test]
fn law_b_tree_and_law_c_paths_hold_over_a_real_tree() {
let set = lawful_set();
let t = Lawful.options("toy-installer", "").unwrap();
assert!(t.get("kernel.preset").is_some() && t.get("wireguard").is_some(), "{:?}", t.paths());
assert_eq!(law_tree_declaration(&Lawful), Vec::<String>::new());
assert_eq!(law_path_flag(&Lawful), Vec::<String>::new());
assert_eq!(law_decision_tree(&Lawful), Vec::<String>::new());
assert!(!set.kernels.is_empty());
}
/// ★ RED PROOF, LAW B (tree) — an impl whose tree drops a declared row,
/// mangles one, or invents a node outside any edge is named per node.
#[test]
fn law_b_names_the_node_that_is_not_its_row() {
struct TreeLiar;
impl BuildThing for TreeLiar {
fn manifest(&self) -> Manifest {
Lawful.manifest()
}
fn variants(&self) -> BuildVariantSet {
lawful_set()
}
fn options(&self, variant: &str, path: &str) -> Option<SettingsTree> {
let mut t = SettingsTree::of_set(&self.variants(), variant)?;
if variant == "toy-installer" {
// drop the declared option, invent an undeclared feature at the root,
// and add a node UNDER the kernel edge (allowed: delegated).
t.roots.retain(|n| n.path != "wireguard");
t.roots.push(crate::tree::SettingsNode::leaf(
"invented",
"",
NodeKind::Feature(crate::tree::FeatureNode::default()),
));
if let Some(k) = t.roots.iter_mut().find(|n| n.path == "kernel") {
k.children.push(crate::tree::SettingsNode::leaf(
"kernel.from_kconfig",
"",
NodeKind::Feature(crate::tree::FeatureNode::default()),
));
}
}
t.subtree(path)
}
}
let p = law_tree_declaration(&TreeLiar);
assert!(p.iter().any(|l| l.contains("LAW B") && l.contains("'wireguard'") && l.contains("no node")), "{p:?}");
assert!(p.iter().any(|l| l.contains("'invented'") && l.contains("no declared row")), "{p:?}");
assert!(!p.iter().any(|l| l.contains("from_kconfig")), "a node under an edge is delegated, not a lie: {p:?}");
}
/// ★ RED PROOF, LAW C (paths) — a node whose path cannot survive its own
/// flag spelling, and an alias that lands on another node, are named.
#[test]
fn law_c_names_the_path_that_does_not_round_trip() {
struct PathLiar;
impl BuildThing for PathLiar {
fn manifest(&self) -> Manifest {
Lawful.manifest()
}
fn variants(&self) -> BuildVariantSet {
lawful_set()
}
fn options(&self, variant: &str, path: &str) -> Option<SettingsTree> {
let mut t = SettingsTree::of_set(&self.variants(), variant)?;
if variant == "toy-installer" {
if let Some(n) = t.roots.iter_mut().find(|n| n.path == "wireguard") {
n.path = "wire guard".to_string();
}
}
t.subtree(path)
}
}
let p = law_path_flag(&PathLiar);
assert!(p.iter().any(|l| l.contains("LAW C") && l.contains("wire guard") && l.contains("PARSE")), "{p:?}");
}
/// ★ RED PROOF, LAW D (tree) — a judge whose cascade differs from the
/// model's is caught with both lists in the message.
#[test]
fn law_d_names_a_judge_that_cascades_differently_from_the_model() {
struct Softie;
impl BuildThing for Softie {
fn manifest(&self) -> Manifest {
Lawful.manifest()
}
fn variants(&self) -> BuildVariantSet {
lawful_set()
}
fn try_toggle(&self, req: &ToggleRequest) -> crate::tree::ToggleDecision {
let mut d = SettingsTree::of_set(&self.variants(), &req.variant).unwrap().try_toggle(req);
d.cascade.clear();
d.cascade.push("nothing-real".to_string());
d
}
}
let p = law_decision_tree(&Softie);
assert!(p.iter().any(|l| l.contains("LAW D") && l.contains("nothing-real")), "{p:?}");
}
/// LAW D's parser red proofs live beside the parser
/// (`kernel_features::tests::the_fragment_parser_refuses_what_is_not_a_decision`);
/// here the law is watched holding over the lawful kernel row, non-vacuously.
#[test]
fn law_d_holds_over_a_real_kernel_row() {
let set = lawful_set();
assert!(!set.kernels.is_empty(), "the subject must exist");
assert_eq!(law_decision_tree(&Lawful), Vec::<String>::new());
}
}