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

nordisk/edda

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