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

//! **The SHARED kernel feature model** — a preset's kconfig symbol set as data:
//! names, states, the dependency forest, the cascade, and the committable fragment.
//!
//! # Why this lives HERE and not in a pane
//!
//! It used to live in dwarves-gui's `kernel_scene.rs`, fused to the 3D camera that
//! painted it — and that file admitted the shape in its own words: *"this is the join
//! edda's `KernelRun::unused_configs` performs, brought over"*. A brought-over join is
//! a TWIN (LAW 5), and it had the twin's usual consequence: any build thing that
//! wanted to answer "which features does this kernel offer, and what happens if I
//! drop one" had to parse the kconfig fragments itself, because the one existing
//! answer was welded to a renderer.
//!
//! The owner, 2026-08-26: *"kan vi få build things som drar en kernel att återvända
//! denna, så att inte varje build thing själv parsar kernel-config"*. This module is
//! that: the model with **no egui, no camera, no colour** in it. The pane renders
//! this; a CLI lists it; a build thing that composes a kernel asks it — one
//! derivation, three doors.
//!
//! # What the model IS
//!
//! Everything is derived from the COMMITTED fragments this crate already owns
//! ([`crate::kernel::BASE_FRAGMENT`] and friends) — no solver, no shell-out, no
//! container round-trip. The rules, unchanged from where they were proven:
//!
//! * **the parent relation is the Kconfig NAME hierarchy** — `P` parents `C` when `P`
//!   is the longest symbol present such that `C` starts with `P` followed by `_`;
//! * **contract** symbols are those [`crate::kernel::BASE_FRAGMENT`] names — the
//!   measured boot contract. Dropping one is refused BY NAME;
//! * **closure** symbols come from the tinyconfig dependency-closure fragment;
//! * the **cascade** runs both directions to a fixed point: a symbol whose parent is
//!   dropped is dropped (forward), and a non-contract symbol whose every child is
//!   dropped is dropped too (backward — "remove this part and then no need for this
//!   dependency"). A symbol with no derivable child is never dropped backward: its
//!   justification is simply not in this repository, and guessing would be a lie.

use std::collections::BTreeMap;

use anyhow::{anyhow, Result};

use crate::kernel::{
    kconfig_symbol, merge_kconfig, HardeningProfile, Preset, BASE_FRAGMENT,
    TINYCONFIG_DEPS_FRAGMENT,
};

/// One kconfig symbol in the picked set.
#[derive(Clone, Debug)]
pub struct KernelFeature {
    /// `CONFIG_X`.
    pub name: String,
    /// The merged directive verbatim (`CONFIG_X=y` or `# CONFIG_X is not set`).
    pub directive: String,
    /// `true` for a positive directive, `false` for the `is not set` spelling. A vetoed
    /// symbol is a feature the preset states as ABSENCE — that is the whole content of
    /// the smallest preset, so it is a node, not a gap.
    pub enabled: bool,
    /// Named by the measured boot contract. Dropping one is refused by name.
    pub contract: bool,
    /// Named by the tinyconfig dependency-closure fragment — a PARENT that exists only
    /// to enable something else.
    pub closure: bool,
    /// **What a RUN measured about this symbol**, when a report has been joined:
    /// `Some(hits)` summed over every function in every object the symbol compiles,
    /// `None` when the report does not mention it at all.
    ///
    /// `Some(0)` and `None` are DIFFERENT and must stay so: zero hits is a measured
    /// drop candidate, and absence is "no measurement covers this".
    pub run_hits: Option<u64>,
    /// Index of the derivable parent (the longest name-prefix symbol present), if any.
    pub parent: Option<usize>,
    /// Indices of the derivable children.
    pub children: Vec<usize>,
}

impl KernelFeature {
    /// Whether this symbol has no derivable edge at all — a consumer counts these and
    /// says so, because a node floating alone is a claim about the DERIVATION, not
    /// about the kernel.
    #[must_use]
    pub fn isolated(&self) -> bool {
        self.parent.is_none() && self.children.is_empty()
    }
}

/// What one symbol IS under a set of click-offs — the five states §10 names.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FeatureState {
    /// In the preset, nothing derivable depends on it, nobody turned it off.
    Kept,
    /// Something you kept still needs it — it cannot go, and [`KernelFeatureModel::holders`]
    /// names who.
    Locked,
    /// You turned this off, and the drop stands.
    ClickedOff,
    /// Nothing else needs it any more — it goes too. *The* answer to "remove this part
    /// and then no need for this dependency."
    CascadeDropped,
    /// Dropping it would break the measured boot contract (`base.config`). Refused BY
    /// NAME — never silently, and the drop does not take.
    Refused,
}

impl FeatureState {
    /// The stable token that rides in observable state and on the wire.
    #[must_use]
    pub fn key(self) -> &'static str {
        match self {
            FeatureState::Kept => "kept",
            FeatureState::Locked => "locked",
            FeatureState::ClickedOff => "clicked_off",
            FeatureState::CascadeDropped => "cascade_dropped",
            FeatureState::Refused => "refused",
        }
    }
}

/// The outcome of asking to toggle one symbol — the pure half of a click, so the CLI's
/// `--opt kernel.<symbol>=off` and a pane's pointer answer identically.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ToggleOutcome {
    /// The symbol was off and is back on; the new click-off set is inside the model's
    /// caller (it removed the name).
    On,
    /// The drop stands.
    Off,
    /// The drop would break the measured boot contract — nothing changed, and here are
    /// the contract symbols it would have broken, by name.
    Refused(Vec<String>),
}

/// The symbol names a fragment names, in order.
fn symbols_of(fragment: &str) -> Vec<&str> {
    fragment.lines().filter_map(kconfig_symbol).collect()
}

/// `true` when `parent` is a Kconfig name-hierarchy ancestor of `child`: the same name
/// plus `_` plus more. `CONFIG_NET` parents `CONFIG_NET_CORE` and does NOT parent
/// `CONFIG_NETDEVICES` — the underscore boundary is the whole rule, and without it the
/// relation would start inventing edges out of coincidental prefixes.
fn name_parents(parent: &str, child: &str) -> bool {
    child.len() > parent.len() + 1
        && child.starts_with(parent)
        && child.as_bytes()[parent.len()] == b'_'
}

/// Every symbol any committed fragment of the kind names — the UNIVERSE a preset's pick
/// is a subset of. Deduped, order-stable.
#[must_use]
pub fn committed_universe() -> Vec<String> {
    let mut out: Vec<String> = Vec::new();
    for frag in [
        TINYCONFIG_DEPS_FRAGMENT,
        BASE_FRAGMENT,
        crate::kernel::HARDENED_FRAGMENT,
        crate::kernel::K8S_HARDENED_FRAGMENT,
        // The absence overlay only reaches the universe through the smallest preset.
        Preset::Nuked.fragments()[2],
    ] {
        for s in symbols_of(frag) {
            if !out.iter().any(|k| k == s) {
                out.push(s.to_string());
            }
        }
    }
    out
}

/// **The derived model for one pick** — `preset × profile`, nothing else.
///
/// Owns no selection, no click-off set and no refusal: those are a CONSUMER's state
/// (a pane's pointer, a CLI's flags). Every method that needs them takes them as
/// arguments, which is exactly what lets two consumers hold different picks over one
/// derivation.
#[derive(Clone, Debug)]
pub struct KernelFeatureModel {
    /// The pick this model is true for.
    pub preset: Preset,
    /// …and its hardening half.
    pub profile: HardeningProfile,
    /// The symbols, in merged-fragment order.
    pub symbols: Vec<KernelFeature>,
    /// `(parent, child, parent_is_closure)` — the derivable forest, as edges.
    pub edges: Vec<(usize, usize, bool)>,
}

impl KernelFeatureModel {
    /// Derive the model for a pick. The ONLY constructor, so there is exactly one
    /// spelling of the parent rule and the contract join in the constellation.
    #[must_use]
    pub fn derive(preset: Preset, profile: HardeningProfile) -> Self {
        let mut frags = preset.fragments();
        frags.extend(profile.fragments());
        let merged = merge_kconfig(&frags);

        let contract: Vec<&str> = symbols_of(BASE_FRAGMENT);
        let closure: Vec<&str> = symbols_of(TINYCONFIG_DEPS_FRAGMENT);

        let mut symbols: Vec<KernelFeature> = merged
            .lines()
            .filter_map(|line| {
                let name = kconfig_symbol(line)?;
                Some(KernelFeature {
                    name: name.to_string(),
                    directive: line.trim().to_string(),
                    enabled: !line.trim_start().starts_with('#'),
                    contract: contract.contains(&name),
                    closure: closure.contains(&name),
                    run_hits: None,
                    parent: None,
                    children: Vec::new(),
                })
            })
            .collect();

        // The parent of a symbol is the LONGEST name-hierarchy ancestor present in this
        // pick — so `CONFIG_SECURITY_LOCKDOWN_LSM_EARLY` hangs off
        // `CONFIG_SECURITY_LOCKDOWN_LSM` and not off `CONFIG_SECURITY`, and the
        // relation is a forest rather than a transitive smear.
        for i in 0..symbols.len() {
            let mut best: Option<usize> = None;
            for j in 0..symbols.len() {
                if i == j || !name_parents(&symbols[j].name, &symbols[i].name) {
                    continue;
                }
                if best.is_none_or(|b| symbols[j].name.len() > symbols[b].name.len()) {
                    best = Some(j);
                }
            }
            symbols[i].parent = best;
        }
        let mut edges: Vec<(usize, usize, bool)> = Vec::new();
        for i in 0..symbols.len() {
            if let Some(p) = symbols[i].parent {
                symbols[p].children.push(i);
                let is_closure = symbols[p].closure;
                edges.push((p, i, is_closure));
            }
        }
        Self { preset, profile, symbols, edges }
    }

    /// **Join a RUN's measurement onto the derived graph.** `Some(0)` (measured and
    /// never entered) and `None` (no measurement covers this symbol) are kept apart
    /// deliberately: one is a drop candidate, the other is ignorance.
    pub fn join_run_hits(&mut self, hits: &BTreeMap<String, u64>) {
        for sym in &mut self.symbols {
            sym.run_hits = hits.get(&sym.name).copied();
        }
    }

    /// Which symbols go if `off` goes, as a per-index mask.
    ///
    /// Two directions, and both are the same fact read from opposite ends:
    ///
    /// * **forward** — a symbol whose parent is dropped is dropped: it cannot exist
    ///   without what enables it;
    /// * **backward** — a symbol that has children, is not itself in the boot
    ///   contract, and has NO kept child left, is dropped: nothing needs it any more.
    ///
    /// A symbol with no derivable child is NEVER dropped backward — its justification
    /// is simply not in this repository, and guessing would be the lie the module docs
    /// refuse.
    #[must_use]
    pub fn cascade_for(&self, off: &[String]) -> Vec<bool> {
        let mut dropped = vec![false; self.symbols.len()];
        for name in off {
            if let Some(i) = self.symbols.iter().position(|s| &s.name == name) {
                dropped[i] = true;
            }
        }
        // Iterate to a fixed point — a backward drop can trigger another forward one.
        loop {
            let mut changed = false;
            for i in 0..self.symbols.len() {
                if dropped[i] {
                    continue;
                }
                let s = &self.symbols[i];
                let parent_gone = s.parent.map(|p| dropped[p]).unwrap_or(false);
                let orphaned = !s.children.is_empty()
                    && !s.contract
                    && s.children.iter().all(|&c| dropped[c]);
                if parent_gone || orphaned {
                    dropped[i] = true;
                    changed = true;
                }
            }
            if !changed {
                break;
            }
        }
        dropped
    }

    /// The state of every symbol, in `symbols` order, under a click-off set and an
    /// optional standing refusal `(clicked, broken)`.
    #[must_use]
    pub fn states(
        &self,
        clicked_off: &[String],
        refusal: Option<(&str, &[String])>,
    ) -> Vec<FeatureState> {
        let dropped = self.cascade_for(clicked_off);
        let refused: Vec<&str> = match refusal {
            Some((clicked, broken)) => {
                let mut v: Vec<&str> = broken.iter().map(String::as_str).collect();
                v.push(clicked);
                v
            }
            None => Vec::new(),
        };
        (0..self.symbols.len())
            .map(|i| {
                let s = &self.symbols[i];
                if refused.contains(&s.name.as_str()) {
                    FeatureState::Refused
                } else if clicked_off.iter().any(|n| n == &s.name) {
                    FeatureState::ClickedOff
                } else if dropped[i] {
                    FeatureState::CascadeDropped
                } else if s.children.iter().any(|&c| !dropped[c]) {
                    FeatureState::Locked
                } else {
                    FeatureState::Kept
                }
            })
            .collect()
    }

    /// Who still holds `i` — the kept children that keep a `Locked` symbol alive.
    #[must_use]
    pub fn holders(&self, i: usize, dropped: &[bool]) -> Vec<String> {
        self.symbols[i]
            .children
            .iter()
            .filter(|&&c| !dropped[c])
            .map(|&c| self.symbols[c].name.clone())
            .collect()
    }

    /// **Ask to toggle one symbol** — the pure decision half of a click, shared by the
    /// pane's pointer and the CLI's `--opt kernel.<symbol>=off`.
    ///
    /// `clicked_off` is MUTATED on success (the name added or removed) and untouched on
    /// a refusal. A drop that would reach the measured boot contract is REFUSED: the
    /// returned list names the contract symbols it would have broken, which is the
    /// difference between a door that says no and a door that says nothing.
    pub fn try_toggle(&self, clicked_off: &mut Vec<String>, name: &str) -> Option<ToggleOutcome> {
        self.symbols.iter().position(|s| s.name == name)?;
        if let Some(pos) = clicked_off.iter().position(|n| n == name) {
            clicked_off.remove(pos);
            return Some(ToggleOutcome::On);
        }
        // Would this drop reach the contract? Ask BEFORE committing it.
        let mut trial = clicked_off.clone();
        trial.push(name.to_string());
        let dropped = self.cascade_for(&trial);
        let broken: Vec<String> = dropped
            .iter()
            .enumerate()
            .filter(|&(j, &d)| d && self.symbols[j].contract)
            .map(|(j, _)| self.symbols[j].name.clone())
            .collect();
        if !broken.is_empty() {
            return Some(ToggleOutcome::Refused(broken));
        }
        *clicked_off = trial;
        Some(ToggleOutcome::Off)
    }

    /// **THE DECISION, AS A KCONFIG FRAGMENT** — what a click (or a `--opt`) is FOR.
    ///
    /// Renders the current pick as a fragment a person can commit beside
    /// `nuked.config` and name in `kconfig_fragment`, so a decision made anywhere —
    /// the pane's pointer or the CLI's flags — reaches a real build.
    ///
    /// Every dropped symbol is written as `# CONFIG_X is not set` — the veto
    /// spelling, NOT an omission — because the canary catches a veto creeping back in
    /// and cannot catch a line that was never written. The cascade is included:
    /// dropping a parent drops its children, and the fragment says all of them so the
    /// file states the whole consequence rather than the click.
    ///
    /// `evidence_source` names the RUN report whose hit counts were joined via
    /// [`join_run_hits`](Self::join_run_hits), or `None` when nothing was measured.
    /// The header records WHY each symbol was dropped, and the evidence is exactly as
    /// strong as the model holds — it never claims a measurement it does not hold,
    /// and a click AGAINST a measurement is written down as such.
    #[must_use]
    pub fn decision_fragment(&self, clicked_off: &[String], evidence_source: Option<&str>) -> String {
        fn strip_config(name: &str) -> &str {
            name.strip_prefix("CONFIG_").unwrap_or(name)
        }
        let dropped = self.cascade_for(clicked_off);
        let mut out = String::new();
        out.push_str("# nordisk KERNEL DECISION — rendered from the shared kernel feature model.\n#\n");
        out.push_str(&format!(
            "# preset = {} · profile = {}\n",
            self.preset.as_str(),
            self.profile.as_str()
        ));
        match evidence_source {
            Some(src) => out.push_str(&format!(
                "# evidence: a RUN report from {src} — a symbol marked `never entered` was\n                 #           MEASURED at zero hits by a profiled boot, not guessed.\n",
            )),
            None => out.push_str(
                "# evidence: NONE. No RUN report was loaded, so every line below is the\n                 #           operator's choice and nothing here was measured.\n",
            ),
        }
        out.push_str(
            "#\n# Each drop is written as a VETO (`# CONFIG_X is not set`) rather than left\n             # out: the build's canary catches a veto that creeps back in, and cannot catch\n             # a line nobody wrote. Cascade drops are included, so this file states the whole\n             # consequence of the clicks and not just the clicks.\n\n",
        );
        let mut any = false;
        for (i, sym) in self.symbols.iter().enumerate() {
            if !dropped[i] {
                continue;
            }
            any = true;
            let why = if clicked_off.iter().any(|n| n == &sym.name) {
                match sym.run_hits {
                    Some(0) => "clicked off; the RUN measured ZERO hits",
                    Some(h) => {
                        out.push_str(&format!(
                            "# WARNING: {} was clicked off although the RUN measured {h} hit(s).\n",
                            sym.name
                        ));
                        "clicked off AGAINST the measurement"
                    }
                    None => "clicked off; no measurement covers this symbol",
                }
            } else {
                "dropped by cascade — nothing kept needs it"
            };
            out.push_str(&format!("# {why}\n# CONFIG_{} is not set\n", strip_config(&sym.name)));
        }
        if !any {
            out.push_str("# (nothing is dropped — this fragment changes nothing)\n");
        }
        out
    }
}

// ── the OPTION-ROW emission: the model as a declared vocabulary ──────────────

/// The `--opt kernel.<name>` handle for a kconfig symbol: `CONFIG_` stripped,
/// lowercased, `_` → `-`. `CONFIG_VIRTIO_NET` → `virtio-net`. One spelling, one
/// direction — the reverse lookup is [`KernelFeatureModel::symbol_for_option`],
/// which walks THIS function over the model rather than parsing the kebab form
/// back (a second grammar would be a second chance to disagree).
#[must_use]
pub fn option_name(symbol: &str) -> String {
    symbol
        .strip_prefix("CONFIG_")
        .unwrap_or(symbol)
        .to_ascii_lowercase()
        .replace('_', "-")
}

/// **One declared option row** — a kconfig symbol of the pick, spoken in the
/// same vocabulary as an installer's [`OptionSpec`](crate::options::OptionSpec)
/// row: a name the CLI types, a default, and what refuses when you turn it off.
///
/// Presets are NOT a separate mechanism: a preset is the named DEFAULT-BUNDLE
/// these rows start from ([`crate::kernel::Preset::ALL`]), and `default_on` is
/// literally "enabled in the chosen preset".
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KernelOptionRow {
    /// The `--opt kernel.<option>` handle ([`option_name`] of the symbol).
    pub option: String,
    /// The kconfig symbol, verbatim: `CONFIG_VIRTIO_NET`.
    pub symbol: String,
    /// Enabled in the preset — the state when nobody says otherwise. `false`
    /// is a symbol the preset states as ABSENCE (a veto row is still a row).
    pub default_on: bool,
    /// The derivable parent this symbol requires, if the model holds one
    /// (the Kconfig name hierarchy — dropping the parent drops this).
    pub requires: Option<String>,
    /// **The contract lock**: the boot-contract symbols that break if this row
    /// is turned off, asked of [`KernelFeatureModel::try_toggle`] itself — the
    /// same door that will refuse the actual `--opt`, so the listing and the
    /// refusal cannot disagree. Empty ⇒ the row may be turned off.
    pub locked_by: Vec<String>,
}

impl KernelOptionRow {
    /// One row, one line — what `--list-options` prints and what a pane's
    /// tooltip says. `kernel.virtio-net  CONFIG_VIRTIO_NET  default=on
    /// requires=CONFIG_VIRTIO  LOCKED(breaks CONFIG_VIRTIO_NET)`.
    #[must_use]
    pub fn describe(&self) -> String {
        let mut s = format!(
            "kernel.{}  {}  default={}",
            self.option,
            self.symbol,
            if self.default_on { "on" } else { "off" }
        );
        if let Some(req) = &self.requires {
            s.push_str(&format!("  requires={req}"));
        }
        if !self.locked_by.is_empty() {
            s.push_str(&format!("  LOCKED(breaks {})", self.locked_by.join(", ")));
        }
        s
    }
}

impl KernelFeatureModel {
    /// **The declared rows, one per symbol of the pick** — the kernel half of
    /// `--list-options`, in merged-fragment order (the same order the pane
    /// draws). Every symbol is a row, including the vetoed ones: the smallest
    /// preset consists ENTIRELY of stated absences, and a vocabulary that
    /// omitted them would list nothing exactly where the preset says the most.
    #[must_use]
    pub fn option_rows(&self) -> Vec<KernelOptionRow> {
        self.symbols
            .iter()
            .map(|s| {
                // Ask the toggling door itself whether this row is locked, from
                // a clean slate: a refusal here IS the refusal the CLI would
                // get, so the listing can never advertise a drop the door
                // refuses (or vice versa).
                let mut probe: Vec<String> = Vec::new();
                let locked_by = match self.try_toggle(&mut probe, &s.name) {
                    Some(ToggleOutcome::Refused(broken)) => broken,
                    _ => Vec::new(),
                };
                KernelOptionRow {
                    option: option_name(&s.name),
                    symbol: s.name.clone(),
                    default_on: s.enabled,
                    requires: s.parent.map(|p| self.symbols[p].name.clone()),
                    locked_by,
                }
            })
            .collect()
    }

    /// The kconfig symbol behind an `--opt kernel.<option>` handle, or `None`
    /// when the pick declares no such row (the caller's refusal then lists
    /// [`option_rows`](Self::option_rows) — never applies the flag to nothing).
    #[must_use]
    pub fn symbol_for_option(&self, option: &str) -> Option<&str> {
        self.symbols
            .iter()
            .find(|s| option_name(&s.name) == option)
            .map(|s| s.name.as_str())
    }
}

// ── the CLI resolver: `--opt kernel.*` through the SAME door as the pane ─────

/// **What the flags decided** — one preset pick plus the click-off set that
/// survived [`KernelFeatureModel::try_toggle`], and the fragment that states it.
///
/// The pane's pointer and the CLI's `--opt` both end here, and the fragment is
/// rendered by the one writer ([`KernelFeatureModel::decision_fragment`]), so
/// the same decision is the same bytes whichever door it came through.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KernelDecision {
    /// The default-bundle the rows started from (declared, or overridden by
    /// `kernel.preset=<name>`).
    pub preset: Preset,
    /// The hardening half of the pick — the declaration's, never a flag's.
    pub profile: HardeningProfile,
    /// The symbols the operator turned off, exactly as the model accepted them.
    pub clicked_off: Vec<String>,
    /// [`KernelFeatureModel::decision_fragment`] over `clicked_off` — the
    /// committable veto fragment, cascade included.
    pub fragment: String,
    /// `kernel.coverage=<probe>`: the DEV probe kernel
    /// ([`crate::kernel_trim::coverage`]), landed by
    /// [`crate::kernel::KernelBuild::apply_decision`]. `None` = the row's kernel.
    pub probe: Option<crate::kernel_trim::coverage::KernelProbe>,
}

impl KernelDecision {
    /// Whether the fragment actually vetoes anything — the difference between
    /// "merge this last" and "there is nothing to merge". A comment-only
    /// fragment must not ride into a build's content id.
    #[must_use]
    pub fn changes_anything(&self) -> bool {
        self.fragment.lines().any(|l| kconfig_symbol(l).is_some())
    }
}

/// **A decision fragment, READ BACK** — what [`parse_decision_fragment`]
/// recovers from the committed text: the pick and the clicks, exactly the
/// inputs [`KernelFeatureModel::decision_fragment`] rendered from.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParsedKernelDecision {
    /// The preset the header names.
    pub preset: Preset,
    /// The profile the header names.
    pub profile: HardeningProfile,
    /// The symbols whose why-line attributes the veto to a CLICK
    /// (`clicked off…`), in fragment order — the operator's own decisions.
    /// Cascade drops are deliberately NOT here: they are consequences, and
    /// re-deriving the model from the pick regenerates them.
    pub clicked_off: Vec<String>,
    /// Every vetoed symbol — clicks and cascade together, in fragment order.
    pub vetoed: Vec<String>,
}

/// **Parse a decision fragment back into the decision** — the inverse of
/// [`KernelFeatureModel::decision_fragment`], which was designed to carry it:
/// the header states `preset` and `profile`, and every veto line sits under a
/// why-line saying whether it was a click or a cascade.
///
/// The round trip is a bijection law (see [`crate::laws`]):
/// `parse(render(preset, profile, clicked_off))` yields the same pick and the
/// same click set, and the parsed `vetoed` list equals the model's
/// [`cascade_for`](KernelFeatureModel::cascade_for) over those clicks.
///
/// # Errors
///
/// * no `# preset = … · profile = …` header, or two of them — a fragment that
///   does not carry its pick cannot be read back, and the render half always
///   writes exactly one;
/// * an unknown or empty preset / profile name in the header;
/// * a line that SETS a value (`CONFIG_X=y`) — a decision fragment can only
///   veto, so a value line means this text is not a decision fragment.
pub fn parse_decision_fragment(fragment: &str) -> Result<ParsedKernelDecision> {
    let mut pick: Option<(Preset, HardeningProfile)> = None;
    let mut clicked_off: Vec<String> = Vec::new();
    let mut vetoed: Vec<String> = Vec::new();
    let mut prev: Option<String> = None;
    for raw in fragment.lines() {
        let line = raw.trim().to_string();
        if let Some(rest) = line.strip_prefix("# preset = ") {
            if pick.is_some() {
                return Err(anyhow!(
                    "two '# preset = …' header lines — which pick is this fragment for?"
                ));
            }
            let Some((p, pr)) = rest.split_once(" · profile = ") else {
                return Err(anyhow!(
                    "the header line does not carry ' · profile = …': {line:?}"
                ));
            };
            if p.trim().is_empty() || pr.trim().is_empty() {
                return Err(anyhow!("the header names an empty preset or profile: {line:?}"));
            }
            let preset = Preset::parse(p).ok_or_else(|| {
                anyhow!(
                    "the header names unknown preset '{p}'; declared bundles: {}",
                    Preset::ALL.map(|x| x.as_str()).join(" · ")
                )
            })?;
            let profile = HardeningProfile::parse(pr)
                .ok_or_else(|| anyhow!("the header names unknown profile '{pr}'"))?;
            pick = Some((preset, profile));
        } else if let Some(sym) = kconfig_symbol(&line) {
            if !line.starts_with('#') {
                return Err(anyhow!(
                    "'{line}' SETS a value — a decision fragment can only veto \
                     (`# CONFIG_X is not set`), so this text is not one"
                ));
            }
            vetoed.push(sym.to_string());
            let why = prev.as_deref().unwrap_or("");
            if why.trim_start_matches('#').trim_start().starts_with("clicked off") {
                clicked_off.push(sym.to_string());
            }
        }
        prev = Some(line);
    }
    let Some((preset, profile)) = pick else {
        return Err(anyhow!(
            "no '# preset = … · profile = …' header — this text is not a decision \
             fragment (or the render half stopped writing the pick, which breaks \
             the decision ↔ fragment bijection)"
        ));
    };
    Ok(ParsedKernelDecision { preset, profile, clicked_off, vetoed })
}

/// Split a raw `--opt` list into the kernel pairs and the rest.
///
/// `kernel.preset=nuked` → `("preset", "nuked")`; `kernel.virtio-net=off` →
/// `("virtio-net", "off")`; everything not `kernel.`-prefixed passes through
/// verbatim for the installer-option path. The split happens BEFORE
/// [`crate::options::parse_cli_options`] because a kernel pair's value is not
/// `on|off` only (`preset=` carries a name), and because an installer resolver
/// handed `kernel.preset` would refuse it as an undeclared option — true words
/// about the wrong subject.
///
/// # Errors
///
/// A `kernel.`-prefixed entry with no `=`, or with an empty name or value, is
/// refused here — malformed is malformed whichever axis it meant.
pub fn split_kernel_opts(raw: &[String]) -> Result<(Vec<(String, String)>, Vec<String>)> {
    let mut kernel: Vec<(String, String)> = Vec::new();
    let mut rest: Vec<String> = Vec::new();
    for o in raw {
        let Some(k) = o.strip_prefix("kernel.") else {
            rest.push(o.clone());
            continue;
        };
        let Some((name, value)) = k.split_once('=') else {
            return Err(anyhow!(
                "--opt kernel.{k}: a kernel option is `kernel.<name>=<value>` \
                 (`kernel.preset=slim`, `kernel.virtio-net=off`)"
            ));
        };
        if name.trim().is_empty() || value.trim().is_empty() {
            return Err(anyhow!("--opt {o}: empty name or value"));
        }
        kernel.push((name.trim().to_string(), value.trim().to_string()));
    }
    Ok((kernel, rest))
}

/// **Resolve `kernel.preset=<p>` + `kernel.<symbol>=on|off` pairs into a
/// [`KernelDecision`]** — the CLI's whole kernel grammar, in declaration order,
/// every step through [`KernelFeatureModel::try_toggle`].
///
/// `declared_preset` / `declared_profile` are the `[[build.kernel]]` row's own
/// pick — the state when nobody says otherwise, exactly as an installer
/// option's `default` is.
///
/// # Refusals — every one BY NAME, never a silent ignore
///
/// * a second `kernel.preset=` (which one was meant?);
/// * an unknown preset name, listing [`Preset::ALL`];
/// * an unknown `kernel.<name>`, listing the declared rows;
/// * a value that is neither `on` nor `off` for a symbol row;
/// * `on` for a symbol the preset vetoes — the decision fragment can only veto,
///   so "add this to the bundle" means picking a bundle that has it;
/// * **a contract-breaking `off`** — [`ToggleOutcome::Refused`] surfaces as an
///   error naming the boot-contract symbols the drop would have broken.
pub fn resolve_kernel_cli(
    declared_preset: Preset,
    declared_profile: HardeningProfile,
    pairs: &[(String, String)],
) -> Result<KernelDecision> {
    use crate::tree::{SettingsTree, ToggleRequest};
    // The tree of the DECLARED pick judges; the preset choice rides `vals`
    // (a val is chosen, not toggled) and re-derives the model inside the tree.
    let declared = SettingsTree::of_kernel_pick("kernel", declared_preset, declared_profile);
    let mut vals: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
    for (name, value) in pairs {
        if name != "preset" {
            continue;
        }
        if vals.contains_key("preset") {
            return Err(anyhow!("kernel.preset given twice — one bundle per build"));
        }
        Preset::parse(value).ok_or_else(|| {
            anyhow!(
                "kernel.preset={value}: unknown preset; declared bundles: {}",
                Preset::ALL.map(|p| p.as_str()).join(" · ")
            )
        })?;
        vals.insert("preset".to_string(), value.trim().to_ascii_lowercase());
    }
    // **`profile` is the kernel subtree's OTHER val, and it is chosen the same
    // way.** `kernel_nodes` has rendered `preset` and `profile` side by side as
    // vals since the tree landed, and ⚒ hands BYGG every flag the tree spells —
    // so the pane offered `--opt kernel.profile=<p>` and this door refused it with
    // *"a symbol row takes on|off"*, because the loop below judges anything that is
    // not `preset` as a feature. A flag the pane renders and the prompt refuses is
    // law C broken across the doors; found 2026-08-27 by the ⚒ lane comparing the
    // two doors' plans on gunnar's real declaration, where it made
    // `BYGG kärnan` unrunnable.
    for (name, value) in pairs {
        if name != "profile" {
            continue;
        }
        if vals.contains_key("profile") {
            return Err(anyhow!("kernel.profile given twice — one hardening profile per build"));
        }
        HardeningProfile::parse(value).ok_or_else(|| {
            anyhow!("kernel.profile={value}: unknown profile; declared profiles: {}", HardeningProfile::names())
        })?;
        vals.insert("profile".to_string(), value.trim().to_ascii_lowercase());
    }
    // The tree the chosen preset spells — the rows the operator's `kernel.<name>`
    // handles resolve against are the rows of THAT bundle. The profile picks which
    // overlay layers on top, and so which fragment the decision renders.
    let preset = vals.get("preset").and_then(|v| Preset::parse(v)).unwrap_or(declared_preset);
    let profile = vals.get("profile").and_then(|v| HardeningProfile::parse(v)).unwrap_or(declared_profile);
    let tree = if preset == declared_preset && profile == declared_profile {
        declared
    } else {
        SettingsTree::of_kernel_pick("kernel", preset, profile)
    };
    let mut req = ToggleRequest { variant: "kernel".to_string(), path: String::new(), on: false, state: Default::default(), vals: vals.clone() };
    for (name, value) in pairs {
        // `coverage` is the DEV probe val (`kernel_trim::coverage`), landed by
        // `chosen_kernel_for` beside the decision, never a symbol row.
        if name == "preset" || name == "profile" || name == crate::kernel_trim::coverage::COVERAGE_OPT {
            continue;
        }
        if tree.get(name).is_none() {
            return Err(anyhow!(
                "--opt kernel.{name}: no such symbol in the {} pick; declared rows: {}",
                preset.as_str(),
                tree.walk()
                    .into_iter()
                    .filter(|n| matches!(n.kind, crate::tree::NodeKind::Feature(_)))
                    .map(|n| n.path.clone())
                    .collect::<Vec<_>>()
                    .join(" · ")
            ));
        }
        req.path = name.clone();
        req.on = match value.as_str() {
            "off" => false,
            "on" => true,
            other => {
                return Err(anyhow!(
                    "--opt kernel.{name}={other}: a symbol row takes on|off \
                     (kernel.preset takes a bundle name)"
                ));
            }
        };
        let d = tree.try_toggle(&req);
        if !d.verdict.ok {
            let why = d.verdict.line();
            return Err(if why.contains("vetoes") {
                anyhow!("--opt kernel.{name}=on: the {} bundle VETOES it — {why}", preset.as_str())
            } else {
                anyhow!("--opt kernel.{name}={value}: {why}")
            });
        }
        if req.on {
            req.state.remove(&d.path);
        } else {
            req.state.insert(d.path, false);
        }
    }
    let mut d = tree.kernel_decision("", &req.state, &vals)?;
    let probe: Vec<(String, String)> = pairs.iter().filter(|(n, _)| n == crate::kernel_trim::coverage::COVERAGE_OPT).cloned().collect();
    d.probe = crate::kernel_trim::coverage::probe_of_pairs(&probe)?;
    Ok(d)
}

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

    /// The name-hierarchy rule, watched failing on the coincidental prefix it exists to
    /// reject. Before the underscore-boundary check `CONFIG_NET` "parented"
    /// `CONFIG_NETDEVICES`, which is a different symbol and would have put a fabricated
    /// edge in the cascade. (Moved here from dwarves-gui with the function itself.)
    #[test]
    fn the_parent_relation_stops_at_an_underscore() {
        assert!(name_parents("CONFIG_NET", "CONFIG_NET_CORE"));
        assert!(name_parents("CONFIG_VIRTIO", "CONFIG_VIRTIO_PCI"));
        assert!(!name_parents("CONFIG_NET", "CONFIG_NETDEVICES"));
        assert!(!name_parents("CONFIG_SECURITY", "CONFIG_SECURITYFS"));
        assert!(!name_parents("CONFIG_NET", "CONFIG_NET"));
    }

    /// The model this file replaced lived in dwarves-gui and measured these numbers on
    /// its own fixtures. They are re-asserted HERE so the lowering is provably the
    /// same derivation and not a rewrite that drifted.
    #[test]
    fn the_lowered_model_matches_the_measured_pane_numbers() {
        let m = KernelFeatureModel::derive(Preset::Normal, HardeningProfile::Vanilla);
        let slim = KernelFeatureModel::derive(Preset::Slim, HardeningProfile::Vanilla);
        let nuked = KernelFeatureModel::derive(Preset::Nuked, HardeningProfile::Vanilla);
        let iso = |m: &KernelFeatureModel| m.symbols.iter().filter(|s| s.isolated()).count();
        eprintln!("PANE-NUMBERS normal={}/{} slim={}/{} nuked={}/{} universe={}", m.symbols.len(), iso(&m), slim.symbols.len(), iso(&slim), nuked.symbols.len(), iso(&nuked), committed_universe().len());
        // Re-measured 2026-08-28. The pane's original line was 16/8 · 32/16 · 48/29 ·
        // universe 73. Two moves since, both in base.config (the contract, so
        // EVERY preset grows by the same count): 2ae5bdc (2026-08-27) added the
        // eleven the live nuked kernel died without (PROC_FS, MEMFD_CREATE, SHMEM,
        // TMPFS, FILE_LOCKING, EPOLL, EVENTFD, TIMERFD, SIGNALFD, FUTEX,
        // POSIX_TIMERS) and left this test red at 16; and CONFIG_SYSFS (the one
        // the installer died without — `kernel::PID1_BOOT_CONTRACT`) is the
        // twelfth. 16+12 = 28, and all twelve are isolated (no name-parent in
        // the set), so the isolated count grows by the same twelve.
        // Re-measured 2026-08-29 (evening): CONFIG_SMP joins base.config — the
        // two-vCPU UpCloud appliance was serving on one core with tinyconfig's
        // SMP off. One isolated node, so every preset and the universe grow by
        // exactly one: normal 32→33/22→23, slim 48→49/30→31, nuked 68→69/45→46,
        // universe 93→94. Lane K (same day): CONFIG_NR_CPUS=64 pinned beside
        // SMP (olddefconfig only lifted it to 2 on the tinyconfig base —
        // measured) — one more isolated node: normal 34/24, slim 50/32,
        // nuked 70/47, universe 95.
        assert_eq!(m.symbols.len(), 33, "normal is 33 nodes (lane AE 2026-08-30: CONFIG_TUN left base.config for gunnar's `wireguard` option)");
        assert_eq!(iso(&m), 23, "normal: 23 isolated (TUN had no name-parent in the set)");
        assert_eq!(slim.symbols.len(), 48, "slim is 48 (lane AE: −CONFIG_TUN from base.config, −CONFIG_MULTIUSER from the tinyconfig closure)");
        assert_eq!(iso(&slim), 30, "slim: 30 isolated (both removals were isolated nodes)");
        // Re-measured 2026-08-29: nuked.config vetoes IPv6 by name (IPV6 + six
        // children: SIT, TUNNEL, SEG6_LWTUNNEL, IOAM6_LWTUNNEL, INET6_AH,
        // INET6_ESP — the user's law, seen alive on the UpCloud VGA console).
        // Seven rows, of which the two INET6_* have no name-parent in the set,
        // so nuked grows 61→68 and its isolated count 43→45; the universe 86→93.
        // Lane HOLGER-MEDIA (2026-10-24): the MULTIUSER veto left nuked.config for the
        // appliance-boot contract (a consumer's default, lifted by name), so nuked is
        // 68 and its isolated count 45 (the veto had no name-parent in the set).
        assert_eq!(nuked.symbols.len(), 68, "nuked is 68 (lane AE: −CONFIG_TUN; lane HOLGER-MEDIA: −CONFIG_MULTIUSER, moved to the appliance-boot contract)");
        assert_eq!(iso(&nuked), 45, "nuked: 45 isolated");
        // Re-measured 2026-09-07 (F46): the hardened overlay grew by a NET
        // SIXTEEN symbols and the universe with it, 94 → 110. Eighteen in —
        // EXPERT (the prompt the vetoes need), RANDOMIZE_KSTACK_OFFSET (the
        // parent the child was missing), ZERO_CALL_USED_REGS, PANIC_ON_OOPS,
        // SECCOMP, SECCOMP_FILTER, STATIC_USERMODEHELPER(+_PATH),
        // CPU_MITIGATIONS, MITIGATION_RETPOLINE,
        // MITIGATION_PAGE_TABLE_ISOLATION and the seven attack-surface vetoes
        // (KEXEC, DEVMEM, BLK_DEV_IO_TRACE, DEBUG_FS, BPF_SYSCALL,
        // USERFAULTFD, PROC_KCORE) — and two OUT: STRICT_DEVMEM and
        // IO_STRICT_DEVMEM, replaced by vetoing DEVMEM itself, which is
        // strictly stronger than filtering it. The three per-preset counts
        // above are unmoved because they are all `HardeningProfile::Vanilla`
        // picks: the overlay is not in them, which is the point.
        // 2026-10-29: the `k8s-hardened` overlay names BPF_SYSCALL (already in
        // the universe, hardened vetoes it) and three new symbols — BPF_JIT,
        // BPF_JIT_ALWAYS_ON, BPF_UNPRIV_DEFAULT_OFF — so 109 → 112.
        assert_eq!(committed_universe().len(), 112, "the committed universe (F46 2026-09-07: the hardened overlay's KSPP closure; lane HOLGER-MEDIA 2026-10-24: −CONFIG_MULTIUSER, no longer a preset line; 2026-10-29: +3 from k8s-hardened)");
    }

    /// The option vocabulary: kebab handles, defaults from the preset, the parent
    /// relation as `requires`, and the contract lock asked of `try_toggle` itself.
    /// Watched red 2026-08-26 with `option_name` returning the symbol verbatim —
    /// the kebab assertions and the `symbol_for_option` round-trip both failed.
    #[test]
    fn option_rows_speak_the_declared_vocabulary() {
        let m = KernelFeatureModel::derive(Preset::Nuked, HardeningProfile::Vanilla);
        let rows = m.option_rows();
        assert_eq!(rows.len(), m.symbols.len(), "every symbol is a row");

        let net = rows.iter().find(|r| r.symbol == "CONFIG_VIRTIO_NET").expect("virtio-net row");
        assert_eq!(net.option, "virtio-net", "kebab-case of CONFIG_X without the prefix");
        assert!(net.default_on, "in the nuked preset virtio-net is ENABLED");
        assert_eq!(net.requires.as_deref(), Some("CONFIG_VIRTIO"), "the parent relation");
        assert!(
            net.locked_by.iter().any(|b| b == "CONFIG_VIRTIO_NET"),
            "a contract symbol is locked and names itself among the broken: {:?}",
            net.locked_by
        );

        // The holder is locked BY its contract children — the same refusal
        // try_toggle gives, so listing and door agree by construction.
        let virtio = rows.iter().find(|r| r.symbol == "CONFIG_VIRTIO").expect("virtio row");
        assert!(
            virtio.locked_by.iter().any(|b| b == "CONFIG_VIRTIO_NET"),
            "dropping the parent breaks the contract children: {:?}",
            virtio.locked_by
        );

        // A vetoed symbol is a ROW with default=off, not a gap — the nuked
        // preset is made of stated absences.
        assert!(
            rows.iter().any(|r| !r.default_on),
            "the nuked pick must list vetoed rows as default=off"
        );

        // The reverse lookup is the same spelling walked backwards.
        assert_eq!(m.symbol_for_option("virtio-net"), Some("CONFIG_VIRTIO_NET"));
        assert_eq!(m.symbol_for_option("no-such-thing"), None, "unknown handle is None, never a guess");

        // The one-line rendering carries the lock and the requirement.
        let line = net.describe();
        assert!(line.starts_with("kernel.virtio-net  CONFIG_VIRTIO_NET  default=on"), "{line}");
        assert!(line.contains("requires=CONFIG_VIRTIO"), "{line}");
        assert!(line.contains("LOCKED("), "{line}");
    }

    /// Presets are named default-bundles, not a separate mechanism: every name in
    /// `Preset::ALL` parses back to itself, so the refusal listing and the parser
    /// cannot drift. Watched red 2026-08-26 with `ALL` misspelling a name.
    #[test]
    fn the_preset_bundles_are_the_parsers_own_names() {
        for p in Preset::ALL {
            assert_eq!(Preset::parse(p.as_str()), Some(p), "{}", p.as_str());
        }
        assert_eq!(Preset::ALL.len(), 4);
    }

    /// The split: kernel pairs out, installer opts through verbatim, malformed
    /// refused. Watched red 2026-08-26 with the split keeping the `kernel.` prefix
    /// on the pair names.
    #[test]
    fn the_split_carries_kernel_pairs_and_passes_the_rest_through() {
        let raw: Vec<String> = [
            "wireguard=off",
            "kernel.preset=nuked",
            "dhcp=on",
            "kernel.virtio-net=on",
        ]
        .map(String::from)
        .to_vec();
        let (kernel, rest) = split_kernel_opts(&raw).expect("well-formed");
        assert_eq!(
            kernel,
            vec![
                ("preset".to_string(), "nuked".to_string()),
                ("virtio-net".to_string(), "on".to_string())
            ]
        );
        assert_eq!(rest, vec!["wireguard=off".to_string(), "dhcp=on".to_string()]);
        // Malformed kernel pairs are refused HERE, not passed through to be
        // refused as somebody else's unknown option.
        assert!(split_kernel_opts(&["kernel.no-equals".to_string()]).is_err());
        assert!(split_kernel_opts(&["kernel.=off".to_string()]).is_err());
    }

    /// The resolver's refusals, each BY NAME: a contract-breaking off names the
    /// broken symbols and produces NO decision; an unknown row lists the declared
    /// ones; `on` against a bundle veto refuses. Watched red 2026-08-26 with the
    /// Refused arm sabotaged to commit the drop anyway — the refusal assertion
    /// failed and the door built a brick.
    #[test]
    fn the_resolver_refuses_what_the_pane_refuses() {
        // CONFIG_VIRTIO holds contract children — off must refuse and name them.
        let err = resolve_kernel_cli(
            Preset::Nuked,
            HardeningProfile::Vanilla,
            &[("virtio".to_string(), "off".to_string())],
        )
        .expect_err("a contract-breaking off must refuse");
        let msg = format!("{err:#}");
        assert!(msg.contains("REFUSED"), "{msg}");
        assert!(msg.contains("CONFIG_VIRTIO_NET"), "the broken symbols are named: {msg}");

        // An unknown row lists what IS declared — never applied to nothing.
        let err = resolve_kernel_cli(
            Preset::Nuked,
            HardeningProfile::Vanilla,
            &[("bluetooth-mesh".to_string(), "off".to_string())],
        )
        .expect_err("unknown row");
        let msg = format!("{err:#}");
        assert!(msg.contains("declared rows"), "{msg}");
        assert!(msg.contains("virtio-net"), "the listing carries the real rows: {msg}");

        // An unknown preset lists the bundles.
        let err = resolve_kernel_cli(
            Preset::Nuked,
            HardeningProfile::Vanilla,
            &[("preset".to_string(), "tiny".to_string())],
        )
        .expect_err("unknown preset");
        assert!(format!("{err:#}").contains("nuked"), "{err:#}");

        // `on` for a symbol the bundle VETOES: the fragment can only veto.
        let m = KernelFeatureModel::derive(Preset::Nuked, HardeningProfile::Vanilla);
        let vetoed = m.symbols.iter().find(|s| !s.enabled).expect("nuked vetoes plenty");
        let err = resolve_kernel_cli(
            Preset::Nuked,
            HardeningProfile::Vanilla,
            &[(option_name(&vetoed.name), "on".to_string())],
        )
        .expect_err("on against a veto");
        assert!(format!("{err:#}").contains("VETOES"), "{err:#}");
    }

    /// The acceptance pair: `kernel.preset=<p>` picks the bundle, a legal off
    /// stands with its cascade in the fragment, and the SAME decision through the
    /// resolver and through the pane's own try_toggle door is BYTE-IDENTICAL.
    /// Watched red 2026-08-26 with the resolver appending a trailing newline the
    /// pane's path does not write.
    #[test]
    fn the_resolver_and_the_pane_render_the_same_bytes() {
        // Find a legally droppable symbol in the slim pick (not contract-locked).
        let model = KernelFeatureModel::derive(Preset::Slim, HardeningProfile::Vanilla);
        let free = model
            .option_rows()
            .into_iter()
            .find(|r| r.default_on && r.locked_by.is_empty())
            .expect("slim has a legally droppable row");

        let decision = resolve_kernel_cli(
            Preset::Slim,
            HardeningProfile::Vanilla,
            &[
                ("preset".to_string(), "slim".to_string()),
                (free.option.clone(), "off".to_string()),
            ],
        )
        .expect("legal drop");
        assert_eq!(decision.preset, Preset::Slim);
        assert_eq!(decision.clicked_off, vec![free.symbol.clone()]);
        assert!(decision.changes_anything());
        assert!(
            decision.fragment.contains(&format!("# {} is not set", free.symbol)),
            "the veto spelling for the clicked symbol:\n{}",
            decision.fragment
        );

        // The pane's pointer: same pick, same click, the model's own doors.
        let mut clicked: Vec<String> = Vec::new();
        assert_eq!(model.try_toggle(&mut clicked, &free.symbol), Some(ToggleOutcome::Off));
        let pane_fragment = model.decision_fragment(&clicked, None);
        assert_eq!(
            decision.fragment.as_bytes(),
            pane_fragment.as_bytes(),
            "one decision, one writer, identical bytes"
        );

        // A no-op decision states that it changes nothing.
        let idle = resolve_kernel_cli(Preset::Slim, HardeningProfile::Vanilla, &[]).unwrap();
        assert!(!idle.changes_anything());

        // off then on round-trips to the idle fragment — the flags are a
        // conversation, not a write-once ledger.
        let back = resolve_kernel_cli(
            Preset::Slim,
            HardeningProfile::Vanilla,
            &[
                (free.option.clone(), "off".to_string()),
                (free.option.clone(), "on".to_string()),
            ],
        )
        .unwrap();
        assert_eq!(back.fragment.as_bytes(), idle.fragment.as_bytes());
    }

    /// A contract-breaking drop refuses and mutates nothing; a legal drop stands and a
    /// second toggle brings it back.
    #[test]
    fn toggle_refuses_the_contract_and_round_trips_a_legal_drop() {
        let m = KernelFeatureModel::derive(Preset::Normal, HardeningProfile::Vanilla);
        let mut off: Vec<String> = Vec::new();
        // CONFIG_VIRTIO holds VIRTIO_PCI/NET/BLK, all contract — dropping it must
        // refuse and name them.
        match m.try_toggle(&mut off, "CONFIG_VIRTIO") {
            Some(ToggleOutcome::Refused(broken)) => {
                assert!(broken.iter().any(|b| b == "CONFIG_VIRTIO_NET"), "{broken:?}");
                assert!(off.is_empty(), "a refusal must not mutate the click-off set");
            }
            other => panic!("expected a refusal, got {other:?}"),
        }
        // An unknown symbol is None, not a silent no-op that looks like success.
        assert_eq!(m.try_toggle(&mut off, "CONFIG_NOT_A_THING"), None);
    }

    /// **The decision ↔ fragment bijection** (LAW D of [`crate::laws`]): a fragment
    /// rendered from a decision made through `try_toggle` parses back to the same
    /// pick, the same click set, and the same total veto set the cascade computed.
    #[test]
    fn a_decision_fragment_parses_back_to_the_decision_that_wrote_it() {
        let model = KernelFeatureModel::derive(Preset::Slim, HardeningProfile::Hardened);
        let free = model
            .option_rows()
            .into_iter()
            .find(|r| r.default_on && r.locked_by.is_empty())
            .expect("slim has a legally droppable row");
        let mut clicked: Vec<String> = Vec::new();
        assert_eq!(model.try_toggle(&mut clicked, &free.symbol), Some(ToggleOutcome::Off));

        let fragment = model.decision_fragment(&clicked, None);
        let parsed = parse_decision_fragment(&fragment).expect("the fragment carries its decision");
        assert_eq!(parsed.preset, Preset::Slim);
        assert_eq!(parsed.profile, HardeningProfile::Hardened);
        assert_eq!(parsed.clicked_off, clicked, "the clicks come back as clicks");
        let dropped = model.cascade_for(&clicked);
        let want: Vec<String> = model
            .symbols
            .iter()
            .enumerate()
            .filter(|&(i, _)| dropped[i])
            .map(|(_, s)| s.name.clone())
            .collect();
        assert_eq!(parsed.vetoed, want, "clicks + cascade, in fragment order");

        // The idle decision round-trips too: no clicks, but the pick still reads back.
        let idle = parse_decision_fragment(&model.decision_fragment(&[], None)).unwrap();
        assert!(idle.clicked_off.is_empty() && idle.vetoed.is_empty());
        assert_eq!((idle.preset, idle.profile), (Preset::Slim, HardeningProfile::Hardened));
    }

    /// The parser's refusals, each watched refusing (LAW 2): a headerless text, a
    /// value-setting line, an empty pick — and a cascade why-line must NOT read
    /// back as a click, because regenerating the cascade is the model's job.
    #[test]
    fn the_fragment_parser_refuses_what_is_not_a_decision() {
        let model = KernelFeatureModel::derive(Preset::Slim, HardeningProfile::Vanilla);
        let fragment = model.decision_fragment(&[], None);

        // Header removed → refused NAMING the header, not parsed to a guess.
        let headerless: String = fragment
            .lines()
            .filter(|l| !l.trim().starts_with("# preset = "))
            .collect::<Vec<_>>()
            .join("\n");
        let err = parse_decision_fragment(&headerless).unwrap_err().to_string();
        assert!(err.contains("header"), "the refusal names the missing header: {err}");

        // A value-setting line means this is not a veto fragment.
        let doctored = "# preset = slim · profile = vanilla\nCONFIG_FOO=y\n";
        let err = parse_decision_fragment(doctored).unwrap_err().to_string();
        assert!(err.contains("SETS a value"), "{err}");

        // An empty / unknown pick is refused by name — `Preset::parse(\"\")` reads
        // as `normal` elsewhere, and a header must never lean on that default.
        for bad in [
            "# preset =  · profile = vanilla\n",
            "# preset = slin · profile = vanilla\n",
            "# preset = slim · profile = hardned\n",
        ] {
            assert!(parse_decision_fragment(bad).is_err(), "{bad:?} must refuse");
        }

        // A cascade drop stays a cascade drop: it is a consequence, not a click.
        let cascade_only = "# preset = slim · profile = vanilla\n\
                            # dropped by cascade — nothing kept needs it\n\
                            # CONFIG_FOO is not set\n";
        let parsed = parse_decision_fragment(cascade_only).unwrap();
        assert!(parsed.clicked_off.is_empty(), "a cascade line is not a click");
        assert_eq!(parsed.vetoed, vec!["CONFIG_FOO".to_string()]);
    }

    /// **`kernel.profile` is a VAL, and this door takes it** — law C across the
    /// doors, wave 3.
    ///
    /// The kernel subtree renders `preset` and `profile` side by side as vals, and
    /// ⚒ hands BYGG every flag the tree spells. This door judged anything that was
    /// not `preset` as a symbol row, so the flag the pane drew came back
    /// *"a symbol row takes on|off"* and `BYGG kärnan` on gunnar's real declaration
    /// could not run at all. RED before this landed: the first assert below was the
    /// refusal.
    ///
    /// And it is a real choice, not a word the parser swallows: hardened and vanilla
    /// render DIFFERENT decision fragments off the same preset.
    #[test]
    fn the_profile_val_is_a_choice_this_door_takes_and_not_a_symbol() {
        let pairs = |s: &[(&str, &str)]| -> Vec<(String, String)> {
            s.iter().map(|(a, b)| ((*a).to_string(), (*b).to_string())).collect()
        };
        let vanilla = resolve_kernel_cli(Preset::Nuked, HardeningProfile::Vanilla, &pairs(&[("profile", "vanilla")]))
            .expect("the pane's own flag `--opt kernel.profile=vanilla` must reach this door");
        assert_eq!(vanilla.profile, HardeningProfile::Vanilla);

        let hardened = resolve_kernel_cli(Preset::Nuked, HardeningProfile::Vanilla, &pairs(&[("profile", "hardened")]))
            .expect("and so must the other one");
        assert_eq!(hardened.profile, HardeningProfile::Hardened, "the CHOICE decides, not the declaration");
        assert_ne!(
            hardened.fragment, vanilla.fragment,
            "a profile that changes nothing in the fragment is a control that does nothing"
        );

        // Unknown by name, twice by name — never a silent degrade to vanilla.
        let bad = resolve_kernel_cli(Preset::Nuked, HardeningProfile::Vanilla, &pairs(&[("profile", "hardned")]));
        assert!(bad.is_err_and(|e| format!("{e:#}").contains("unknown profile")), "a typo is refused BY NAME");
        let twice = resolve_kernel_cli(
            Preset::Nuked,
            HardeningProfile::Vanilla,
            &pairs(&[("profile", "vanilla"), ("profile", "hardened")]),
        );
        assert!(twice.is_err_and(|e| format!("{e:#}").contains("given twice")));
    }
}