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/tree.rs

//! **The settings TREE** — one recursive tree per variant, in the owner's
//! vocabulary: **variant → features on the variant → settings / sub-settings →
//! typ**.
//!
//! ```text
//! gunnar-installer                       (variant · typ installer)
//! ├─ wireguard   (feature)   └─ wireguard.hub_conf         (setting, when on)
//! ├─ dhcp        (feature)   └─ dhcp.static_ip, dhcp.gateway   (settings, when off)
//! └─ kernel      (feature, edge → tunnr/tunnr-kernel : kernel)
//!    ├─ kernel.preset        (val: full | normal | slim | nuked)
//!    └─ kernel.<symbol>      (features, from the kernel feature model)
//! ```
//!
//! Exactly **three node kinds** and recursion — nothing else:
//!
//! | kind | what it is | today's flat spelling |
//! |---|---|---|
//! | [`FeatureNode`] | on/off with a default; decides a cargo feature (or a kconfig symbol) | `[[build.installer.options]]` row |
//! | [`SettingNode`] | a typed value ([`ParamKind`]), `@file` / `@fixture` staging, `boot_param` bake | `[[build.installer.params]]` row |
//! | [`ValNode`] | one-of with a default — `kernel.preset` is one | the `--opt kernel.preset=` special case |
//!
//! **Paths are the ids**: `dhcp.static_ip`, `kernel.preset`, `kernel.unix`. A
//! node may carry an [`Edge`] — a feature bound to a coordinate `repo/variant`
//! of a required typ ([`ArtifactKind`]); today's
//! [`InstallerBuild::kernel_donor`](crate::installer::InstallerBuild::kernel_donor)
//! is the first edge, kept as-is on the struct and PROJECTED into the tree.
//!
//! # Derived, not declared — no toml change
//!
//! The tree is derived from the flat declaration every published 0.1.x reader
//! already loads: each [`OptionSpec`] becomes a feature node, its
//! `params_when_on` / `params_when_off` become that feature's children (with
//! [`SettingsNode::when`] saying which state they belong to), the unconditional
//! [`ParamSpec`] rows become root settings, and a `kernel_donor` becomes the
//! `kernel` feature with its edge and its `preset` val. Nothing in the
//! declaration changes, so law A's round trip is untouched. The tree's own
//! serde is `default` + `skip_serializing_if` throughout, so the JSON a pane
//! or a plugin host reads stays stable as fields are added.
//!
//! # Flat spellings stay accepted — as unambiguous aliases
//!
//! `--param static_ip=` ≡ `--param dhcp.static_ip=`: [`SettingsTree::resolve`]
//! takes a full path first, then a bare last segment when exactly ONE node
//! carries it. Two nodes sharing a short name make that short name a **named
//! refusal** listing both full paths — never a silent first-match.

use std::collections::BTreeMap;

use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};

use crate::installer::InstallerBuild;
use crate::iso::IsoBuild;
use crate::kernel::{HardeningProfile, KernelBuild, Preset};
use crate::kernel_features::KernelFeatureModel;
use crate::options::{OptionBake, OptionSpec};
use crate::params::{FixtureKind, ParamKind, ParamSpec};
use crate::variant::{ArtifactKind, BuildVariant, BuildVariantSet};

/// The path separator: `kernel.preset`.
pub const PATH_SEP: char = '.';

/// The path a variant's consumed kernel lives under (`kernel`, `kernel.preset`,
/// `kernel.<symbol>`) — the one spelling both doors already use
/// (`--opt kernel.preset=nuked`, `split_kernel_opts`).
pub const KERNEL_PATH: &str = "kernel";

/// The path of a variant's ROOT preset val (`preset = airgap`) — the
/// "preconfig": a named bundle of choices over the whole tree, declared as
/// `[build.<kind>.presets.<name>]` and chosen with `--opt preset=<name>`.
pub const PRESET_PATH: &str = "preset";

/// **The `data` group's paths on the tree** — `[build.installer.data]`'s two
/// arms as VAL nodes, so the group an installer declares is spellable
/// (`--opt data.fs=xfs`), listable (`flags_of` renders every choice) and
/// renders back (law A, [`Rows::data`]).
///
/// A declared group that the tree does not carry is a group nobody can reach:
/// lane N (2026-08-30) had to flip `fs = "fat"` in a scratch worktree to build
/// a FAT medium, because `--list-options` showed `dhcp`, `wireguard` and
/// `kernel.*` and nothing else — a whole lane's detour for a value the recipe
/// already named. These two are the general fix's first users, and the rule is
/// the one the root preset val already follows: a group is a val (or a set of
/// them) at a reserved path, ignored by the option-row loop and read back BY
/// PATH.
pub const DATA_FS_PATH: &str = "data.fs";
/// The writer arm of the `data` group — see [`DATA_FS_PATH`].
pub const DATA_IO_PATH: &str = "data.io";

/// A declared bundle: path → value (`wireguard = "off"`, `"kernel.preset" =
/// "nuked"`, `dhcp.static_ip = "10.0.2.15/24"`).
pub type Bundle = BTreeMap<String, String>;

/// Every bundle a variant declares, by name (`[build.<kind>.presets.<name>]`).
pub type Presets = BTreeMap<String, Bundle>;

/// **A feature: on/off with a default.** Off means the code behind it is NOT
/// COMPILED IN (a cargo feature) or the kconfig symbol is vetoed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct FeatureNode {
    /// The state when nobody says otherwise.
    #[serde(default)]
    pub default: bool,
    /// What the toggle decides: the cargo feature of the app binary, or the
    /// `CONFIG_` symbol of a kernel feature. Empty when the feature only
    /// groups its children (a bare edge node).
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub feature: String,
    /// Symbol families that must be ABSENT from the artifact when this is off
    /// ([`OptionSpec::forbid_symbols`]).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub forbid_symbols: Vec<String>,
    /// The cmdline selector this feature bakes, per state ([`OptionSpec::bakes`]).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bakes: Option<OptionBake>,
    /// **The lock**: turning this off would break these (boot-contract kernel
    /// symbols, asked of the kernel model's own `try_toggle`). Empty ⇒ free.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub locked_by: Vec<String>,
    /// **What this feature needs of the KERNEL behind the variant's kernel
    /// edge** ([`OptionSpec::kernel_requires`]) — the requirement closure's
    /// forward rows, carried on the node that OWNS them.
    ///
    /// It is here because the tree is the MODEL and the rows are its spelling
    /// (LAW A): without it `SettingsTree::rows()` rendered an option row with
    /// an empty `kernel_requires`, so the first impl that declared one broke
    /// law A by declaring it (measured on gunnar, lane AF 2026-08-30 — the
    /// rows had never been through a law-checked impl before, because the one
    /// consumer that wanted them could not spell them).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub kernel_requires: Vec<crate::kernel_requires::KconfigReq>,
    /// The general form of the line above, addressed by edge
    /// ([`OptionSpec::requires`]). Same reason for being here.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub requires: Vec<crate::requires::EdgeRequirement>,
}

/// **A setting: a typed value.** Where it lands is exactly one of `boot_param`
/// (a kernel-cmdline token) or `staged_file` (a file on the medium, `@path` /
/// `@fixture` staging).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SettingNode {
    /// The value's declared shape — the one validator
    /// ([`ParamKind::validate_value`](crate::params::ParamSpec::validate_value)).
    #[serde(default)]
    pub param_kind: ParamKind,
    /// Whether a build without a value must refuse.
    #[serde(default)]
    pub required: bool,
    /// Landing: `<boot_param>=<value>` on the cmdline.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub boot_param: String,
    /// Landing: a staging-tree file whose contents ARE the value (`@path`).
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub staged_file: String,
    /// Where the staged file lands ([`crate::params::Landing`]): `/data` or the release.
    #[serde(default, skip_serializing_if = "crate::params::Landing::is_data")]
    pub lands: crate::params::Landing,
    /// The fixture the forge may mint for it (`@fixture`), when declared.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fixture: Option<FixtureKind>,
    /// The kind the `[[…params]]` ROW spells, when the owning feature
    /// sharpened it through `param_kinds` (`param_kind` is then the sharper
    /// one). `None` = the row's kind IS `param_kind`. Carried so the tree
    /// renders back to exactly the rows it came from (law A).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub declared_kind: Option<ParamKind>,
}

/// **A val: one-of with a default.** `kernel.preset = full | normal | slim |
/// nuked` is one; a variant-root `preset = airgap` (the "preconfig") is the
/// same node kind.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ValNode {
    /// The choices, in declaration order.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub choices: Vec<String>,
    /// The choice when nobody says otherwise. Always one of `choices` — or
    /// EMPTY for a root preset, whose honest default is "no bundle chosen".
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub default: String,
    /// **What each choice EXPANDS to**, for a preset val: choice → (path →
    /// value) over the rest of the tree. Empty for a plain val (`kernel.preset`
    /// expands inside the kernel model, not here).
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub bundles: Presets,
}

/// **The three node kinds.** Serialized with a `kind` tag (`feature` /
/// `setting` / `val`) beside the node's own fields.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum NodeKind {
    /// On/off, default.
    Feature(FeatureNode),
    /// A typed value.
    Setting(SettingNode),
    /// One-of with a default.
    Val(ValNode),
}

impl NodeKind {
    /// The tag word (`feature` / `setting` / `val`).
    pub fn word(&self) -> &'static str {
        match self {
            NodeKind::Feature(_) => "feature",
            NodeKind::Setting(_) => "setting",
            NodeKind::Val(_) => "val",
        }
    }
}

/// **A consumption edge**: this feature is bound to another build thing's
/// artifact, named by coordinate and required to be of one typ. `kernel ↔
/// tunnr/tunnr-kernel : kernel` is the first.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Edge {
    /// `repo/variant` — the donor checkout and the variant inside it.
    pub coordinate: String,
    /// The typ the consumed artifact must have. Checked at declaration time.
    pub typ: ArtifactKind,
    /// The donor checkout may be absent ([`EdgeDecl::lazy`]).
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub lazy: bool,
}

impl Edge {
    /// Build an edge from its two halves.
    pub fn new(repo: &str, variant: &str, typ: ArtifactKind) -> Self {
        Edge { coordinate: format!("{repo}/{variant}"), typ, lazy: false }
    }

    /// The edge a declaration row spells ([`EdgeDecl`]).
    pub fn from_decl(d: &EdgeDecl) -> Self {
        Edge::new(&d.repo, &d.variant, d.typ)
    }

    /// **The edge as a view spells it** — `<repo>/<variant> : <typ>`, the one
    /// line a settings tree, a step console and a refusal all print.
    pub fn spelled(&self) -> String {
        format!("{} : {}", self.coordinate, self.typ.as_str())
    }

    /// **Read [`spelled`](Self::spelled) back.** A string without the ` : ` is
    /// the coordinate alone and the typ is the kind's default — so a view
    /// holding only the string never re-decides a kind itself.
    pub fn from_spelled(s: &str) -> Self {
        match s.rsplit_once(" : ") {
            Some((c, t)) => Edge { coordinate: c.to_string(), typ: ArtifactKind::from(t.to_string()), lazy: false },
            None => Edge { coordinate: s.to_string(), typ: ArtifactKind::default(), lazy: false },
        }
    }

    /// `(repo, variant)` — the coordinate split at its one `/`.
    pub fn split(&self) -> Option<(&str, &str)> {
        self.coordinate.split_once('/')
    }

    /// The declaration row this edge renders back to.
    pub fn to_decl(&self) -> Option<EdgeDecl> {
        let (repo, variant) = self.split()?;
        Some(EdgeDecl { repo: repo.to_string(), variant: variant.to_string(), typ: self.typ, lazy: self.lazy })
    }
}

/// **A declared consumption edge** — the toml spelling of an [`Edge`] on ANY
/// variant kind: `[build.<kind>.edges.<feature>] repo = "tunnr" variant =
/// "tunnr-kernel" typ = "kernel"`. The feature name is the table key (the
/// node's path at the variant's root); `repo/variant` is the coordinate the
/// consumed artifact is built at, and `typ` is what it must BE — checked at
/// declaration time against the donor's own declaration
/// ([`BuildVariantSet::validate_edges_with`](crate::variant::BuildVariantSet::validate_edges_with)).
///
/// The kernel edge keeps its older spelling: `[build.<kind>.kernel_donor] repo /
/// kernel` ([`KernelDonor`](crate::installer::KernelDonor)) IS the `kernel`
/// edge of typ `kernel`, benign to every published 0.1.x reader. An
/// `edges.kernel` of typ kernel is refused by name — one spelling per edge.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EdgeDecl {
    /// The donor CHECKOUT's name — its directory beside the consuming repo.
    pub repo: String,
    /// The variant inside that checkout whose artifact is consumed.
    pub variant: String,
    /// The typ that artifact must have.
    pub typ: ArtifactKind,
    /// **Lazy** (`lazy = true`): the donor checkout may be ABSENT. An open product
    /// whose one sold row consumes a guest built by a commercial checkout must
    /// load, list and build its other rows in a tree without that checkout
    /// (holger, owner rule: the open build never needs monetize-impl). A lazy
    /// edge is type-checked like any other when its donor is beside the consumer
    /// and left unresolved at load when it is not; building the row that carries
    /// it still resolves the donor, and refuses by name without it. Unspelled
    /// when false, so every eager edge keeps its old toml.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub lazy: bool,
}

impl EdgeDecl {
    /// `repo/variant`.
    pub fn coordinate(&self) -> String {
        format!("{}/{}", self.repo, self.variant)
    }

    /// The declaration's own shape: both halves named, a typ that is a
    /// consumable artifact. `who` prefixes the refusal (`iso 'x'`),
    /// `feature` is the table key.
    pub fn validate(&self, who: &str, feature: &str) -> Result<()> {
        if feature.trim().is_empty() || feature.contains(PATH_SEP) || feature.chars().any(char::is_whitespace) {
            bail!("{who}: edge {feature:?}: an edge is a ROOT feature — one path segment, no '.', no whitespace");
        }
        if self.repo.trim().is_empty() {
            bail!("{who}: edges.{feature}.repo is empty — the edge names WHICH checkout declares the consumed variant");
        }
        if self.variant.trim().is_empty() {
            bail!(
                "{who}: edges.{feature}.variant is empty — the edge names WHICH variant in '{}' is consumed",
                self.repo
            );
        }
        if feature == KERNEL_PATH && self.typ == ArtifactKind::Kernel {
            bail!(
                "{who}: edges.kernel of typ kernel is the kernel donor — spell it once, as \
                 [build.<kind>.kernel_donor] repo = \"{}\" kernel = \"{}\"",
                self.repo,
                self.variant
            );
        }
        Ok(())
    }
}

/// **A consumption edge, WHOLE** — the feature that owns the need together
/// with the coordinate and the typ it needs. [`EdgeDecl`] is the toml ROW, in
/// which the feature is the table key and therefore not on the struct; this is
/// the row and its key in one value, and it is the shape every reader of "what
/// does this variant consume" is handed ([`declared_edges`],
/// [`BuildVariantSet::consumption_edges`](crate::variant::BuildVariantSet::consumption_edges)).
///
/// The two spellings — `[build.<kind>.kernel_donor]` (the `kernel` edge of typ
/// kernel) and `[build.<kind>.edges.<feature>]` (any feature, any typ) — both
/// arrive here, so a consumer of this type never learns which one was written:
/// the general edge and the kernel donor are ONE mechanism with two spellings,
/// and `kernel_donor` is the older one kept because published readers know it.
///
/// **Types match at DECLARATION time.** A consumer naming a donor variant of
/// another typ is refused when the declaration is loaded
/// ([`wrong_typ`] is the one refusal, shared with the DAG's walker) — not at
/// compose time, not at run time, for the same reason
/// [`KernelDonor::validate`](crate::installer::KernelDonor::validate) refuses
/// an empty half at declaration time: a later error would be true words about
/// the wrong defect.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsumptionEdge {
    /// The feature path in the CONSUMER's tree that owns this need ("registry").
    /// Next to the feature, per the model: a requirement is a property of the
    /// feature that needs it, not of the repo.
    pub feature: String,
    /// The donor CHECKOUT's name — its directory beside the consuming repo.
    pub repo: String,
    /// The variant name inside that checkout ("mvp-server-musl").
    pub variant: String,
    /// The typ both sides must agree on ("kernel", "server", "container", …).
    pub typ: String,
    /// The donor checkout may be absent ([`EdgeDecl::lazy`]).
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub lazy: bool,
}

impl ConsumptionEdge {
    /// The edge a declaration row spells, with its table key.
    pub fn from_decl(feature: impl Into<String>, d: &EdgeDecl) -> Self {
        ConsumptionEdge {
            feature: feature.into(),
            repo: d.repo.clone(),
            variant: d.variant.clone(),
            typ: d.typ.as_str().to_string(),
            lazy: d.lazy,
        }
    }

    /// All four halves given, the typ as its canonical word.
    pub fn new(feature: impl Into<String>, repo: impl Into<String>, variant: impl Into<String>, typ: ArtifactKind) -> Self {
        ConsumptionEdge {
            feature: feature.into(),
            repo: repo.into(),
            variant: variant.into(),
            typ: typ.as_str().to_string(),
            lazy: false,
        }
    }

    /// The typ as the enum — the word parsed back. An unknown word reads as the
    /// [`ArtifactKind`] default, exactly as a `kind = "…"` field does, so the
    /// two roads into a declaration cannot disagree about what a word means.
    pub fn kind(&self) -> ArtifactKind {
        ArtifactKind::from(self.typ.clone())
    }

    /// `repo/variant`.
    pub fn coordinate(&self) -> String {
        format!("{}/{}", self.repo, self.variant)
    }

    /// The toml row this edge renders back to (the feature becomes the key).
    pub fn decl(&self) -> EdgeDecl {
        EdgeDecl { repo: self.repo.clone(), variant: self.variant.clone(), typ: self.kind(), lazy: self.lazy }
    }

    /// The tree-node form: a coordinate and the typ required of it.
    pub fn edge(&self) -> Edge {
        Edge { lazy: self.lazy, ..Edge::new(&self.repo, &self.variant, self.kind()) }
    }

    /// The declaration's own shape ([`EdgeDecl::validate`]), the feature
    /// included — `who` prefixes the refusal (`installer 'gunnar-installer'`).
    pub fn validate(&self, who: &str) -> Result<()> {
        self.decl().validate(who, &self.feature)
    }

    /// `installer 'x': edge 'registry' → holger/mvp-server-musl : exe` — the
    /// prefix every refusal about this edge carries.
    pub fn who(&self, kind_word: &str, variant: &str) -> String {
        format!("{kind_word} '{variant}': edge '{}' → {} : {}", self.feature, self.coordinate(), self.typ)
    }

    /// **The typ check**, against what the donor's own declaration says that
    /// variant IS. `declared` is [`BuildVariantSet::kind_of`](crate::variant::BuildVariantSet::kind_of)
    /// of the donor's set. Ok when they agree; [`wrong_typ`] otherwise.
    pub fn check_typ(&self, who: &str, declared: ArtifactKind) -> Result<()> {
        if declared == self.kind() {
            Ok(())
        } else {
            Err(wrong_typ(who, &self.repo, &self.variant, declared, self.kind()))
        }
    }
}

/// **The ONE refusal for a typ that does not match** — consumer (in `who`),
/// feature (in `who`), the donor coordinate and BOTH typs. Shared by the
/// declaration-time check
/// ([`BuildVariantSet::validate_edges_with`](crate::variant::BuildVariantSet::validate_edges_with))
/// and the DAG's walker ([`crate::build_dag::BuildDag::derive`]), so the two
/// roads into a build cannot phrase the same defect two ways.
pub fn wrong_typ(who: &str, repo: &str, variant: &str, declared: ArtifactKind, required: ArtifactKind) -> anyhow::Error {
    anyhow::anyhow!(
        "{who}: '{repo}' declares '{variant}' as typ {}, not {} — the edge's typ must be what the \
         donor builds",
        declared.as_str(),
        required.as_str()
    )
}

/// **The preset a build was made with** — recorded on the variant
/// (`applied_preset`) and in the receipt: the bundle's name AND the flags it
/// expanded to, so the descriptor says both what was chosen and what that
/// meant on the day (law D's shape: decision ↔ its whole consequence).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AppliedPreset {
    /// The bundle's name (`airgap`).
    pub bundle: String,
    /// The rendered flags the bundle expanded to, in bundle order
    /// (`--opt wireguard=off`, `--opt kernel.preset=nuked`, …).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub expanded: Vec<String>,
}

impl AppliedPreset {
    /// One line for a human — the CLI's stderr and the pane's receipt.
    pub fn describe(&self) -> String {
        format!("preset = {} → {}", self.bundle, self.expanded.join(" · "))
    }
}

/// **What expanding a variant's `--opt` / `--param` line produced** — the
/// bundle (when `preset=<name>` was typed) plus the flags with the bundle's
/// choices folded in: an explicit flag on the line WINS over the bundle's
/// (the bundle is the defaults the operator starts from, exactly as a kernel
/// preset is the defaults its symbol rows start from).
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Expanded {
    /// `--opt` pairs, `name=value`, bundle choices first, explicit after.
    pub opts: Vec<String>,
    /// `--param` pairs, `name=value`, same order rule.
    pub params: Vec<String>,
    /// The bundle, when one was chosen.
    pub preset: Option<AppliedPreset>,
}

/// **Declaration-time shape check over a variant's edge rows** — every
/// `[…edges.<feature>]` well-formed ([`EdgeDecl::validate`]), no feature
/// declared twice (as an edge AND an option), and no `edges.kernel` beside a
/// `kernel_donor`. The TYPE check against the donor's own declaration is the
/// set's ([`BuildVariantSet::validate_edges_with`](crate::variant::BuildVariantSet::validate_edges_with)),
/// because it needs the donor checkout.
pub fn validate_edge_rows(
    who: &str,
    kernel_donor: Option<&crate::installer::KernelDonor>,
    edges: &BTreeMap<String, EdgeDecl>,
    options: &[OptionSpec],
) -> Result<()> {
    for (feature, e) in edges {
        e.validate(who, feature)?;
        if feature == KERNEL_PATH && kernel_donor.is_some() {
            bail!(
                "{who}: declares BOTH [kernel_donor] and [edges.kernel] — one kernel edge per \
                 variant; the donor row is its spelling"
            );
        }
        if let Some(o) = options.iter().find(|o| &o.name == feature) {
            bail!(
                "{who}: '{feature}' is declared twice — as an edge → {} and as an option \
                 deciding cargo feature {:?}; a feature is one or the other",
                e.coordinate(),
                o.feature
            );
        }
    }
    if let Some(o) = kernel_donor.and(options.iter().find(|o| o.name == KERNEL_PATH)) {
        bail!(
            "{who}: '{}' is declared twice — as the kernel donor edge and as an option deciding \
             cargo feature {:?}",
            KERNEL_PATH,
            o.feature
        );
    }
    Ok(())
}

/// Every edge declaration a variant carries, in one shape: the `kernel_donor`
/// row (the `kernel` edge of typ kernel) first, then `[…edges.<feature>]` in
/// key order. The one place the two spellings meet.
pub fn declared_edges(
    kernel_donor: Option<&crate::installer::KernelDonor>,
    edges: &BTreeMap<String, EdgeDecl>,
) -> Vec<ConsumptionEdge> {
    let mut out = Vec::with_capacity(edges.len() + 1);
    if let Some(d) = kernel_donor {
        out.push(ConsumptionEdge::new(KERNEL_PATH, &d.repo, &d.kernel, ArtifactKind::Kernel));
    }
    for (f, e) in edges {
        out.push(ConsumptionEdge::from_decl(f.clone(), e));
    }
    out
}

/// **One node of the tree.** Its id is [`path`](Self::path).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsNode {
    /// The full dotted path — the id: `dhcp.static_ip`.
    pub path: String,
    /// What a human is shown. Falls back to the last path segment when empty.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub label: String,
    /// Which of the three kinds, with that kind's data.
    #[serde(flatten)]
    pub kind: NodeKind,
    /// For a child of a feature: the parent state this node is ACTIVE in
    /// (`Some(true)` = `params_when_on`, `Some(false)` = `params_when_off`).
    /// `None` = always active.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub when: Option<bool>,
    /// The consumption edge this node is bound to, if any.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub edge: Option<Edge>,
    /// Sub-settings.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub children: Vec<SettingsNode>,
}

impl SettingsNode {
    /// A node with no children, no edge, always active.
    pub fn leaf(path: impl Into<String>, label: impl Into<String>, kind: NodeKind) -> Self {
        SettingsNode {
            path: path.into(),
            label: label.into(),
            kind,
            when: None,
            edge: None,
            children: Vec::new(),
        }
    }

    /// The last path segment — the short name (`static_ip` of `dhcp.static_ip`).
    pub fn name(&self) -> &str {
        self.path.rsplit(PATH_SEP).next().unwrap_or(&self.path)
    }

    /// What a human is shown — the label, or the short name.
    pub fn display_label(&self) -> &str {
        if self.label.trim().is_empty() { self.name() } else { &self.label }
    }

    /// Every node of this subtree, pre-order, self first.
    pub fn walk(&self) -> Vec<&SettingsNode> {
        let mut out = Vec::with_capacity(1 + self.children.len());
        self.visit(&mut |n| out.push(n));
        out
    }

    /// Pre-order visit without allocating — the walk the hot paths use.
    pub fn visit<'a>(&'a self, f: &mut dyn FnMut(&'a SettingsNode)) {
        f(self);
        for c in &self.children {
            c.visit(f);
        }
    }

    /// The node at `path` inside this subtree (exact full path).
    pub fn get(&self, path: &str) -> Option<&SettingsNode> {
        if self.path == path {
            return Some(self);
        }
        if !path.starts_with(&self.path) {
            return None;
        }
        self.children.iter().find_map(|c| c.get(path))
    }

    /// Re-root: prefix every path in the subtree with `prefix.` (used to
    /// project a donor kernel's own tree under `kernel.`).
    pub fn prefixed(mut self, prefix: &str) -> Self {
        fn go(n: &mut SettingsNode, prefix: &str) {
            n.path = format!("{prefix}{PATH_SEP}{}", n.path);
            for c in &mut n.children {
                go(c, prefix);
            }
        }
        go(&mut self, prefix);
        self
    }
}

/// **One variant's whole settings tree.**
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SettingsTree {
    /// The variant this tree belongs to.
    pub variant: String,
    /// The variant's typ — what the artifact IS, which decides its verb.
    #[serde(default)]
    pub typ: ArtifactKind,
    /// The top-level nodes, in declaration order.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub roots: Vec<SettingsNode>,
}

/// What [`SettingsTree::resolve`] answered: the node, and whether the caller's
/// spelling was the full path or a short alias.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved<'a> {
    /// The node.
    pub node: &'a SettingsNode,
    /// `true` when the spelling was a bare short name that resolved uniquely.
    pub via_alias: bool,
}

impl SettingsTree {
    /// Every node, pre-order.
    pub fn walk(&self) -> Vec<&SettingsNode> {
        let mut out = Vec::new();
        self.visit(&mut |n| out.push(n));
        out
    }

    /// Pre-order visit of every node without allocating.
    pub fn visit<'a>(&'a self, f: &mut dyn FnMut(&'a SettingsNode)) {
        for r in &self.roots {
            r.visit(f);
        }
    }

    /// How many nodes the tree holds.
    pub fn len(&self) -> usize {
        let mut n = 0;
        self.visit(&mut |_| n += 1);
        n
    }

    /// Whether the tree holds no node at all.
    pub fn is_empty(&self) -> bool {
        self.roots.is_empty()
    }

    /// Every full path, pre-order.
    pub fn paths(&self) -> Vec<&str> {
        self.walk().into_iter().map(|n| n.path.as_str()).collect()
    }

    /// The node at an exact full path.
    pub fn get(&self, path: &str) -> Option<&SettingsNode> {
        self.roots.iter().find_map(|r| r.get(path))
    }

    /// The subtree rooted at `path` (`""` = the whole tree as-is).
    pub fn subtree(&self, path: &str) -> Option<SettingsTree> {
        if path.trim().is_empty() {
            return Some(self.clone());
        }
        self.get(path).map(|n| SettingsTree {
            variant: self.variant.clone(),
            typ: self.typ,
            roots: vec![n.clone()],
        })
    }

    /// **Resolve a spelling to a node** — full path first, then the short
    /// name as an unambiguous alias. A short name two nodes carry is REFUSED
    /// by name, listing both paths; an unknown spelling is refused listing
    /// what is declared.
    pub fn resolve(&self, spelling: &str) -> Result<Resolved<'_>> {
        let s = spelling.trim();
        if s.is_empty() {
            bail!("an empty settings path");
        }
        if let Some(node) = self.get(s) {
            return Ok(Resolved { node, via_alias: false });
        }
        // The alias scan: one pass, no intermediate Vec of every node — only
        // the hits are collected (normally zero or one).
        let mut hits: Vec<&SettingsNode> = Vec::new();
        self.visit(&mut |n| {
            if n.name() == s {
                hits.push(n);
            }
        });
        match hits.len() {
            1 => Ok(Resolved { node: hits[0], via_alias: true }),
            0 => bail!(
                "'{s}' is not a setting of variant '{}' (declared: {})",
                self.variant,
                self.declared_list()
            ),
            _ => bail!(
                "'{s}' is AMBIGUOUS in variant '{}' — {} nodes carry that name; spell the full \
                 path: {}",
                self.variant,
                hits.len(),
                hits.iter().map(|n| n.path.as_str()).collect::<Vec<_>>().join(", ")
            ),
        }
    }

    /// The full path a spelling resolves to (see [`resolve`](Self::resolve)).
    pub fn canonical_path(&self, spelling: &str) -> Result<String> {
        Ok(self.resolve(spelling)?.node.path.clone())
    }

    fn declared_list(&self) -> String {
        let p = self.paths();
        if p.is_empty() { "none".to_string() } else { p.join(", ") }
    }

    // ── derivation from today's flat declaration ─────────────────────────────

    /// **The tree of an installer** — options become features with their
    /// conditional params as children, unconditional params become root
    /// settings, and the `kernel_donor` becomes the `kernel` feature with its
    /// edge. `donor` is the donor's own `[[build.kernel]]` row when the caller
    /// has it (same checkout, or resolved beside it); without it the kernel
    /// subtree starts from the `normal` / `vanilla` pick.
    pub fn of_installer(inst: &InstallerBuild, donor: Option<&KernelBuild>) -> Self {
        let mut roots = option_and_param_nodes(&inst.options, &inst.params, &inst.fixtures, "");
        roots.extend(edge_nodes(inst.kernel_donor.as_ref(), &inst.edges, donor));
        roots.extend(data_nodes(&inst.data));
        roots.extend(preset_node(&inst.presets));
        SettingsTree { variant: inst.name.clone(), typ: ArtifactKind::Installer, roots }
    }

    /// **The tree of an ISO** — its options as features, its params as
    /// settings, and its edges (`kernel_donor` → the `kernel` feature with the
    /// donor's subtree, `[build.iso.edges.<f>]` → a bare edge feature). `donor`
    /// as in [`of_installer`](Self::of_installer).
    pub fn of_iso(iso: &IsoBuild, donor: Option<&KernelBuild>) -> Self {
        let mut roots = option_and_param_nodes(&iso.options, &iso.params, &iso.fixtures, "");
        roots.extend(edge_nodes(iso.kernel_donor.as_ref(), &iso.edges, donor));
        roots.extend(preset_node(&iso.presets));
        SettingsTree { variant: iso.name.clone(), typ: ArtifactKind::Iso, roots }
    }

    /// **The tree of bare rows** — what the option resolver builds so that every
    /// decision about `--opt` / `--param` is answered by the tree and never by a
    /// second walk over the rows.
    pub fn of_rows(
        variant: &str,
        typ: ArtifactKind,
        options: &[OptionSpec],
        params: &[ParamSpec],
        fixtures: &BTreeMap<String, FixtureKind>,
    ) -> Self {
        SettingsTree {
            variant: variant.to_string(),
            typ,
            roots: option_and_param_nodes(options, params, fixtures, ""),
        }
    }

    /// **The tree of a kernel pick, BARE** — `preset` / `profile` vals and a
    /// feature per symbol at the root, with no `kernel.` scope on the paths.
    ///
    /// This is the INTERNAL JUDGE of the kernel grammar, and its names are the
    /// ones [`split_kernel_opts`](crate::kernel_features::split_kernel_opts)
    /// hands it — already stripped of the scope. It is NOT what a door renders:
    /// the tree of a declared kernel row ([`of_kernel`](Self::of_kernel)) puts
    /// the same nodes under `kernel`, because that is the spelling every door
    /// ACCEPTS. Keeping the judge bare and the door scoped is what lets one
    /// string, `--opt kernel.preset=slim`, work at the donor's door and at a
    /// consumer's alike.
    pub fn of_kernel_pick(variant: &str, preset: Preset, profile: HardeningProfile) -> Self {
        SettingsTree { variant: variant.to_string(), typ: ArtifactKind::Kernel, roots: kernel_nodes(preset, profile, "") }
    }

    /// **The tree of a kernel** — `preset` and `profile` vals, then one feature
    /// per symbol of the pick (from the shared kernel feature model, lock and
    /// all). An unparseable preset/profile falls back to `normal` / `vanilla`
    /// — [`validate`](KernelBuild::validate) refuses those by name; the tree
    /// is a view, not a second gate.
    pub fn of_kernel(k: &KernelBuild) -> Self {
        let preset = k.preset().unwrap_or(Preset::Normal);
        let profile = k.hardening_profile().unwrap_or(HardeningProfile::Vanilla);
        // UNDER THE `kernel` PREFIX (lane AK, 2026-08-30), the same as the
        // consumer-side nesting (`of_installer` gives the edge node exactly
        // these children). It used to be bare, and that is what made
        // `--list-options` print a spelling the CLI refuses: the listing renders
        // the tree's own flags (law C), so it said `--opt preset=slim`, and the
        // resolver answered `no preset 'slim' on 'tunnr-kernel'; declared
        // bundles: full · normal · slim · nuked` — a refusal that lists `slim`
        // while saying there is no `slim`. It cost lane AH a wrong turn. ONE
        // spelling at both doors, `kernel.preset=slim`: what the CLI has always
        // accepted, what a consumer routes down the edge unchanged
        // (`build_steps::route_opts` passes `kernel.*` through), and now what
        // the listing prints. The row's own EDGES keep their bare paths:
        // `--build toolchain` is this door's spelling of the edge a consumer
        // reaches as `kernel.toolchain`.
        let mut roots = kernel_nodes(preset, profile, KERNEL_PATH);
        roots.extend(edge_nodes(None, &k.edges, None));
        SettingsTree { variant: k.name.clone(), typ: ArtifactKind::Kernel, roots }
    }

    /// **The tree of a cargo variant** — its switchable axis: every `open`
    /// feature is a feature node (default off). The row's `features` are its
    /// identity, not a setting (an installer option that defaults ON is what
    /// makes one of them switchable — `OptionSpec::validate_against_variant`).
    pub fn of_variant(v: &BuildVariant) -> Self {
        let mut roots: Vec<SettingsNode> = v
            .open
            .iter()
            .map(|f| {
                SettingsNode::leaf(
                    f.clone(),
                    "",
                    NodeKind::Feature(FeatureNode { default: false, feature: f.clone(), ..Default::default() }),
                )
            })
            .collect();
        roots.extend(edge_nodes(None, &v.edges, None));
        roots.extend(preset_node(&v.presets));
        SettingsTree { variant: v.name.clone(), typ: v.kind, roots }
    }

    /// **The tree of any declared variant of a set**, by name, or `None` when
    /// the set declares no such variant. An installer's donor kernel is looked
    /// up in the SAME set when the donor repo is this one (a self-donor); a
    /// donor in another checkout is projected from the default pick — the
    /// impl's `options("kernel")` (stage 2) is where a fetched donor answers.
    pub fn of_set(set: &BuildVariantSet, variant: &str) -> Option<Self> {
        if let Some(inst) = set.installers.iter().find(|i| i.name == variant) {
            let donor = inst
                .kernel_donor
                .as_ref()
                .and_then(|d| set.kernels.iter().find(|k| k.name == d.kernel));
            return Some(Self::of_installer(inst, donor));
        }
        if let Some(iso) = set.isos.iter().find(|i| i.name == variant) {
            let donor = iso
                .kernel_donor
                .as_ref()
                .and_then(|d| set.kernels.iter().find(|k| k.name == d.kernel));
            return Some(Self::of_iso(iso, donor));
        }
        if let Some(k) = set.kernels.iter().find(|k| k.name == variant) {
            return Some(Self::of_kernel(k));
        }
        // A container's / vm's tree is its edges alone — the same edge every
        // kind carries (design f3), so `--build <edge>` resolves against them too.
        //
        // These two arms sit ABOVE the cargo-variant arm, and that is load-bearing:
        // a `kind = "container"` variant is required to carry a `container_ref`
        // (`validate_kind`), so the pair `[[build.variant]] name = "X"` +
        // `[[build.container]] name = "X"` is the normal shape. With the variant arm
        // first, `of_set("X")` answered `of_variant` — a cargo tree with no edges —
        // and the container's declared edge was invisible to `--build`, to the DAG
        // and to the step console. Not refused, not built, silently absent.
        //
        // ⚠ TWIN, deliberately named rather than left to be found: this dispatch
        // order and `BuildVariantSet::edges_of`'s are the same rule written twice,
        // and they were BOTH wrong in the same way. `delegating_and_tree_agree_on_a_containers_edges`
        // in variant.rs fails if they drift apart again.
        // The descriptor owns the EDGES; the paired variant row still owns its
        // switchable axis (`open` features, presets). Dropping those made a
        // container that leaves `gpu` open draw no gpu control at all whenever the
        // tree was derived from the declaration (no `<repo>-build-thing` crate).
        // A delegating variant declaring edges of its own is refused at load, so
        // taking its non-edge nodes cannot double an edge.
        if let Some(c) = set.containers.iter().find(|c| c.name == variant) {
            let mut roots = paired_axis(set, &c.name);
            roots.extend(edge_nodes(None, &c.edges, None));
            return Some(SettingsTree { variant: c.name.clone(), typ: ArtifactKind::Container, roots });
        }
        if let Some(vm) = set.vms.iter().find(|v| v.name == variant) {
            let mut roots = paired_axis(set, &vm.name);
            roots.extend(edge_nodes(None, &vm.edges, None));
            return Some(SettingsTree { variant: vm.name.clone(), typ: ArtifactKind::Vm, roots });
        }
        if let Some(v) = set.variants.iter().find(|v| v.name == variant) {
            return Some(Self::of_variant(v));
        }
        None
    }

    /// Every declared variant's tree, in declaration order (cargo rows, then
    /// containers, vms, isos, installers, kernels).
    pub fn forest(set: &BuildVariantSet) -> Vec<Self> {
        let names: Vec<String> = set
            .variants
            .iter()
            .map(|v| v.name.clone())
            .chain(set.containers.iter().map(|c| c.name.clone()))
            .chain(set.vms.iter().map(|v| v.name.clone()))
            .chain(set.isos.iter().map(|i| i.name.clone()))
            .chain(set.installers.iter().map(|i| i.name.clone()))
            .chain(set.kernels.iter().map(|k| k.name.clone()))
            .collect();
        names.iter().filter_map(|n| Self::of_set(set, n)).collect()
    }
}

/// The switchable axis of the `[[build.variant]]` row a delegating descriptor
/// (`[[build.container]]` / `[[build.vm]]`) pairs with — by name, or by the row's
/// `container_ref` / `vm_ref` — WITHOUT its edges (the descriptor owns those).
fn paired_axis(set: &BuildVariantSet, descriptor: &str) -> Vec<SettingsNode> {
    set.variants
        .iter()
        .find(|v| v.name == descriptor || v.container_ref.as_deref() == Some(descriptor) || v.vm_ref.as_deref() == Some(descriptor))
        .map(|v| SettingsTree::of_variant(v).roots.into_iter().filter(|n| n.edge.is_none()).collect())
        .unwrap_or_default()
}

/// A setting node from a param row, with the sharper kind an option declared.
fn setting_node(
    p: &ParamSpec,
    sharpen: Option<ParamKind>,
    fixtures: &BTreeMap<String, FixtureKind>,
    prefix: &str,
    when: Option<bool>,
) -> SettingsNode {
    let path = if prefix.is_empty() { p.name.clone() } else { format!("{prefix}{PATH_SEP}{}", p.name) };
    let mut n = SettingsNode::leaf(
        path,
        p.label.clone(),
        NodeKind::Setting(SettingNode {
            param_kind: sharpen.unwrap_or(p.kind),
            required: p.required,
            boot_param: p.boot_param.clone(),
            staged_file: p.staged_file.clone(),
            lands: p.lands,
            fixture: fixtures.get(&p.name).copied(),
            declared_kind: sharpen.map(|_| p.kind),
        }),
    );
    n.when = when;
    n
}

/// Options → feature nodes with their conditional params as children;
/// unconditional params → root settings. Declaration order throughout.
fn option_and_param_nodes(
    options: &[OptionSpec],
    params: &[ParamSpec],
    fixtures: &BTreeMap<String, FixtureKind>,
    prefix: &str,
) -> Vec<SettingsNode> {
    let mut out = Vec::with_capacity(options.len() + params.len());
    let by_name: BTreeMap<&str, &ParamSpec> = params.iter().map(|p| (p.name.as_str(), p)).collect();
    let mut owned: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
    for o in options {
        let path = if prefix.is_empty() { o.name.clone() } else { format!("{prefix}{PATH_SEP}{}", o.name) };
        let mut node = SettingsNode::leaf(
            path.clone(),
            o.label.clone(),
            NodeKind::Feature(FeatureNode {
                default: o.default,
                feature: o.feature.clone(),
                forbid_symbols: o.forbid_symbols.clone(),
                bakes: o.bakes.clone(),
                locked_by: Vec::new(),
                kernel_requires: o.kernel_requires.clone(),
                requires: o.requires.clone(),
            }),
        );
        for (state, names) in [(true, &o.params_when_on), (false, &o.params_when_off)] {
            for name in names {
                let Some(p) = by_name.get(name.as_str()) else { continue };
                owned.insert(name.as_str());
                node.children.push(setting_node(
                    p,
                    o.param_kinds.get(name).copied(),
                    fixtures,
                    &path,
                    Some(state),
                ));
            }
        }
        out.push(node);
    }
    for p in params {
        if owned.contains(p.name.as_str()) {
            continue;
        }
        out.push(setting_node(p, None, fixtures, prefix, None));
    }
    out
}

/// **An index over a tree for hot loops** — `path → node` and `short name →
/// paths`, built once with one visit; [`get`](Self::get) and
/// [`resolve`](Self::resolve) are then O(1) / O(hits) instead of a visit per
/// call. The laws and any pane that resolves thousands of spellings use it;
/// a single `--opt` goes through [`SettingsTree::resolve`] directly.
pub struct SettingsIndex<'a> {
    tree: &'a SettingsTree,
    by_path: std::collections::HashMap<&'a str, &'a SettingsNode>,
    by_name: std::collections::HashMap<&'a str, Vec<&'a SettingsNode>>,
}

impl<'a> SettingsIndex<'a> {
    /// Build the index — one visit, two maps.
    pub fn of(tree: &'a SettingsTree) -> Self {
        let n = tree.len();
        let mut by_path = std::collections::HashMap::with_capacity(n);
        let mut by_name: std::collections::HashMap<&'a str, Vec<&'a SettingsNode>> =
            std::collections::HashMap::with_capacity(n);
        tree.visit(&mut |node| {
            by_path.insert(node.path.as_str(), node);
            by_name.entry(node.name()).or_default().push(node);
        });
        SettingsIndex { tree, by_path, by_name }
    }

    /// The node at an exact full path.
    pub fn get(&self, path: &str) -> Option<&'a SettingsNode> {
        self.by_path.get(path).copied()
    }

    /// The same answer as [`SettingsTree::resolve`], from the index.
    pub fn resolve(&self, spelling: &str) -> Result<Resolved<'a>> {
        let s = spelling.trim();
        if s.is_empty() {
            bail!("an empty settings path");
        }
        if let Some(node) = self.get(s) {
            return Ok(Resolved { node, via_alias: false });
        }
        match self.by_name.get(s).map(Vec::as_slice) {
            Some([one]) => Ok(Resolved { node: one, via_alias: true }),
            Some(many) if many.len() > 1 => bail!(
                "'{s}' is AMBIGUOUS in variant '{}' — {} nodes carry that name; spell the full path: {}",
                self.tree.variant,
                many.len(),
                many.iter().map(|n| n.path.as_str()).collect::<Vec<_>>().join(", ")
            ),
            _ => bail!(
                "'{s}' is not a setting of variant '{}' (declared: {})",
                self.tree.variant,
                self.tree.declared_list()
            ),
        }
    }
}

/// **The edge nodes of a variant** — one root feature per declared edge
/// ([`declared_edges`]): the `kernel` edge carries the donor's own subtree
/// (`preset` / `profile` vals and the pick's symbols — from `donor` when the
/// caller has the donor's `[[build.kernel]]` row, else the `normal` /
/// `vanilla` pick); every other edge is a bare feature (default on, no cargo
/// feature of its own) whose subtree is DELEGATED — the consumed build thing's
/// `options(path)` is where it says more.
pub fn edge_nodes(
    kernel_donor: Option<&crate::installer::KernelDonor>,
    edges: &BTreeMap<String, EdgeDecl>,
    donor: Option<&KernelBuild>,
) -> Vec<SettingsNode> {
    declared_edges(kernel_donor, edges)
        .into_iter()
        .map(|e| {
            let feature = e.feature.clone();
            let mut node = SettingsNode::leaf(
                feature.clone(),
                if feature == KERNEL_PATH { "Kernel".to_string() } else { String::new() },
                NodeKind::Feature(FeatureNode { default: true, ..Default::default() }),
            );
            node.edge = Some(e.edge());
            if feature == KERNEL_PATH && e.kind() == ArtifactKind::Kernel {
                let (preset, profile) = donor
                    .map(|k| {
                        (
                            k.preset().unwrap_or(Preset::Normal),
                            k.hardening_profile().unwrap_or(HardeningProfile::Vanilla),
                        )
                    })
                    .unwrap_or((Preset::Normal, HardeningProfile::Vanilla));
                node.children = kernel_nodes(preset, profile, KERNEL_PATH);
            }
            node
        })
        .collect()
}

/// **The root preset val** of a variant that declares bundles: choices = the
/// bundle names in key order, default EMPTY (no bundle chosen), each bundle
/// carried on the node so a pane can show what a choice means. `None` when
/// the variant declares no presets — the node does not exist then.
pub fn preset_node(presets: &Presets) -> Option<SettingsNode> {
    if presets.is_empty() {
        return None;
    }
    Some(SettingsNode::leaf(
        PRESET_PATH,
        "Preset",
        NodeKind::Val(ValNode { choices: presets.keys().cloned().collect(), default: String::new(), bundles: presets.clone() }),
    ))
}

/// **The `data` group as val nodes** — `data.fs` and `data.io`, each with its
/// own roster of choices and the DECLARED arm as the default. Always present on
/// an installer's tree, declared or not: an undeclared group is at its type
/// default and an operator must be able to SEE that it is (and move it), which
/// is the difference between "recipe-only" and "invisible".
pub fn data_nodes(data: &crate::data::Data) -> Vec<SettingsNode> {
    vec![
        SettingsNode::leaf(
            DATA_FS_PATH,
            "/data filesystem",
            NodeKind::Val(ValNode {
                choices: crate::data::DataFs::ALL.iter().map(|f| f.as_str().to_string()).collect(),
                default: data.fs.as_str().to_string(),
                bundles: Presets::new(),
            }),
        ),
        SettingsNode::leaf(
            DATA_IO_PATH,
            "/data writer arm",
            NodeKind::Val(ValNode {
                choices: crate::data::DataIo::ALL.iter().map(|a| a.as_str().to_string()).collect(),
                default: data.io.as_str().to_string(),
                bundles: Presets::new(),
            }),
        ),
    ]
}

/// The kernel subtree: `preset` + `profile` vals, then a feature per symbol.
fn kernel_nodes(preset: Preset, profile: HardeningProfile, prefix: &str) -> Vec<SettingsNode> {
    let at = |name: &str| if prefix.is_empty() { name.to_string() } else { format!("{prefix}{PATH_SEP}{name}") };
    let mut out = vec![
        SettingsNode::leaf(
            at("preset"),
            "Preset",
            NodeKind::Val(ValNode {
                choices: Preset::ALL.iter().map(|p| p.as_str().to_string()).collect(),
                default: preset.as_str().to_string(),
                bundles: Presets::new(),
            }),
        ),
        SettingsNode::leaf(
            at("profile"),
            "Hardening profile",
            NodeKind::Val(ValNode {
                choices: HardeningProfile::ALL.iter().map(|p| p.as_str().to_string()).collect(),
                default: profile.as_str().to_string(),
                bundles: Presets::new(),
            }),
        ),
        // The DEV probe kernel (`kernel.coverage=gcov`): the coverage half of
        // the measured kernel trim. Default off; refused on a release medium.
        SettingsNode::leaf(
            at(crate::kernel_trim::coverage::COVERAGE_OPT),
            "Coverage probe (DEV only)",
            NodeKind::Val(ValNode {
                choices: crate::kernel_trim::coverage::KernelProbe::CHOICES.iter().map(|c| c.to_string()).collect(),
                default: "off".to_string(),
                bundles: Presets::new(),
            }),
        ),
    ];
    let model = KernelFeatureModel::derive(preset, profile);
    for row in model.option_rows() {
        out.push(SettingsNode::leaf(
            at(&row.option),
            row.symbol.clone(),
            NodeKind::Feature(FeatureNode {
                default: row.default_on,
                feature: row.symbol.clone(),
                forbid_symbols: Vec::new(),
                bakes: None,
                locked_by: row.locked_by.clone(),
                // A kernel symbol node is the DONOR's own axis, not a consumer
                // feature: it requires nothing of anything behind an edge.
                kernel_requires: Vec::new(),
                requires: Vec::new(),
            }),
        ));
    }
    out
}

// ── the rows: the serde spelling of the tree ─────────────────────────────────

/// **The toml rows a tree renders back to** — [`OptionSpec`] / [`ParamSpec`] /
/// fixtures / the kernel donor are the on-disk SPELLING of the tree, nothing
/// more; this is the render half of the bijection law A proves.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Rows {
    /// `[[…options]]`, in root order.
    pub options: Vec<OptionSpec>,
    /// `[[…params]]`, sorted by name (the declaration's interleaving is not
    /// a decision and is not kept).
    pub params: Vec<ParamSpec>,
    /// `[…fixtures]`.
    pub fixtures: BTreeMap<String, FixtureKind>,
    /// `[…kernel_donor]`, from the `kernel` edge.
    pub kernel_donor: Option<crate::installer::KernelDonor>,
    /// `[…edges.<feature>]`, from every other edge node at the root.
    pub edges: BTreeMap<String, EdgeDecl>,
    /// `[…presets.<name>]`, from the root `preset` val's bundles.
    pub presets: Presets,
    /// `[build.installer.data]`, from the `data.fs` / `data.io` vals.
    pub data: crate::data::Data,
}

/// **What [`SettingsTree::split_opts`] made of an `--opt` line** — this
/// variant's own toggles, this variant's own vals, and the pairs that belong to
/// a donor behind an edge.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SplitOpts {
    /// Canonical path → on/off, for the features of THIS variant.
    pub features: BTreeMap<String, bool>,
    /// Canonical path → the chosen value, checked against the val's roster.
    pub choices: BTreeMap<String, String>,
    /// `path=value`, verbatim, for everything under an edge — the donor's door.
    pub behind_an_edge: Vec<String>,
}

impl SettingsTree {
    /// **Render the rows** this tree spells (installer / iso shape). A feature
    /// carrying an edge is the kernel donor, not an option; its subtree (the
    /// donor's vals and symbols) belongs to the donor's own row.
    pub fn rows(&self) -> Rows {
        let mut out = Rows::default();
        let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
        let mut push_setting = |n: &SettingsNode, s: &SettingNode| {
            if !seen.insert(n.name().to_string()) {
                return;
            }
            out.params.push(ParamSpec {
                name: n.name().to_string(),
                label: n.label.clone(),
                kind: s.declared_kind.unwrap_or(s.param_kind),
                required: s.required,
                boot_param: s.boot_param.clone(),
                staged_file: s.staged_file.clone(),
                lands: s.lands,
            });
            if let Some(f) = s.fixture {
                out.fixtures.insert(n.name().to_string(), f);
            }
        };
        for n in &self.roots {
            match &n.kind {
                NodeKind::Feature(_) if n.edge.is_some() => {}
                // A kernel symbol is the kernel row's, spelled by its pick — not an option row.
                NodeKind::Feature(f) if f.feature.starts_with("CONFIG_") => {}
                NodeKind::Feature(f) => {
                    let mut o = OptionSpec {
                        name: n.name().to_string(),
                        label: n.label.clone(),
                        default: f.default,
                        feature: f.feature.clone(),
                        forbid_symbols: f.forbid_symbols.clone(),
                        bakes: f.bakes.clone(),
                        kernel_requires: f.kernel_requires.clone(),
                        requires: f.requires.clone(),
                        ..Default::default()
                    };
                    for c in &n.children {
                        if let NodeKind::Setting(s) = &c.kind {
                            match c.when {
                                Some(true) => o.params_when_on.push(c.name().to_string()),
                                Some(false) => o.params_when_off.push(c.name().to_string()),
                                None => {}
                            }
                            if s.declared_kind.is_some() {
                                o.param_kinds.insert(c.name().to_string(), s.param_kind);
                            }
                            push_setting(c, s);
                        }
                    }
                    out.options.push(o);
                }
                NodeKind::Setting(s) => push_setting(n, s),
                NodeKind::Val(_) => {}
            }
        }
        out.params.sort_by(|a, b| a.name.cmp(&b.name));
        // The edges: the `kernel` edge of typ kernel is the donor row; every
        // other root edge is an `[…edges.<feature>]` row.
        for n in &self.roots {
            let Some(e) = &n.edge else { continue };
            if n.path == KERNEL_PATH && e.typ == ArtifactKind::Kernel {
                if let Some((repo, kernel)) = e.split() {
                    out.kernel_donor =
                        Some(crate::installer::KernelDonor { repo: repo.to_string(), kernel: kernel.to_string() });
                }
            } else if let Some(d) = e.to_decl() {
                out.edges.insert(n.path.clone(), d);
            }
        }
        if let Some(NodeKind::Val(v)) = self.get(PRESET_PATH).map(|n| &n.kind) {
            out.presets = v.bundles.clone();
        }
        // The `data` group, read back BY PATH — the same road `presets` takes.
        // An absent val is the type default, so a tree that carries no group
        // (an iso's, a cargo variant's) renders the default row and law A is
        // byte-identical to before the group was on the tree at all.
        if let Some(NodeKind::Val(v)) = self.get(DATA_FS_PATH).map(|n| &n.kind) {
            if let Some(fs) = crate::data::DataFs::parse(&v.default) {
                out.data.fs = fs;
            }
        }
        if let Some(NodeKind::Val(v)) = self.get(DATA_IO_PATH).map(|n| &n.kind) {
            if let Some(io) = crate::data::DataIo::parse(&v.default) {
                out.data.io = io;
            }
        }
        out
    }

    // ── presets: a val at the root, a bundle of choices over the tree ────────

    /// **Prove every declared bundle against this tree** — each path resolves
    /// to a node (full path or unambiguous alias), each value fits the node
    /// (`on|off` for a feature, one of the choices for a val, a `--param`
    /// value for a setting), and no bundle names the preset itself. A bundle
    /// that names a path the tree lacks is a NAMED refusal at declaration
    /// time, listing what the tree declares.
    pub fn validate_presets(&self) -> Result<()> {
        let Some(NodeKind::Val(v)) = self.get(PRESET_PATH).map(|n| &n.kind) else { return Ok(()) };
        for name in &v.choices {
            if name.trim().is_empty() || name.contains('=') || name.chars().any(char::is_whitespace) {
                bail!("variant '{}': preset {name:?}: a bundle's name carries no whitespace and no '='", self.variant);
            }
            self.expand_preset(name).map_err(|e| anyhow::anyhow!("variant '{}': preset '{name}': {e:#}", self.variant))?;
        }
        Ok(())
    }

    /// **The spelling a `--opt preset=<x>` on a KERNEL tree actually wanted.**
    ///
    /// A kernel row's preset is a VAL under the `kernel` scope, not a
    /// `[presets]` bundle — two different things that share the word. Before
    /// lane AK the two disagreed out loud: `--list-options` rendered `--opt
    /// preset=slim` (the tree's own flags, law C) and the door answered `no
    /// preset 'slim' on 'tunnr-kernel'; declared bundles: full · normal · slim ·
    /// nuked` — a refusal that lists `slim` while saying there is no `slim`. The
    /// listing is fixed (`of_kernel` scopes its nodes), and the refusal now
    /// names the spelling for anyone who typed the old one from memory or from
    /// a stale doc.
    fn kernel_preset_hint(&self, bundle: &str) -> String {
        let scoped = format!("{KERNEL_PATH}{PATH_SEP}{PRESET_PATH}");
        match self.get(&scoped).map(|n| &n.kind) {
            Some(NodeKind::Val(v)) => format!(
                " — but '{}' has a KERNEL preset, which is a val and not a bundle: spell it `--opt {scoped}={bundle}` (declared: {}). `--list-options` prints that spelling.",
                self.variant,
                v.choices.join(" · ")
            ),
            _ => String::new(),
        }
    }

    /// **Expand a bundle into flags** — each `(path, value)` of the named
    /// bundle becomes the flag its node takes: `--opt <path>=on|off` for a
    /// feature, `--opt <path>=<choice>` for a val, `--param <path>=<value>`
    /// for a setting — with the path CANONICALISED through the tree (an alias
    /// becomes the full path), so the expansion is a set of law-C lines the
    /// tree itself renders. Unknown bundle, unknown path, a value the node
    /// refuses — each a named refusal.
    pub fn expand_preset(&self, bundle: &str) -> Result<Vec<crate::flags::Flag>> {
        use crate::flags::Flag;
        let Some(NodeKind::Val(v)) = self.get(PRESET_PATH).map(|n| &n.kind) else {
            bail!("'{}' declares no presets (no `[…presets.<name>]` table){}", self.variant, self.kernel_preset_hint(bundle));
        };
        let Some(choices) = v.bundles.get(bundle) else {
            bail!(
                "no preset '{bundle}' on '{}'; declared bundles: {}{}",
                self.variant,
                if v.choices.is_empty() { "none".to_string() } else { v.choices.join(" · ") },
                self.kernel_preset_hint(bundle)
            );
        };
        let mut out = Vec::with_capacity(choices.len());
        for (path, value) in choices {
            if path == PRESET_PATH {
                bail!("a bundle cannot choose the preset itself ('{path}')");
            }
            let node = self.resolve(path).map_err(|e| anyhow::anyhow!("{e:#}"))?.node;
            let flag = match &node.kind {
                NodeKind::Feature(_) => {
                    let probe = Flag::choice(&node.path, value.clone());
                    let Some(on) = probe.feature_state() else {
                        bail!("'{}' is a feature and takes on|off, not {value:?}", node.path);
                    };
                    Flag::opt(&node.path, on)
                }
                NodeKind::Val(val) => {
                    if !val.choices.iter().any(|c| c == value) {
                        bail!("'{}' is a val and takes one of {}, not {value:?}", node.path, val.choices.join(" | "));
                    }
                    Flag::choice(&node.path, value.clone())
                }
                NodeKind::Setting(_) => {
                    if value.trim().is_empty() {
                        bail!("'{}' is a setting and a bundle cannot give it an empty value", node.path);
                    }
                    Flag::param(&node.path, value.clone())
                }
            };
            out.push(flag);
        }
        Ok(out)
    }

    /// **Fold a chosen preset into an `--opt` / `--param` line** — the door's
    /// one expansion, shared by the CLI and the pane. `preset=<name>` is
    /// looked for among `opts` (by the root path, exactly); when found it is
    /// consumed, the bundle expands ([`expand_preset`](Self::expand_preset)),
    /// and the bundle's flags go FIRST so every explicit flag on the line
    /// overrides its bundle twin (a later `--opt` for the same path is what the
    /// resolvers read as "supplied twice", so the bundle's twin is DROPPED, not
    /// duplicated). Two `preset=` on one line is refused — one bundle per build.
    /// **Split an `--opt` line by asking the TREE what each path is.**
    ///
    /// This is the one parser a door needs, and it exists because
    /// [`crate::options::parse_cli_options`] cannot be one: its return type is
    /// `name → bool`, so it answers *every* `--opt` with "an option is on or
    /// off". That sentence is what `--opt kernel.preset=nuked` got on the
    /// ISO-compose path (`suite_cli::compose_installer_cli` handed it the
    /// UNSPLIT line), and gunnar's `BUILD-KEYED-INSTALLER.md` carried a
    /// "⚠ do NOT add `--opt kernel.preset=`" warning for it rather than a fix.
    /// The tree already knew better: `kernel.preset` is a VAL with four
    /// choices, `dhcp` is a FEATURE, and `data.fs` is a val too.
    ///
    /// Three buckets, because a door has three things to do with them:
    ///
    /// * `features` — this variant's own on/off toggles, for
    ///   [`crate::options::resolve_options`];
    /// * `choices` — this variant's own vals (`data.fs`, `data.io`, a root
    ///   `preset`), each already checked against its roster;
    /// * `behind_an_edge` — verbatim `path=value` for anything under a node
    ///   that carries an edge (`kernel.*`): the DONOR's door judges those
    ///   (`kernel_features::resolve_kernel_cli`), and a consumer's door that
    ///   tried would be a second opinion about someone else's declaration.
    ///
    /// Every refusal is by name and comes from the tree: an unknown spelling
    /// lists what IS declared ([`resolve`](Self::resolve)), a val lists its
    /// roster, a feature says what an on/off value looks like, and a `--param`
    /// path typed as an `--opt` is told which flag it is.
    pub fn split_opts<S: AsRef<str>>(&self, opts: &[S]) -> Result<SplitOpts> {
        let mut out = SplitOpts::default();
        for pair in opts {
            let pair = pair.as_ref().trim();
            let Some((spelling, value)) = pair.split_once('=') else {
                bail!("--opt {pair}: an option is `<path>=<value>` (`dhcp=off`, `data.fs=xfs`, `kernel.preset=nuked`)");
            };
            let (spelling, value) = (spelling.trim(), value.trim());
            // The word an operator typed is OPTION, so the refusal opens with
            // it and the tree's own "not a setting of variant X (declared: …)"
            // follows — the roster is what makes a typo fixable.
            let node = match self.resolve(spelling) {
                Ok(r) => r.node,
                Err(e) => {
                    // A path the tree does not draw, UNDER an edge, belongs to
                    // the donor's door (`kernel.toolchain=off` is the donor
                    // kernel's own edge toggle) — the rule
                    // `BuildLine::canonical_in` applies to the step line. Kept
                    // verbatim; anything else is refused by name.
                    let under_edge = spelling
                        .split_once(PATH_SEP)
                        .is_some_and(|(root, rest)| !rest.is_empty() && self.get(root).is_some_and(|n| n.edge.is_some()));
                    if under_edge {
                        out.behind_an_edge.push(format!("{spelling}={value}"));
                        continue;
                    }
                    bail!("--opt {pair}: '{spelling}' is not a declared option — {e:#}");
                }
            };
            let path = node.path.clone();
            // Behind an edge ⇒ the donor's door. Asked of the ROOT the path
            // hangs from, so `kernel.preset` and `kernel.vt` route together.
            let root = path.split(PATH_SEP).next().unwrap_or(&path);
            if self.get(root).is_some_and(|n| n.edge.is_some()) {
                out.behind_an_edge.push(format!("{path}={value}"));
                continue;
            }
            match &node.kind {
                NodeKind::Feature(_) => {
                    let Some(on) = crate::flags::Flag::choice(&path, value).feature_state() else {
                        bail!(
                            "--opt {path}={value}: '{path}' is a feature of variant '{}' — it is on or off \
                             (on|off, true|false, yes|no, 1|0)",
                            self.variant
                        );
                    };
                    if out.features.insert(path.clone(), on).is_some() {
                        bail!("--opt '{path}' was supplied twice");
                    }
                }
                NodeKind::Val(v) => {
                    if !v.choices.iter().any(|c| c == value) {
                        bail!(
                            "--opt {path}={value}: '{path}' takes one of {} (declared default: {})",
                            v.choices.join(" · "),
                            if v.default.is_empty() { "<none>" } else { v.default.as_str() }
                        );
                    }
                    if out.choices.insert(path.clone(), value.to_string()).is_some() {
                        bail!("--opt '{path}' was supplied twice");
                    }
                }
                NodeKind::Setting(_) => bail!(
                    "--opt {path}={value}: '{path}' is a VALUE, not a toggle — it is spelled `--param {path}={value}`"
                ),
            }
        }
        Ok(out)
    }

    pub fn expand_opts<S: AsRef<str>>(&self, opts: &[S], params: &[S]) -> Result<Expanded> {
        use crate::flags::Flag;
        let mut chosen: Option<String> = None;
        let mut explicit_opts: Vec<String> = Vec::with_capacity(opts.len());
        for o in opts {
            let o = o.as_ref();
            match o.split_once('=') {
                Some((k, v)) if k.trim() == PRESET_PATH => {
                    if chosen.is_some() {
                        bail!("--opt preset= given twice — one bundle per build");
                    }
                    chosen = Some(v.trim().to_string());
                }
                _ => explicit_opts.push(o.to_string()),
            }
        }
        let explicit_params: Vec<String> = params.iter().map(|p| p.as_ref().to_string()).collect();
        let Some(bundle) = chosen else {
            return Ok(Expanded { opts: explicit_opts, params: explicit_params, preset: None });
        };
        let flags = self.expand_preset(&bundle)?;
        let key = |pair: &str| -> String {
            let name = pair.split_once('=').map(|(k, _)| k).unwrap_or(pair).trim();
            self.canonical_path(name).unwrap_or_else(|_| name.to_string())
        };
        let explicit_opt_keys: Vec<String> = explicit_opts.iter().map(|o| key(o)).collect();
        let explicit_param_keys: Vec<String> = explicit_params.iter().map(|p| key(p)).collect();
        let mut out = Expanded { preset: Some(AppliedPreset { bundle: bundle.clone(), expanded: flags.iter().map(Flag::render).collect() }), ..Default::default() };
        for f in &flags {
            match f {
                Flag::Opt { path, value } => {
                    if !explicit_opt_keys.iter().any(|k| k == path) {
                        out.opts.push(format!("{path}={value}"));
                    }
                }
                Flag::Param { path, value } => {
                    if !explicit_param_keys.iter().any(|k| k == path) {
                        out.params.push(format!("{path}={value}"));
                    }
                }
                // A bundle chooses SETTINGS; a step is asked for on the line
                // (`--build X`, `build_dag`). `expand_preset` renders no such
                // flag today — refused by name rather than dropped, so the day
                // a bundle spells one the operator hears about it.
                Flag::Build { target } => bail!(
                    "preset '{bundle}' would chain a build of '{target}' — a bundle chooses settings; \
                     `--build {target}` is typed on the line"
                ),
            }
        }
        out.opts.extend(explicit_opts);
        out.params.extend(explicit_params);
        Ok(out)
    }

    /// **The kernel pick** the subtree under `prefix` (`""` for a kernel
    /// variant, `kernel` under an installer) spells: its `preset` and `profile`
    /// vals, parsed. `None` when there is no such subtree.
    pub fn kernel_pick(&self, prefix: &str) -> Option<(Preset, HardeningProfile)> {
        let at = |name: &str| if prefix.is_empty() { name.to_string() } else { format!("{prefix}{PATH_SEP}{name}") };
        let val = |path: &str| match &self.get(path)?.kind {
            NodeKind::Val(v) => Some(v.default.clone()),
            _ => None,
        };
        let preset = Preset::parse(&val(&at("preset"))?)?;
        let profile = HardeningProfile::parse(&val(&at("profile"))?)?;
        Some((preset, profile))
    }

    /// The `open` axis a cargo variant's tree spells.
    pub fn open_features(&self) -> std::collections::BTreeSet<String> {
        self.roots
            .iter()
            .filter(|n| matches!(&n.kind, NodeKind::Feature(f) if !f.default && n.edge.is_none()))
            .map(|n| n.name().to_string())
            .collect()
    }

    /// **The kernel decision the tree's state spells** — preset from `vals`
    /// (or the val's default), the click-off set from `state`, and the
    /// fragment rendered by the one writer
    /// ([`KernelFeatureModel::decision_fragment`]). Every clicked-off path is
    /// re-judged through [`try_toggle`](Self::try_toggle) so a state a door
    /// never accepted cannot become a fragment.
    pub fn kernel_decision(
        &self,
        prefix: &str,
        state: &BTreeMap<String, bool>,
        vals: &BTreeMap<String, String>,
    ) -> Result<crate::kernel_features::KernelDecision> {
        let at = |name: &str| if prefix.is_empty() { name.to_string() } else { format!("{prefix}{PATH_SEP}{name}") };
        let (declared_preset, declared_profile) = self
            .kernel_pick(prefix)
            .ok_or_else(|| anyhow::anyhow!("'{}' has no kernel subtree at '{prefix}'", self.variant))?;
        let preset = match vals.get(&at("preset")) {
            None => declared_preset,
            Some(word) => Preset::parse(word).ok_or_else(|| {
                anyhow::anyhow!(
                    "{}={word}: unknown preset; declared bundles: {}",
                    at("preset"),
                    Preset::ALL.map(|p| p.as_str()).join(" · ")
                )
            })?,
        };
        // The OTHER val of the same subtree, read the same way. Until 2026-08-27
        // this reader took the profile off the declaration and ignored a chosen
        // one, so `kernel.profile = hardened` in the pane's vals rendered the
        // vanilla fragment — the two vals sat side by side and only one of them
        // was a choice.
        let profile = match vals.get(&at("profile")) {
            None => declared_profile,
            Some(word) => HardeningProfile::parse(word).ok_or_else(|| {
                anyhow::anyhow!("{}={word}: unknown profile; declared profiles: {}", at("profile"), HardeningProfile::names())
            })?,
        };
        let model = KernelFeatureModel::derive(preset, profile);
        let mut clicked_off: Vec<String> = Vec::new();
        let mut req = ToggleRequest { variant: self.variant.clone(), path: String::new(), on: false, state: BTreeMap::new(), vals: vals.clone() };
        for (path, on) in state {
            if *on {
                continue;
            }
            req.path = path.clone();
            let d = self.try_toggle(&req);
            if !d.verdict.ok {
                anyhow::bail!("{}", d.verdict.line());
            }
            req.state.insert(d.path.clone(), false);
            let name = d.path.strip_prefix(&at("")).unwrap_or(&d.path);
            if let Some(sym) = model.symbol_for_option(name) {
                clicked_off.push(sym.to_string());
            }
        }
        let fragment = model.decision_fragment(&clicked_off, None);
        Ok(crate::kernel_features::KernelDecision { preset, profile, clicked_off, fragment, probe: None })
    }
}

// ── the toggle: the judge, cascade included ──────────────────────────────────

/// **Ask to toggle one feature** — what a click in ⚒ and `--opt <path>=on|off`
/// both send. Carries the operator's state so the answer is about THIS pick,
/// not a clean slate.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ToggleRequest {
    /// The variant whose tree is being toggled.
    pub variant: String,
    /// The feature path (or an unambiguous short alias).
    pub path: String,
    /// The state asked for.
    pub on: bool,
    /// Features the operator has ALREADY decided (path → state); every other
    /// feature sits at its default. A kernel symbol clicked off earlier is here.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub state: BTreeMap<String, bool>,
    /// Vals the operator has chosen (path → choice): `kernel.preset = nuked`.
    /// The kernel model is derived from the chosen preset, not the declared one.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub vals: BTreeMap<String, String>,
}

impl ToggleRequest {
    /// A request against the defaults (nothing decided yet).
    pub fn new(variant: impl Into<String>, path: impl Into<String>, on: bool) -> Self {
        ToggleRequest { variant: variant.into(), path: path.into(), on, ..Default::default() }
    }
}

/// **The decision.** Accepted or refused by name, with the whole consequence:
/// which other features fall with this one (the cascade) and which settings
/// the new state refuses.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToggleDecision {
    /// The canonical full path the request resolved to.
    pub path: String,
    /// The state asked for.
    pub on: bool,
    /// Accepted, or refused with every reason named.
    pub verdict: crate::plugin::Verdict,
    /// Feature paths that ALSO go off as a consequence (a dropped kernel
    /// parent drops its children). Empty for a plain toggle.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub cascade: Vec<String>,
    /// Setting paths that have no meaning in the new state and are refused
    /// if supplied (`dhcp=on` refuses `dhcp.static_ip`).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub refuses: Vec<String>,
    /// Setting paths the new state makes active (and whose `required` applies).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub activates: Vec<String>,
}

impl ToggleDecision {
    fn refused(path: &str, on: bool, why: String) -> Self {
        ToggleDecision {
            path: path.to_string(),
            on,
            verdict: crate::plugin::Verdict::refuse(why),
            cascade: Vec::new(),
            refuses: Vec::new(),
            activates: Vec::new(),
        }
    }
}

impl SettingsTree {
    /// **The plain toggle, cascade included where the tree can compute it** —
    /// the default every build thing starts from
    /// ([`BuildThing::try_toggle`](crate::plugin::BuildThing::try_toggle)).
    ///
    /// * an unknown or ambiguous path — refused by name;
    /// * a setting or a val — refused: a setting takes `--param`, a val takes
    ///   a choice, neither is on/off;
    /// * a plain feature — accepted; `refuses` / `activates` list its children
    ///   by the `when` they belong to;
    /// * a **kernel symbol** (a feature under a subtree that carries a
    ///   `preset` val) — judged by the shared kernel model
    ///   ([`KernelFeatureModel::try_toggle`]): off is refused when the drop
    ///   would reach the boot contract (naming the contract symbols), on is
    ///   refused for a symbol the chosen preset vetoes, and the cascade is
    ///   the model's own.
    pub fn try_toggle(&self, req: &ToggleRequest) -> ToggleDecision {
        let node = match self.resolve(&req.path) {
            Ok(r) => r.node,
            Err(e) => return ToggleDecision::refused(&req.path, req.on, format!("{e:#}")),
        };
        let path = node.path.clone();
        let feature = match &node.kind {
            NodeKind::Feature(f) => f,
            NodeKind::Setting(_) => {
                return ToggleDecision::refused(
                    &path,
                    req.on,
                    format!("'{path}' is a setting, not a feature — it takes `--param {path}=<value>`, not on|off"),
                );
            }
            NodeKind::Val(v) => {
                return ToggleDecision::refused(
                    &path,
                    req.on,
                    format!(
                        "'{path}' is a val, not a feature — it takes one of {}, not on|off",
                        v.choices.join(" | ")
                    ),
                );
            }
        };

        // A kernel symbol? Its parent level carries a `preset` val.
        let prefix = path.rsplit_once(PATH_SEP).map(|(p, _)| p.to_string());
        let at = |name: &str| match &prefix {
            Some(p) => format!("{p}{PATH_SEP}{name}"),
            None => name.to_string(),
        };
        let preset_node = self.get(&at("preset"));
        if feature.feature.starts_with("CONFIG_") && preset_node.is_some() {
            return self.try_toggle_kernel(req, &path, &at("preset"), &at("profile"), prefix.as_deref());
        }

        let mut d = ToggleDecision {
            path: path.clone(),
            on: req.on,
            verdict: crate::plugin::Verdict::ok(),
            cascade: Vec::new(),
            refuses: Vec::new(),
            activates: Vec::new(),
        };
        for c in &node.children {
            match c.when {
                Some(w) if w == req.on => d.activates.push(c.path.clone()),
                Some(_) => d.refuses.push(c.path.clone()),
                None => {}
            }
        }
        d
    }

    fn try_toggle_kernel(
        &self,
        req: &ToggleRequest,
        path: &str,
        preset_path: &str,
        profile_path: &str,
        prefix: Option<&str>,
    ) -> ToggleDecision {
        let chosen = |val_path: &str| -> Option<String> {
            req.vals.get(val_path).cloned().or_else(|| match &self.get(val_path)?.kind {
                NodeKind::Val(v) => Some(v.default.clone()),
                _ => None,
            })
        };
        let preset_word = chosen(preset_path).unwrap_or_default();
        let Some(preset) = Preset::parse(&preset_word) else {
            return ToggleDecision::refused(
                path,
                req.on,
                format!(
                    "{preset_path}={preset_word}: unknown preset; declared bundles: {}",
                    Preset::ALL.map(|p| p.as_str()).join(" · ")
                ),
            );
        };
        let profile_word = chosen(profile_path).unwrap_or_else(|| "vanilla".to_string());
        let Some(profile) = HardeningProfile::parse(&profile_word) else {
            return ToggleDecision::refused(path, req.on, format!("{profile_path}={profile_word}: unknown profile"));
        };
        let model = KernelFeatureModel::derive(preset, profile);
        let symbol_of = |p: &str| -> Option<String> {
            let name = match prefix {
                Some(pre) => p.strip_prefix(pre)?.strip_prefix(PATH_SEP)?,
                None => p,
            };
            if name.contains(PATH_SEP) {
                return None;
            }
            model.symbol_for_option(name).map(str::to_string)
        };
        let path_of = |symbol: &str| -> String {
            let name = crate::kernel_features::option_name(symbol);
            match prefix {
                Some(pre) => format!("{pre}{PATH_SEP}{name}"),
                None => name,
            }
        };
        let Some(symbol) = symbol_of(path) else {
            return ToggleDecision::refused(
                path,
                req.on,
                format!(
                    "'{path}': no such symbol in the {} pick; declared rows: {}",
                    preset.as_str(),
                    model.option_rows().iter().map(|r| path_of(&r.symbol)).collect::<Vec<_>>().join(" · ")
                ),
            );
        };
        // The operator's current click-off set, in the model's vocabulary.
        let mut clicked: Vec<String> = req
            .state
            .iter()
            .filter(|(_, on)| !**on)
            .filter_map(|(p, _)| symbol_of(p))
            .collect();
        let row_default_on = model.symbols.iter().find(|s| s.name == symbol).map(|s| s.enabled).unwrap_or(false);
        let mut d = ToggleDecision {
            path: path.to_string(),
            on: req.on,
            verdict: crate::plugin::Verdict::ok(),
            cascade: Vec::new(),
            refuses: Vec::new(),
            activates: Vec::new(),
        };
        if req.on {
            if !row_default_on {
                d.verdict = crate::plugin::Verdict::refuse(format!(
                    "'{path}': the {} preset vetoes {symbol} — the decision can only veto; \
                     to have it, pick a bundle that has it ({preset_path}=…)",
                    preset.as_str()
                ));
                return d;
            }
            if let Some(i) = clicked.iter().position(|s| s == &symbol) {
                clicked.remove(i);
            }
        } else {
            if clicked.iter().any(|s| s == &symbol) {
                // Already off — the door says so rather than toggling it back.
                d.verdict = crate::plugin::Verdict::ok_with(format!("'{path}' is already off"));
            } else {
                // The model's own door: it refuses a contract-breaking drop.
                let mut trial = clicked.clone();
                match model.try_toggle(&mut trial, &symbol) {
                    Some(crate::kernel_features::ToggleOutcome::Off) => clicked = trial,
                    Some(crate::kernel_features::ToggleOutcome::Refused(broken)) => {
                        d.verdict = crate::plugin::Verdict::refuse(format!(
                            "'{path}': turning {symbol} off would break the boot contract: {}",
                            broken.iter().map(|b| format!("{} ({b})", path_of(b))).collect::<Vec<_>>().join(", ")
                        ));
                        return d;
                    }
                    // `On` cannot happen (not in `trial`); `None` = not in the model.
                    _ => {
                        d.verdict = crate::plugin::Verdict::refuse(format!("'{path}': {symbol} is not in the {} pick", preset.as_str()));
                        return d;
                    }
                }
            }
        }
        // The consequence: everything the cascade drops beyond the clicks.
        let dropped = model.cascade_for(&clicked);
        d.cascade = model
            .symbols
            .iter()
            .enumerate()
            .filter(|&(i, s)| dropped[i] && !clicked.contains(&s.name) && s.enabled)
            .map(|(_, s)| path_of(&s.name))
            .collect();
        d
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// gunnar's real shape, minus gunnar: two options with conditional params,
    /// one unconditional param, a fixture, and a kernel donor.
    fn gunnar_like() -> BuildVariantSet {
        BuildVariantSet::from_toml_str(
            r#"
            [build]
            offers = ["dhcp-client", "wg-client"]

            [[build.variant]]
            name = "gunnar-server"
            features = ["dhcp-client", "wg-client"]
            no_default_features = true

            [[build.installer]]
            name = "gunnar-installer"
            variant = "gunnar-server"

            [build.installer.kernel_donor]
            repo = "tunnr"
            kernel = "tunnr-kernel"

            [build.installer.fixtures]
            hub_conf = "wg-hub"

            [[build.installer.params]]
            name = "site"
            kind = "text"
            required = true
            boot_param = "korp.site"

            [[build.installer.params]]
            name = "hub_conf"
            staged_file = "hub.conf"

            [[build.installer.params]]
            name = "static_ip"
            boot_param = "korp.ip"

            [[build.installer.params]]
            name = "gateway"
            boot_param = "korp.gw"

            [[build.installer.options]]
            name = "wireguard"
            label = "WireGuard-klient"
            feature = "wg-client"
            params_when_on = ["hub_conf"]

            [[build.installer.options]]
            name = "dhcp"
            feature = "dhcp-client"
            params_when_off = ["static_ip", "gateway"]
            param_kinds = { static_ip = "ipv4-cidr", gateway = "ipv4" }
            bakes = { boot_param = "korp.env.GUNNAR_NET", when_on = "eth0:dhcp", when_off = "eth0:static:{static_ip}:{gateway}" }

            [[build.kernel]]
            name = "tunnr-kernel"
            version = "6.12.4"
            tarball_sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
            preset = "nuked"
            "#,
        )
        .expect("parses")
    }

    #[test]
    fn a_flat_declaration_derives_the_tree_with_no_toml_change() {
        let set = gunnar_like();
        let t = SettingsTree::of_set(&set, "gunnar-installer").expect("declared");
        assert_eq!(t.typ, ArtifactKind::Installer);
        let roots: Vec<&str> = t.roots.iter().map(|n| n.path.as_str()).collect();
        // The two `data` vals close the tree: an installer ALWAYS carries the
        // group, declared or not, so `--opt data.fs=` is spellable and
        // `--list-options` shows the arm the recipe is on.
        assert_eq!(roots, vec!["wireguard", "dhcp", "site", "kernel", "data.fs", "data.io"]);

        let wg = t.get("wireguard").unwrap();
        assert!(matches!(&wg.kind, NodeKind::Feature(f) if f.default && f.feature == "wg-client"));
        let hub = t.get("wireguard.hub_conf").expect("params_when_on became a child");
        assert_eq!(hub.when, Some(true));
        assert!(matches!(&hub.kind, NodeKind::Setting(s) if s.fixture == Some(FixtureKind::WgHub) && s.staged_file == "hub.conf"));

        let ip = t.get("dhcp.static_ip").expect("params_when_off became a child");
        assert_eq!(ip.when, Some(false));
        assert!(matches!(&ip.kind, NodeKind::Setting(s) if s.param_kind == ParamKind::Ipv4Cidr), "param_kinds sharpened it");
        assert!(matches!(&t.get("dhcp").unwrap().kind, NodeKind::Feature(f) if f.bakes.is_some()));

        let site = t.get("site").unwrap();
        assert_eq!(site.when, None, "an unconditional param is a root setting");
        assert!(matches!(&site.kind, NodeKind::Setting(s) if s.required && s.boot_param == "korp.site"));
    }

    #[test]
    fn the_kernel_donor_is_the_first_edge_and_projects_preset_and_symbols() {
        let set = gunnar_like();
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let k = t.get("kernel").expect("the donor became the kernel feature");
        assert_eq!(
            k.edge,
            Some(Edge { coordinate: "tunnr/tunnr-kernel".into(), typ: ArtifactKind::Kernel, lazy: false })
        );
        assert_eq!(k.edge.as_ref().unwrap().split(), Some(("tunnr", "tunnr-kernel")));
        let preset = t.get("kernel.preset").expect("kernel.preset is a val");
        assert!(matches!(&preset.kind, NodeKind::Val(v) if v.choices == ["full","normal","slim","nuked"] && v.default == "nuked"),
            "the self-donor's declared preset is the default: {preset:?}");
        assert!(t.get("kernel.unix").is_some(), "kernel.<symbol> features: {:?}", t.paths());
        assert!(matches!(&t.get("kernel.unix").unwrap().kind, NodeKind::Feature(f) if f.feature == "CONFIG_UNIX"));
        // The donor struct is untouched: kept as-is for compatibility.
        assert_eq!(set.installers[0].kernel_donor.as_ref().unwrap().repo, "tunnr");
    }

    #[test]
    fn flat_spellings_are_unambiguous_aliases_and_a_clash_is_a_named_refusal() {
        let set = gunnar_like();
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let r = t.resolve("static_ip").unwrap();
        assert_eq!(r.node.path, "dhcp.static_ip");
        assert!(r.via_alias);
        let r = t.resolve("dhcp.static_ip").unwrap();
        assert!(!r.via_alias);
        assert_eq!(t.canonical_path("hub_conf").unwrap(), "wireguard.hub_conf");
        assert_eq!(t.canonical_path("kernel.preset").unwrap(), "kernel.preset");

        // A short name two nodes carry is ambiguous. Build the clash by hand:
        // `wireguard.preset` beside `kernel.preset`.
        let mut t2 = t.clone();
        t2.roots[0].children.push(SettingsNode::leaf(
            "wireguard.preset",
            "",
            NodeKind::Val(ValNode { choices: vec!["hub".into()], default: "hub".into(), bundles: Presets::new() }),
        ));
        let e = format!("{:#}", t2.resolve("preset").unwrap_err());
        assert!(e.contains("AMBIGUOUS"), "{e}");
        assert!(e.contains("kernel.preset") && e.contains("wireguard.preset"), "both paths are listed: {e}");
        // The full paths still work, and an exact full path always wins over an alias.
        assert_eq!(t2.resolve("kernel.preset").unwrap().node.path, "kernel.preset");
        t2.roots.push(SettingsNode::leaf(
            "preset",
            "",
            NodeKind::Val(ValNode { choices: vec!["airgap".into()], default: "airgap".into(), bundles: Presets::new() }),
        ));
        assert_eq!(t2.resolve("preset").unwrap().node.path, "preset", "a root val named preset is the exact path");

        let e = format!("{:#}", t.resolve("dchp").unwrap_err());
        assert!(e.contains("not a setting") && e.contains("dhcp.static_ip"), "{e}");
    }

    /// **The listing and the refusal say the SAME spelling** (lane AK,
    /// 2026-08-30). `--list-options` renders `flags_of(tree)` verbatim (law C),
    /// so the tree IS the listing; before this lane the kernel row's tree spelt
    /// its vals bare and the door accepted only the `kernel.` scope, so the
    /// listing printed `--opt preset=slim` and the door answered `no preset
    /// 'slim' on 'tunnr-kernel'; declared bundles: full · normal · slim · nuked`
    /// — a refusal that lists `slim` while saying there is no `slim`. It cost
    /// lane AH a wrong turn. RED without either half of the fix.
    #[test]
    fn the_kernel_listing_prints_the_spelling_the_door_accepts() {
        let set = gunnar_like();
        let k = SettingsTree::of_set(&set, "tunnr-kernel").unwrap();
        let lines: Vec<String> = crate::flags::flags_of(&k).iter().map(|f| f.render()).collect();
        assert!(lines.contains(&"--opt kernel.preset=slim".to_string()), "{lines:?}");
        assert!(lines.contains(&"--opt kernel.profile=hardened".to_string()), "{lines:?}");
        assert!(!lines.iter().any(|l| l == "--opt preset=slim"), "the bare spelling is not offered: {lines:?}");
        // Every rendered `kernel.*` line is one `split_kernel_opts` accepts and
        // the kernel grammar then judges — the listing is runnable, not decorative.
        let (pairs, rest) = crate::kernel_features::split_kernel_opts(&["kernel.preset=slim".to_string()]).unwrap();
        assert!(rest.is_empty());
        assert_eq!(pairs, vec![("preset".to_string(), "slim".to_string())]);
        // And the spelling the listing USED to print refuses by naming the one
        // it prints now.
        let e = format!("{:#}", k.expand_preset("slim").unwrap_err());
        assert!(e.contains("--opt kernel.preset=slim"), "{e}");
        assert!(e.contains("--list-options"), "the refusal points at the listing that agrees with it: {e}");
    }

    #[test]
    fn a_kernel_variant_and_a_cargo_variant_have_trees_too() {
        let set = gunnar_like();
        let k = SettingsTree::of_set(&set, "tunnr-kernel").unwrap();
        assert_eq!(k.typ, ArtifactKind::Kernel);
        // The kernel row's own tree is scoped `kernel.*` — the ONE spelling
        // both doors accept (lane AK); a bare `unix` still resolves by name.
        assert_eq!(k.roots[0].path, "kernel.preset");
        assert!(k.get("kernel.unix").is_some());
        assert!(k.resolve("unix").is_ok(), "the bare name is still an unambiguous alias");
        let v = SettingsTree::of_set(&set, "gunnar-server").unwrap();
        assert_eq!(v.typ, ArtifactKind::Exe);
        assert!(v.paths().is_empty(), "always-on features are identity, not settings: {:?}", v.paths());
        let mut open = set.variants[0].clone();
        open.open.insert("gui".into());
        let v = SettingsTree::of_variant(&open);
        assert_eq!(v.paths(), vec!["gui"]);
        assert_eq!(v.open_features(), open.open);
        assert!(SettingsTree::of_set(&set, "ghost").is_none());
        assert_eq!(SettingsTree::forest(&set).len(), 3);
    }

    #[test]
    fn subtree_and_prefixing_keep_paths_as_ids() {
        let set = gunnar_like();
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let sub = t.subtree("dhcp").unwrap();
        assert_eq!(sub.roots.len(), 1);
        assert_eq!(sub.paths(), vec!["dhcp", "dhcp.static_ip", "dhcp.gateway"]);
        assert!(t.subtree("nope").is_none());
        assert_eq!(t.subtree("").unwrap(), t);
        let k = SettingsTree::of_set(&set, "tunnr-kernel").unwrap();
        let projected = k.roots[0].clone().prefixed("donor");
        assert_eq!(projected.path, "donor.kernel.preset");
    }

    /// The index gives the tree's own answers, including the ambiguity refusal.
    #[test]
    fn the_index_answers_exactly_what_the_tree_answers() {
        let set = gunnar_like();
        let mut t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        t.roots[0].children.push(SettingsNode::leaf(
            "wireguard.preset",
            "",
            NodeKind::Val(ValNode { choices: vec!["hub".into()], default: "hub".into(), bundles: Presets::new() }),
        ));
        let ix = SettingsIndex::of(&t);
        for spelling in ["dhcp.static_ip", "static_ip", "hub_conf", "kernel.preset", "preset", "nope", ""] {
            let a = t.resolve(spelling).map(|r| (r.node.path.clone(), r.via_alias)).map_err(|e| format!("{e:#}"));
            let b = ix.resolve(spelling).map(|r| (r.node.path.clone(), r.via_alias)).map_err(|e| format!("{e:#}"));
            assert_eq!(a, b, "{spelling}");
        }
        assert_eq!(ix.get("dhcp.gateway").map(|n| n.path.as_str()), Some("dhcp.gateway"));
    }

    /// The JSON shape the ⚒ pane and the plugin host read: tagged kinds, no
    /// empty fields, and a round trip through serde.
    #[test]
    fn the_tree_serializes_tagged_and_lean_and_round_trips() {
        let set = gunnar_like();
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let json = serde_json::to_string(&t).unwrap();
        assert!(json.contains(r#""kind":"feature""#), "{json}");
        assert!(json.contains(r#""kind":"setting""#), "{json}");
        assert!(json.contains(r#""kind":"val""#), "{json}");
        assert!(json.contains(r#""typ":"installer""#), "{json}");
        assert!(!json.contains(r#""forbid_symbols":[]"#), "empty fields are skipped: {json}");
        let back: SettingsTree = serde_json::from_str(&json).unwrap();
        assert_eq!(back, t);
        // An older/leaner document with unknown keys still loads (benign).
        let lean: SettingsNode = serde_json::from_str(
            r#"{"path":"x","kind":"feature","default":true,"future_field":1}"#,
        )
        .unwrap();
        assert!(matches!(lean.kind, NodeKind::Feature(FeatureNode { default: true, .. })));
    }

    // ── the toggle ───────────────────────────────────────────────────────────

    #[test]
    fn a_plain_feature_toggles_and_names_what_it_activates_and_refuses() {
        let set = gunnar_like();
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let d = t.try_toggle(&ToggleRequest::new("gunnar-installer", "dhcp", false));
        assert!(d.verdict.ok, "{}", d.verdict.line());
        assert_eq!(d.path, "dhcp");
        assert_eq!(d.activates, vec!["dhcp.static_ip", "dhcp.gateway"]);
        assert!(d.refuses.is_empty());
        let d = t.try_toggle(&ToggleRequest::new("gunnar-installer", "dhcp", true));
        assert_eq!(d.refuses, vec!["dhcp.static_ip", "dhcp.gateway"]);
        // Aliases resolve; settings and vals are refused by name.
        let d = t.try_toggle(&ToggleRequest::new("gunnar-installer", "static_ip", false));
        assert!(!d.verdict.ok);
        assert!(d.verdict.line().contains("is a setting") && d.verdict.line().contains("--param dhcp.static_ip="), "{}", d.verdict.line());
        let d = t.try_toggle(&ToggleRequest::new("gunnar-installer", "kernel.preset", false));
        assert!(!d.verdict.ok && d.verdict.line().contains("is a val") && d.verdict.line().contains("nuked"), "{}", d.verdict.line());
        let d = t.try_toggle(&ToggleRequest::new("gunnar-installer", "dchp", false));
        assert!(!d.verdict.ok && d.verdict.line().contains("not a setting"), "{}", d.verdict.line());
    }

    /// The kernel subtree answers with the shared model: a contract-breaking
    /// drop is refused naming the contract, a veto cannot be switched on, and
    /// the cascade is the model's own — under `kernel.` in an installer and at
    /// the root of the kernel variant alike.
    #[test]
    fn the_kernel_subtree_is_judged_by_the_kernel_model_with_its_cascade() {
        let set = gunnar_like();
        let model = KernelFeatureModel::derive(Preset::Nuked, HardeningProfile::Vanilla);
        // Find one locked row and one free row with children, from the model itself.
        let rows = model.option_rows();
        let locked = rows.iter().find(|r| !r.locked_by.is_empty()).expect("the nuked pick has a contract row");
        let free_parent = rows
            .iter()
            .find(|r| r.default_on && r.locked_by.is_empty() && rows.iter().any(|c| c.requires.as_deref() == Some(&r.symbol) && c.default_on))
            .expect("a free parent with an enabled child");
        let vetoed = rows.iter().find(|r| !r.default_on).expect("nuked states absences");

        for (variant, prefix) in [("gunnar-installer", "kernel."), ("tunnr-kernel", "")] {
            let t = SettingsTree::of_set(&set, variant).unwrap();
            let d = t.try_toggle(&ToggleRequest::new(variant, format!("{prefix}{}", locked.option), false));
            assert!(!d.verdict.ok, "{variant}: {}", d.verdict.line());
            assert!(d.verdict.line().contains("boot contract"), "{}", d.verdict.line());
            let d = t.try_toggle(&ToggleRequest::new(variant, format!("{prefix}{}", free_parent.option), false));
            assert!(d.verdict.ok, "{variant}: {}", d.verdict.line());
            assert!(!d.cascade.is_empty(), "dropping a parent drops its children: {d:?}");
            assert!(d.cascade.iter().all(|p| p.starts_with(prefix)), "cascade paths carry the prefix: {:?}", d.cascade);
            let d = t.try_toggle(&ToggleRequest::new(variant, format!("{prefix}{}", vetoed.option), true));
            assert!(!d.verdict.ok && d.verdict.line().contains("vetoes"), "{}", d.verdict.line());
        }

        // The chosen preset in `vals` changes the model: under `full` nothing is declared.
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let mut req = ToggleRequest::new("gunnar-installer", format!("kernel.{}", locked.option), false);
        req.vals.insert("kernel.preset".into(), "full".into());
        let d = t.try_toggle(&req);
        assert!(!d.verdict.ok && d.verdict.line().contains("full pick"), "{}", d.verdict.line());
        req.vals.insert("kernel.preset".into(), "slin".into());
        let d = t.try_toggle(&req);
        assert!(d.verdict.line().contains("unknown preset"), "{}", d.verdict.line());
    }

    /// **The rows are the spelling of the tree**: rows → tree → rows is the
    /// identity (options in order, params by name, fixtures, the donor), and
    /// the kernel pick reads back.
    #[test]
    fn the_tree_renders_back_to_exactly_the_rows_it_came_from() {
        let set = gunnar_like();
        let inst = &set.installers[0];
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let rows = t.rows();
        assert_eq!(rows.options, inst.options);
        let mut want = inst.params.clone();
        want.sort_by(|a, b| a.name.cmp(&b.name));
        assert_eq!(rows.params, want);
        assert_eq!(rows.fixtures, inst.fixtures);
        assert_eq!(rows.kernel_donor, inst.kernel_donor);
        assert_eq!(t.kernel_pick("kernel"), Some((Preset::Nuked, HardeningProfile::Vanilla)));
        let k = SettingsTree::of_set(&set, "tunnr-kernel").unwrap();
        assert_eq!(k.kernel_pick("kernel"), Some((Preset::Nuked, HardeningProfile::Vanilla)), "the row's own tree is scoped too");
        assert_eq!(k.kernel_pick(""), None, "and nothing sits bare at the root any more");
        assert!(k.rows().options.is_empty() && k.rows().kernel_donor.is_none());
        // A second derivation from the rendered rows spells the same rows again.
        let again = SettingsTree::of_rows("gunnar-installer", ArtifactKind::Installer, &rows.options, &rows.params, &rows.fixtures);
        let mut again_rows = again.rows();
        assert!(again_rows.kernel_donor.is_none(), "bare rows carry no edge");
        assert!(again_rows.data.is_default(), "bare rows carry no group either");
        again_rows.kernel_donor = rows.kernel_donor.clone();
        again_rows.data = rows.data.clone();
        assert_eq!(again_rows, rows);
        // The two `data` vals are the group's, not the option rows': they come
        // with the DECLARED group and go with the kernel subtree in the count.
        assert_eq!(again.paths().len() + t.subtree("kernel").unwrap().len() + 2, t.len());
    }

    /// The derivation does not touch the declaration: law A still holds over
    /// the same set.
    #[test]
    fn deriving_the_tree_leaves_law_a_intact() {
        let set = gunnar_like();
        let _ = SettingsTree::forest(&set);
        assert_eq!(crate::laws::law_declaration_struct(&set), Vec::<String>::new());
    }

    // ── the consumption edge, generalised (wave 2, gap #3) ──────────────────

    /// gunnar's shape with the LIVE iso consuming the same donor kernel, a
    /// cargo variant consuming a wasm build thing, and the installer consuming
    /// a container image beside its kernel.
    fn edgy() -> BuildVariantSet {
        BuildVariantSet::from_toml_str(
            r#"
            [build]

            [[build.variant]]
            name = "gunnar-server"
            role = "server"
            open = ["telemetry"]

            [build.variant.edges.recipe]
            repo = "gunnar"
            variant = "gunnar-build-thing"
            typ = "wasm"

            [[build.variant]]
            name = "gunnar-build-thing"
            kind = "wasm"
            target = "wasm32-unknown-unknown"

            [[build.iso]]
            name = "gunnar-appliance-iso"
            from_variant = "gunnar-server"

            [build.iso.kernel_donor]
            repo = "tunnr"
            kernel = "tunnr-kernel"

            [[build.installer]]
            name = "gunnar-installer"
            variant = "gunnar-server"

            [build.installer.kernel_donor]
            repo = "tunnr"
            kernel = "tunnr-kernel"

            [build.installer.edges.runtime]
            repo = "tunnr"
            variant = "tunnr-runtime"
            typ = "container"

            [[build.installer.options]]
            name = "dhcp"
            feature = "dhcp-client"

            [[build.kernel]]
            name = "tunnr-kernel"
            version = "6.12.4"
            tarball_sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
            preset = "nuked"
            "#,
        )
        .expect("parses")
    }

    /// RED-FIRST: before wave 2 `of_iso` took no donor and an iso's
    /// `[build.iso.kernel_donor]` was dropped by the loader — the live medium
    /// could not name its kernel at all.
    #[test]
    fn an_iso_declares_its_kernel_edge_and_the_tree_projects_it_like_the_installers() {
        let set = edgy();
        let iso = SettingsTree::of_set(&set, "gunnar-appliance-iso").expect("declared");
        let k = iso.get(KERNEL_PATH).expect("the kernel edge is a root feature of the iso");
        assert_eq!(k.edge, Some(Edge::new("tunnr", "tunnr-kernel", ArtifactKind::Kernel)));
        assert_eq!(iso.kernel_pick("kernel"), Some((Preset::Nuked, HardeningProfile::Vanilla)), "the self-donor's pick");
        let inst = SettingsTree::of_set(&set, "gunnar-installer").expect("declared");
        assert_eq!(inst.get(KERNEL_PATH).unwrap().edge, k.edge, "one edge, two consumers");
        // The iso's rows render the donor back (law A's tree half).
        assert_eq!(iso.rows().kernel_donor, set.isos[0].kernel_donor);
    }

    /// Any variant kind carries edges, each a root feature bound to
    /// `repo/variant : typ`, and the rows render them back exactly.
    #[test]
    fn edges_are_first_class_on_any_variant_kind_and_round_trip_through_the_rows() {
        let set = edgy();
        let v = SettingsTree::of_set(&set, "gunnar-server").unwrap();
        let recipe = v.get("recipe").expect("the cargo variant's edge");
        assert_eq!(recipe.edge, Some(Edge::new("gunnar", "gunnar-build-thing", ArtifactKind::Wasm)));
        assert!(matches!(&recipe.kind, NodeKind::Feature(f) if f.default && f.feature.is_empty()));
        assert!(recipe.children.is_empty(), "a non-kernel edge's subtree is delegated");
        assert_eq!(v.rows().edges, set.variants[0].edges);
        assert_eq!(v.open_features(), set.variants[0].open, "an edge is not an open feature");

        let inst = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let roots: Vec<&str> = inst.roots.iter().map(|n| n.path.as_str()).collect();
        assert_eq!(roots, ["dhcp", "kernel", "runtime", "data.fs", "data.io"], "options, then the donor, then the other edges, then the data group");
        let rows = inst.rows();
        assert_eq!(rows.edges, set.installers[0].edges);
        assert_eq!(rows.kernel_donor, set.installers[0].kernel_donor);
        // `--build` renders for every edge, by path and by coordinate (law C).
        let flags: Vec<String> = crate::flags::flags_of(&inst).iter().map(|f| f.render()).collect();
        for want in ["--build kernel", "--build tunnr/tunnr-kernel", "--build runtime", "--build tunnr/tunnr-runtime"] {
            assert!(flags.iter().any(|f| f == want), "{want} missing from {flags:?}");
        }
        assert_eq!(crate::laws::law_rows_tree(&set), Vec::<String>::new());
        assert_eq!(crate::laws::law_declaration_struct(&set), Vec::<String>::new());
    }

    /// The two spellings of the kernel edge, the shape rules, and a feature
    /// declared twice — each a named refusal at declaration time.
    #[test]
    fn a_malformed_edge_is_refused_by_name_at_declaration_time() {
        let mut set = edgy();
        let inst = &mut set.installers[0];
        inst.edges.insert(
            "kernel".into(),
            EdgeDecl { repo: "tunnr".into(), variant: "tunnr-kernel".into(), typ: ArtifactKind::Kernel, lazy: false },
        );
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("spell it once") && e.contains("kernel_donor"), "{e}");
        inst.edges.remove("kernel");

        inst.edges.insert("dhcp".into(), EdgeDecl { repo: "x".into(), variant: "y".into(), typ: ArtifactKind::Exe, lazy: false });
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("'dhcp' is declared twice"), "{e}");
        inst.edges.remove("dhcp");

        inst.edges.insert("a.b".into(), EdgeDecl { repo: "x".into(), variant: "y".into(), typ: ArtifactKind::Exe, lazy: false });
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("ROOT feature"), "{e}");
        inst.edges.remove("a.b");

        inst.edges.insert("half".into(), EdgeDecl { repo: String::new(), variant: "y".into(), typ: ArtifactKind::Exe, lazy: false });
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("edges.half.repo is empty"), "{e}");

        // An iso with both a kernel FILE and a donor is two kernels.
        let mut iso = set.isos[0].clone();
        iso.kernel = "artifacts/bzImage".into();
        let e = format!("{:#}", iso.validate().unwrap_err());
        assert!(e.contains("one kernel per medium"), "{e}");

        // A cargo variant whose edge is also one of its own features.
        let mut v = set.variants[0].clone();
        v.open.insert("recipe".into());
        let e = format!("{:#}", set.validate(&v).unwrap_err());
        assert!(e.contains("'recipe' is declared twice"), "{e}");
    }

    /// **The type check** — an edge is proven against the donor's own
    /// declaration through the constellation root: missing checkout, missing
    /// variant, wrong typ — each refused by name; a self-edge resolves in the
    /// same set; a sound constellation passes.
    #[test]
    fn an_edge_is_type_checked_against_the_donor_through_the_constellation_root() {
        let base = tempfile::tempdir().unwrap();
        let gunnar = base.path().join("gunnar");
        std::fs::create_dir_all(gunnar.join(".nornir")).unwrap();
        let set = edgy();
        let text = toml::to_string(&crate::variant::BuildConfig::emitting(&set)).unwrap();
        std::fs::write(gunnar.join(".nornir/build-variants.toml"), &text).unwrap();

        // No tunnr beside: the checkout is named.
        let e = format!("{:#}", set.validate_edges_in(&gunnar).unwrap_err());
        assert!(e.contains("no checkout 'tunnr' beside"), "{e}");

        // tunnr beside, declaring the kernel but no runtime container.
        let tunnr = base.path().join("tunnr");
        std::fs::create_dir_all(tunnr.join(".nornir")).unwrap();
        std::fs::write(
            tunnr.join(".nornir/build-variants.toml"),
            r#"
[build]
[[build.kernel]]
name = "tunnr-kernel"
version = "6.12.4"
tarball_sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
preset = "nuked"
"#,
        )
        .unwrap();
        let e = format!("{:#}", set.validate_edges_in(&gunnar).unwrap_err());
        assert!(e.contains("installer 'gunnar-installer': edge 'runtime' → tunnr/tunnr-runtime : container"), "{e}");
        assert!(e.contains("declares no variant 'tunnr-runtime'") && e.contains("tunnr-kernel"), "{e}");

        // …declaring it with the WRONG typ.
        std::fs::write(
            tunnr.join(".nornir/build-variants.toml"),
            r#"
[build]
[[build.kernel]]
name = "tunnr-kernel"
version = "6.12.4"
tarball_sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
preset = "nuked"

[[build.variant]]
name = "tunnr-runtime"
kind = "exe"
"#,
        )
        .unwrap();
        let e = format!("{:#}", set.validate_edges_in(&gunnar).unwrap_err());
        assert!(e.contains("as typ exe, not container"), "{e}");

        // …and right: every edge — self (wasm), kernel, container — checks out.
        std::fs::write(
            tunnr.join(".nornir/build-variants.toml"),
            r#"
[build]
[[build.kernel]]
name = "tunnr-kernel"
version = "6.12.4"
tarball_sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
preset = "nuked"

[[build.variant]]
name = "tunnr-runtime"
kind = "container"
container_ref = "tunnr-runtime"

[[build.container]]
name = "tunnr-runtime"
# `base`, NOT `base_image`. This fixture said `base_image` from the day it was
# written and `ContainerBuild` has never had such a field: the key was dropped
# in silence, so the fixture declared a scratch base it never had. Found
# 2026-08-30 by `schema`'s R3 the first time the loader was made to refuse what
# it cannot read — in this crate's own test data.
base = "scratch"
"#,
        )
        .unwrap();
        set.validate_edges_in(&gunnar).expect("a sound constellation");
        assert_eq!(set.kernel_donor_of("gunnar-appliance-iso").map(|d| d.kernel.as_str()), Some("tunnr-kernel"));
        assert_eq!(set.kernel_donor_consumers().len(), 2);
        assert_eq!(set.edges_of("gunnar-server").unwrap().len(), 1);
        assert_eq!(set.edges_of("gunnar-installer").unwrap().len(), 2);
        assert_eq!(set.edges_of("tunnr-kernel"), Some(Vec::new()));
        assert_eq!(set.edges_of("nope"), None);
    }

    // ── presets: a val at the variant's root (wave 2, gap #5) ───────────────

    /// gunnar's shape plus a declared bundle — a FIXTURE: gunnar's own
    /// declaration has no honest use for one today (its "airgap" is the payload
    /// shape, already the default; "no hub yet" is first-boot reality with
    /// WireGuard on), so the bundle lives here and in the doors' tests only.
    fn with_presets() -> BuildVariantSet {
        let mut set = gunnar_like();
        let inst = &mut set.installers[0];
        inst.presets.insert(
            "airgap".into(),
            [("wireguard", "off"), ("dhcp", "off"), ("kernel.preset", "nuked"), ("static_ip", "10.0.2.15/24"), ("gateway", "10.0.2.2")]
                .into_iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
        );
        inst.presets.insert("hub".into(), [("wireguard".to_string(), "on".to_string())].into_iter().collect());
        set
    }

    /// RED-FIRST: a variant's root had no `val`; `preset = airgap` could not be
    /// declared, projected or chosen. Now it is a root val whose choices are
    /// the bundle names, carrying each bundle, rendering back to the rows.
    #[test]
    fn a_root_preset_is_a_val_over_the_tree_and_round_trips_through_the_rows() {
        let set = with_presets();
        let inst = &set.installers[0];
        inst.validate().expect("every bundle names paths the tree has");
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let p = t.get(PRESET_PATH).expect("the root preset val");
        let NodeKind::Val(v) = &p.kind else { panic!("a val") };
        assert_eq!(v.choices, ["airgap", "hub"]);
        assert!(v.default.is_empty(), "no bundle is chosen until somebody says so");
        assert_eq!(v.bundles, inst.presets);
        assert_eq!(t.rows().presets, inst.presets);
        // Law A and law C hold over it: the flags render each choice.
        assert_eq!(crate::laws::law_rows_tree(&set), Vec::<String>::new());
        assert_eq!(crate::laws::law_declaration_struct(&set), Vec::<String>::new());
        let flags: Vec<String> = crate::flags::flags_of(&t).iter().map(|f| f.render()).collect();
        assert!(flags.contains(&"--opt preset=airgap".to_string()), "{flags:?}");
        // A variant with no presets has no such node at all.
        let plain = SettingsTree::of_set(&gunnar_like(), "gunnar-installer").unwrap();
        assert!(plain.get(PRESET_PATH).is_none());
        // Serde: the bundles ride the node, lean when empty.
        let json = serde_json::to_string(&t).unwrap();
        assert!(json.contains("\"bundles\""));
        assert!(!serde_json::to_string(&plain).unwrap().contains("\"bundles\""));
        assert_eq!(serde_json::from_str::<SettingsTree>(&json).unwrap(), t);
    }

    /// `--opt preset=airgap` EXPANDS into the tree's own flags — features to
    /// `--opt path=on|off`, vals to `--opt path=<choice>`, settings to
    /// `--param path=<value>` — with paths canonicalised; the record carries
    /// the bundle AND the expansion; an explicit flag on the line wins.
    #[test]
    fn choosing_a_preset_expands_into_law_c_lines_and_explicit_flags_win() {
        let set = with_presets();
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let flags: Vec<String> = t.expand_preset("airgap").unwrap().iter().map(|f| f.render()).collect();
        assert_eq!(
            flags,
            [
                "--opt dhcp=off",
                "--param dhcp.gateway=10.0.2.2",
                "--opt kernel.preset=nuked",
                "--param dhcp.static_ip=10.0.2.15/24",
                "--opt wireguard=off",
            ],
            "bundle order, aliases canonicalised to full paths"
        );
        let x = t.expand_opts(&["preset=airgap", "dhcp=on"], &["static_ip=10.0.0.9/24"]).unwrap();
        let ap = x.preset.as_ref().expect("the bundle is recorded");
        assert_eq!(ap.bundle, "airgap");
        assert_eq!(ap.expanded, flags, "…with the whole expansion");
        assert_eq!(x.opts, ["kernel.preset=nuked", "wireguard=off", "dhcp=on"], "the explicit dhcp=on replaced the bundle's dhcp=off");
        assert_eq!(x.params, ["dhcp.gateway=10.0.2.2", "static_ip=10.0.0.9/24"], "the explicit static_ip replaced the bundle's (alias matched by canonical path)");
        assert!(ap.describe().starts_with("preset = airgap → --opt dhcp=off"));
        // No preset on the line: nothing changes, nothing is recorded.
        let plain = t.expand_opts(&["dhcp=off"], &[] as &[&str]).unwrap();
        assert_eq!(plain, Expanded { opts: vec!["dhcp=off".into()], params: vec![], preset: None });
    }

    /// Every refusal by name: a bundle naming a path the tree lacks (at
    /// declaration time), a value the node refuses, the preset choosing
    /// itself, an unknown bundle, two bundles on one line, no presets at all.
    #[test]
    fn a_bundle_that_names_what_the_tree_lacks_is_refused_by_name() {
        let mut set = with_presets();
        let t = SettingsTree::of_set(&set, "gunnar-installer").unwrap();
        let e = format!("{:#}", t.expand_preset("nope").unwrap_err());
        assert!(e.contains("no preset 'nope'") && e.contains("airgap · hub"), "{e}");
        let e = format!("{:#}", t.expand_opts(&["preset=airgap", "preset=hub"], &[] as &[&str]).unwrap_err());
        assert!(e.contains("given twice"), "{e}");
        let plain = SettingsTree::of_set(&gunnar_like(), "gunnar-installer").unwrap();
        let e = format!("{:#}", plain.expand_opts(&["preset=airgap"], &[] as &[&str]).unwrap_err());
        assert!(e.contains("declares no presets"), "{e}");

        let inst = &mut set.installers[0];
        inst.presets.get_mut("hub").unwrap().insert("wifi".into(), "on".into());
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("preset 'hub'") && e.contains("'wifi' is not a setting of variant 'gunnar-installer'"), "{e}");
        inst.presets.get_mut("hub").unwrap().remove("wifi");

        inst.presets.get_mut("hub").unwrap().insert("wireguard".into(), "maybe".into());
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("takes on|off, not \"maybe\""), "{e}");
        inst.presets.get_mut("hub").unwrap().insert("wireguard".into(), "on".into());

        inst.presets.get_mut("hub").unwrap().insert("kernel.preset".into(), "huge".into());
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("takes one of full | normal | slim | nuked"), "{e}");
        inst.presets.get_mut("hub").unwrap().remove("kernel.preset");

        inst.presets.get_mut("hub").unwrap().insert("preset".into(), "airgap".into());
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("cannot choose the preset itself"), "{e}");
        inst.presets.get_mut("hub").unwrap().remove("preset");

        inst.presets.insert("two words".into(), Bundle::new());
        let e = format!("{:#}", inst.validate().unwrap_err());
        assert!(e.contains("no whitespace"), "{e}");
    }

    /// **LAW A over a requirement-carrying option** — the rows derive the tree
    /// and the tree renders the rows BACK, `kernel_requires` and all.
    ///
    /// Seen red 2026-08-30 (lane AF) on the first impl that ever declared the
    /// key: gunnar's three option rows each came back with an EMPTY
    /// `kernel_requires`, so `assert_bijective` reported
    /// `LAW A (rows ↔ tree): installer 'gunnar-installer': the tree renders
    /// different option rows than declared` — declaring a requirement broke the
    /// law by itself. Drop either `kernel_requires` carry in `feature_nodes` /
    /// `rows` and this test fails the same way.
    #[test]
    fn an_options_kernel_requires_survives_the_round_trip_through_the_tree() {
        use crate::kernel_requires::KconfigReq;
        let mut inst = InstallerBuild { name: "inst".into(), ..Default::default() };
        inst.options.push(crate::options::OptionSpec {
            name: "wireguard".into(),
            feature: "wg-client".into(),
            default: true,
            kernel_requires: vec![KconfigReq::y("CONFIG_TUN", "boringtun opens /dev/net/tun")],
            ..Default::default()
        });
        let tree = SettingsTree::of_installer(&inst, None);
        let node = tree.walk().into_iter().find(|n| n.path == "wireguard").expect("the option is a node");
        let NodeKind::Feature(f) = &node.kind else { panic!("a feature node") };
        assert_eq!(f.kernel_requires.len(), 1, "the requirement rides the NODE, not just the row");
        assert_eq!(f.kernel_requires[0].symbol, "CONFIG_TUN");
        assert_eq!(tree.rows().options, inst.options, "the tree renders the row back, requirement and all");
        assert!(crate::laws::law_rows_tree(&{
            let mut set = crate::variant::BuildVariantSet::default();
            set.installers.push(inst.clone());
            set
        })
        .is_empty());
    }


    /// **The tree splits an `--opt` line by asking what each path IS** — the
    /// fix for two measured defects at once.
    ///
    /// Seen red, both with the same sentence out of
    /// `options::parse_cli_options`, whose return type is `name → bool`:
    ///
    /// * `--opt kernel.preset=nuked` on the ISO-compose path — "an option is on
    ///   or off". gunnar's `BUILD-KEYED-INSTALLER.md` told operators not to type
    ///   the flag rather than the forge learning to read it;
    /// * `--opt data.fs=fat` — the same, and before that the path was not even
    ///   on the tree, so `--list-options` never showed it and lane N flipped a
    ///   row in a scratch worktree to build a FAT medium.
    ///
    /// Three buckets, and every refusal names the path and what it takes.
    #[test]
    fn the_tree_reads_a_choice_a_toggle_and_a_donors_pair_out_of_one_opt_line() {
        let set = gunnar_like();
        let t = SettingsTree::of_set(&set, "gunnar-installer").expect("declared");
        let split = t
            .split_opts(&["dhcp=off".to_string(), "data.fs=xfs".to_string(), "kernel.preset=nuked".to_string()])
            .expect("one line, three kinds");
        assert_eq!(split.features["dhcp"], false);
        assert_eq!(split.choices["data.fs"], "xfs");
        assert_eq!(split.behind_an_edge, ["kernel.preset=nuked"], "the donor's door judges its own declaration");
        // The old parser is exactly what could not do this.
        for pair in ["data.fs=xfs", "kernel.preset=nuked"] {
            let e = format!("{:#}", crate::options::parse_cli_options(&[pair]).unwrap_err());
            assert!(e.contains("an option is on or off"), "{pair}: {e}");
        }
        // RED 1: a val's value that is not on its roster, listing the roster.
        let e = format!("{:#}", t.split_opts(&["data.fs=ext4".to_string()]).unwrap_err());
        assert!(e.contains("data.fs") && e.contains("fat") && e.contains("xfs"), "{e}");
        // RED 2: a feature given a choice-shaped value.
        let e = format!("{:#}", t.split_opts(&["dhcp=nuked".to_string()]).unwrap_err());
        assert!(e.contains("is a feature") && e.contains("on or off"), "{e}");
        // RED 3: a --param path typed as an --opt says which flag it is.
        let e = format!("{:#}", t.split_opts(&["site=oden".to_string()]).unwrap_err());
        assert!(e.contains("--param site=oden"), "{e}");
        // RED 4: a path the variant does not declare, listing what it does.
        let e = format!("{:#}", t.split_opts(&["data.mount=noatime".to_string()]).unwrap_err());
        assert!(e.contains("not a setting of variant"), "{e}");
        // The short-name alias works here as everywhere else.
        assert_eq!(t.split_opts(&["fs=fat".to_string()]).unwrap().choices["data.fs"], "fat");
    }

    /// **A path under an edge that the consumer's tree does not draw is the
    /// donor's, not a typo** — the same rule [`crate::build_dag::BuildLine::canonical_in`]
    /// applies to the step line. `kernel.toolchain=off` is the donor kernel's
    /// OWN edge toggle: the consumer's tree draws the kernel's kconfig rows, not
    /// the donor's edges, so the compose door refused the very flag the chained
    /// kernel step had just consumed. MEASURED 2026-10-29 (tunnr-etcd NVMe and
    /// XFS media): the kernel step BUILT, then the installer compose said
    /// "'kernel.toolchain' is not a declared option".
    #[test]
    fn an_undrawn_path_under_an_edge_goes_to_the_donor_and_an_undrawn_root_is_still_refused() {
        let set = gunnar_like();
        let t = SettingsTree::of_set(&set, "gunnar-installer").expect("declared");
        let split = t
            .split_opts(&["kernel.toolchain=off".to_string(), "dhcp=off".to_string()])
            .expect("an edge-scoped flag is the donor's to judge");
        assert_eq!(split.behind_an_edge, ["kernel.toolchain=off"]);
        assert_eq!(split.features["dhcp"], false);
        // Not a hole: a root that is no edge is still refused by name.
        let e = format!("{:#}", t.split_opts(&["nonsense.toolchain=off".to_string()]).unwrap_err());
        assert!(e.contains("not a declared option"), "{e}");
    }

    /// **`--opt data.fs=` reaches the ROW, through the one writer both doors
    /// call** — and a val the tree offers that no door applies is refused
    /// rather than dropped.
    #[test]
    fn a_group_choice_lands_on_the_installer_row_and_an_unwired_val_is_refused() {
        let mut inst = gunnar_like().installers[0].clone();
        assert_eq!(inst.data.fs, crate::data::DataFs::Fat, "the fixture is at the default");
        let t = SettingsTree::of_installer(&inst, None);
        let split = t.split_opts(&["data.fs=xfs".to_string(), "data.io=uring".to_string()]).unwrap();
        inst.apply_group_choices(&split.choices).unwrap();
        assert_eq!(inst.data.fs, crate::data::DataFs::Xfs);
        assert_eq!(inst.data.io, crate::data::DataIo::Uring);
        // …and the kernel closure MOVES with it: the flag is not cosmetic.
        let mut set = gunnar_like();
        set.installers[0] = inst.clone();
        let c = crate::requires::closure_of_consumer(&set, "gunnar", &inst.name, &std::collections::BTreeMap::new())
            .unwrap()
            .unwrap();
        let rows = &c.on_edge("kernel").unwrap().as_kconfig().unwrap().rows;
        assert!(rows.contains_key("CONFIG_XFS_FS") && rows.contains_key("CONFIG_IO_URING"), "{:?}", rows.keys());
        // RED: a val nobody wired.
        let mut stray: BTreeMap<String, String> = BTreeMap::new();
        stray.insert("preset".into(), "x".into());
        let e = format!("{:#}", inst.apply_group_choices(&stray).unwrap_err());
        assert!(e.contains("no door applies"), "{e}");
    }

    #[test]
    fn an_edge_spelled_by_a_view_reads_back_to_the_same_edge() {
        let e = Edge::new("tunnr", "tunnr-kernel", ArtifactKind::Kernel);
        assert_eq!(e.spelled(), "tunnr/tunnr-kernel : kernel");
        assert_eq!(Edge::from_spelled(&e.spelled()), e);
        let bare = Edge::from_spelled("tunnr/tunnr-kernel");
        assert_eq!((bare.coordinate.as_str(), bare.typ), ("tunnr/tunnr-kernel", ArtifactKind::default()));
        assert!(ArtifactKind::Iso.takes_kernel_donor() && ArtifactKind::Installer.takes_kernel_donor());
        assert!(!ArtifactKind::Vm.takes_kernel_donor() && !ArtifactKind::Exe.takes_kernel_donor());
    }
}