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

//! Build-variant descriptor + validator + `cargo build` materializer.
//!
//! # The model
//!
//! * [`BuildVariant`] — ONE concrete build point: an optional workspace
//!   `-p <package>`, an optional `--bin <bin>`, a set of `--features`, a
//!   `--no-default-features` toggle, a [`Profile`] (`--profile`), an optional
//!   `--target <triple>`, and an optional `--cfg` (passed via `RUSTFLAGS`).
//! * [`BuildVariantSet`] — the SPACE of choices a repo offers: the named
//!   variants plus the rules that make a variant *valid* — the feature space
//!   the repo `offers`, `exclusive` (mutually-exclusive) feature groups, and
//!   per-bin `required` features (Cargo's `required-features`, mirrored so we
//!   fail fast with a clear message instead of a raw cargo error).
//!
//! # Declarative source
//!
//! The set is data. The same `[build]` / `[[build.variant]]` schema loads
//! either from the workspace `nornir.toml` (as [`crate::config::Nornir::build`])
//! or from a standalone `.nornir/build-variants.toml` next to the repo
//! ([`BuildVariantSet::discover`]). Worked example: holger's three executables
//! plus its GUI, in `tests/fixtures/holger-build-variants.toml`.
//!
//! # Materialize + run
//!
//! [`BuildVariant::cargo_args`] renders the exact `cargo` argv;
//! [`BuildVariant::build`] validates against the owning set and then runs it.

use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::Command;

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

/// A cargo build **profile** — `--profile <name>`. `Dev` is the cargo default
/// and emits no flag; `Release` emits `--profile release`; `Custom` carries any
/// user-defined profile name (e.g. a `[profile.dist]` in `Cargo.toml`).
///
/// Deserializes from a bare TOML string (`profile = "release"`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum Profile {
    /// The default cargo profile — no `--profile` flag is emitted.
    Dev,
    /// `--profile release`.
    Release,
    /// A user-defined profile — `--profile <name>`.
    Custom(String),
}

impl Default for Profile {
    fn default() -> Self {
        Profile::Dev
    }
}

impl From<String> for Profile {
    fn from(s: String) -> Self {
        match s.trim() {
            "" | "dev" | "debug" => Profile::Dev,
            "release" => Profile::Release,
            other => Profile::Custom(other.to_string()),
        }
    }
}

impl Profile {
    /// The canonical label — the exact word cargo's `--profile` takes and the
    /// directory name under `target/` the artifact lands in. Borrowed rather than
    /// owned so a hot key-building loop ([`BuildVariantSet::check_bins`]) does not
    /// allocate a `String` per variant just to name a profile.
    pub fn label(&self) -> &str {
        match self {
            Profile::Dev => "dev",
            Profile::Release => "release",
            Profile::Custom(s) => s.as_str(),
        }
    }
}

impl From<Profile> for String {
    fn from(p: Profile) -> Self {
        match p {
            Profile::Dev => "dev".to_string(),
            Profile::Release => "release".to_string(),
            Profile::Custom(s) => s,
        }
    }
}

/// The **fat/thin service shape** of a build variant — the suite's second build
/// axis, orthogonal to `(package × bin × features × profile × target)`.
///
/// A nordisk suite member (holger · korp · dwarves · nornir · modgunn · skade)
/// ships as either a self-contained *default service* with its siblings compiled
/// IN, or a *standalone/thin component* that reaches remote owners over gRPC — or
/// a configurable mix. This enum names that choice so a variant can carry
/// [`BuildVariant::embeds`] (siblings compiled-in) and [`BuildVariant::reaches`]
/// (siblings reached over gRPC).
///
/// Deserializes from a bare TOML string. The canonical key is **`linkage`**
/// (`linkage = "fat"`); `shape = "fat"` is accepted as the historical spelling of
/// the SAME field — there is no second field to keep in sync.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ServiceShape {
    /// The **default service (fat)** — the member plus the sibling functionality
    /// it needs compiled IN and running embedded. Self-contained: shut every
    /// other server down and it still runs. Carries a non-empty `embeds`.
    FatService,
    /// The **standalone / thin component** — a leaner build that compiles in NO
    /// siblings; it links gRPC clients and `reaches` remote owners. Smaller
    /// binary; needs the owners reachable. `embeds` MUST be empty.
    ThinStandalone,
    /// A configurable middle: embed SOME siblings, reach OTHERS over gRPC.
    Mixed,
    /// **The axis does not apply to this artifact.** A `lib` is linked by whoever
    /// consumes it and composes nothing; a browser demo with no wire at all
    /// reaches nobody. Saying `linkage = "not-applicable"` is NOT the same as
    /// saying nothing: an omitted axis silently reads as the [`FatService`]
    /// default, i.e. as a positive claim of self-containment that nobody checked.
    /// This value makes "inapplicable" a declaration a reader can tell apart from
    /// "undeclared", and [`BuildVariantSet::validate`] holds it to `embeds` and
    /// `reaches` both being empty.
    NotApplicable,
}

impl Default for ServiceShape {
    fn default() -> Self {
        ServiceShape::FatService
    }
}

impl From<String> for ServiceShape {
    fn from(s: String) -> Self {
        match s.trim().to_ascii_lowercase().as_str() {
            "thin" | "thin-standalone" | "standalone" | "component" => ServiceShape::ThinStandalone,
            "mixed" | "mix" | "configurable" => ServiceShape::Mixed,
            "not-applicable" | "not_applicable" | "inapplicable" | "n/a" | "na" | "none" => {
                ServiceShape::NotApplicable
            }
            // "fat" | "fat-service" | "service" | "default" | "" → the default.
            _ => ServiceShape::FatService,
        }
    }
}

impl From<ServiceShape> for String {
    fn from(s: ServiceShape) -> Self {
        s.as_str().to_string()
    }
}

impl ServiceShape {
    /// Stable label (matches the canonical string form).
    pub fn as_str(&self) -> &'static str {
        match self {
            ServiceShape::FatService => "fat",
            ServiceShape::ThinStandalone => "thin",
            ServiceShape::Mixed => "mixed",
            ServiceShape::NotApplicable => "not-applicable",
        }
    }
    /// True when the fat/thin axis says something about this artifact at all —
    /// i.e. everything except [`NotApplicable`](Self::NotApplicable).
    pub fn is_applicable(&self) -> bool {
        !matches!(self, ServiceShape::NotApplicable)
    }
}

/// **The kind table, written ONCE.** Generates the enum, the canonical string,
/// every accepted spelling, and [`ArtifactKind::ALL`] — from one list.
///
/// # Why a macro and not four hand-written lists
///
/// It was four lists, and on 2026-09-06 adding a tenth kind (`Package`, for a pip
/// wheel) showed what that costs. The enum, `as_str`, `From<String>` and the kind
/// tables in [`crate::verbs`] / [`crate::contract`] each named every kind
/// separately, and only the first two were held by the compiler. `From<String>`
/// ends in `_ => Exe`, so a kind missing from it does not fail to compile — a
/// `kind = "package"` row would have read as `exe` and built a cargo binary,
/// silently. `verbs::VERBS` ends in `unwrap_or_default()`, so a kind with no row
/// answers "no verbs" by accident and indistinguishably from a kind that has
/// none. Both of their tests enumerated kinds BY HAND, so both would have stayed
/// green with the new kind absent from everything they check: the hollow-guard
/// shape LAW 2 exists for, twice, in the one place a new kind arrives.
///
/// So the spellings live beside the variant that owns them, and `ALL` is derived
/// rather than typed. A new kind is now a compile error in exactly one place —
/// this list — and the tables that key on a kind are driven off `ALL`, which
/// makes a missing decision a RED TEST instead of a default answer.
///
/// The `default` is the fallback of `From<String>`: an unrecognised spelling (and
/// the empty string) reads as that kind, which is why `Exe`'s own spellings need
/// no entry to work and are listed anyway — a reader should not have to know
/// which arm is the fallback to find out what `"bin"` means.
macro_rules! artifact_kinds {
    (
        default = $default:ident;
        $(
            $(#[$meta:meta])*
            $variant:ident => $canonical:literal $(| $alias:literal)* ,
        )+
    ) => {
        /// The **artifact kind** a build variant produces — the suite's THIRD build
        /// axis, ORTHOGONAL to [`ServiceShape`] (fat/thin) and to
        /// `(package × bin × features × profile × target)`.
        ///
        /// A build-thing is one `(kind × shape × target)` point:
        /// e.g. `Service`×thin×x86_64 (a gRPC-client daemon), `Service`×fat×x86_64 (an
        /// embedded server), `Wasm`×—×wasm32 (a web demo), `Exe`×fat (a CLI),
        /// `Container`×thin (a deploy image).
        ///
        /// `Exe`/`Lib`/`Wasm`/`Service` are all produced by `cargo build`
        /// ([`BuildVariant::cargo_args`]); `Container`/`Vm` delegate to the rich
        /// [`ContainerBuild`](crate::container::ContainerBuild) /
        /// [`VmBuild`](crate::vm::VmBuild) descriptors. Deserializes from a bare TOML
        /// string (`kind = "wasm"`), identical style to [`Profile`] / [`ServiceShape`].
        ///
        /// Every variant, its canonical spelling and its aliases are declared in one
        /// place — see [`artifact_kinds`] for why that is a macro.
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
        #[serde(from = "String", into = "String")]
        pub enum ArtifactKind {
            $( $(#[$meta])* $variant, )+
        }

        impl ArtifactKind {
            /// **Every kind, in declaration order** — the list every table keyed by
            /// kind is driven off, so a new kind cannot be added to the enum without
            /// appearing here (they are the same list) and a table that forgot it
            /// fails RED rather than answering a default.
            pub const ALL: &'static [ArtifactKind] = &[ $(ArtifactKind::$variant),+ ];

            /// Stable label (matches the canonical string form).
            pub fn as_str(&self) -> &'static str {
                match self { $(ArtifactKind::$variant => $canonical,)+ }
            }

            /// **Every spelling this kind answers to**, canonical first — the inverse
            /// of [`From<String>`], and the reason a recipe author can be told what a
            /// refused `kind = "…"` COULD have said.
            pub fn spellings(&self) -> &'static [&'static str] {
                match self { $(ArtifactKind::$variant => &[$canonical $(, $alias)*],)+ }
            }
        }

        impl From<String> for ArtifactKind {
            fn from(s: String) -> Self {
                match s.trim().to_ascii_lowercase().as_str() {
                    $( $canonical $(| $alias)* => ArtifactKind::$variant, )+
                    // Anything unrecognised, and the empty string, is the default.
                    _ => ArtifactKind::$default,
                }
            }
        }
    };
}

artifact_kinds! {
    default = Exe;

    /// A native executable (default) — a plain binary/tool.
    Exe => "exe" | "bin" | "binary",
    /// A cargo library artifact (`--lib`); no `--bin`.
    Lib => "lib" | "library" | "rlib" | "cdylib" | "staticlib",
    /// A `wasm32-*` module (requires a wasm `target`).
    Wasm => "wasm" | "wasm32" | "web",
    /// An OCI / appliance image — delegates to a
    /// [`ContainerBuild`](crate::container::ContainerBuild).
    Container => "container" | "image" | "oci" | "docker",
    /// A bootable VM / appliance — delegates to a [`VmBuild`](crate::vm::VmBuild).
    Vm => "vm" | "appliance" | "boot",
    /// A bootable ISO-9660 CD/DVD image — delegates to an
    /// [`IsoBuild`](crate::iso::IsoBuild).
    Iso => "iso" | "cdrom" | "bootable-iso",
    /// A per-site **installer** ISO — the medium that LAYS A PRODUCT ONTO A
    /// DISK, minted from an [`InstallerBuild`](crate::installer::InstallerBuild)
    /// variant. A separate kind from [`Iso`](Self::Iso) because the two artifacts
    /// answer different questions in a build record: a live ISO *is* the
    /// appliance, an installer ISO *produces* one — and until this variant
    /// existed the attestation spelled both `"iso"`, so an auditor could not
    /// tell a bootable product from the tool that installs it.
    ///
    /// A variant may declare `kind = "installer"` directly; `[[build.installer]]`
    /// is permanent sugar for the same thing. (Until 2026-08-15 `validate()`
    /// REFUSED the field spelling and pointed at the section — reversed by the
    /// [vocabulary ruling](crate#the-vocabulary).)
    Installer => "installer" | "installer-iso",
    /// A nordisk suite-member daemon: an executable that ALSO wraps as a
    /// skidbladnir `ServiceSpec` (dedicated system user + unit). Requires a
    /// `member`.
    Service => "service" | "daemon" | "unit",
    /// A **built Linux kernel** — a `bzImage` + its `.config` + a pin file of
    /// sha256s, compiled from a PINNED kernel.org tarball inside a toolchain
    /// container under a named hardening profile. Delegates to a
    /// [`KernelBuild`](crate::kernel::KernelBuild); `[[build.kernel]]` is the
    /// sugar section. A separate kind from [`Vm`](Self::Vm)/[`Iso`](Self::Iso)
    /// because those CONSUME a kernel the constellation already has — this is
    /// where that kernel comes from, with its provenance attached.
    Kernel => "kernel" | "bzimage" | "linux-kernel",
    /// A **distribution package** — a produced FILE SET, minted from a
    /// [`PackageBuild`](crate::package::PackageBuild); `[[build.package]]` is the
    /// sugar section.
    ///
    /// The set, not the file, is the point. facett's pip release is a wheel AND
    /// an sdist AND the `SHA256SUMS` a downloader runs `sha256sum -c` against AND
    /// a provenance record over the produced bytes — four files that are one
    /// artifact, because any three of them without the fourth is not a release
    /// anybody can check. Every other kind here produces ONE thing and names it;
    /// this is the kind whose output is a set, and
    /// [`PackageBuild::required_members`](crate::package::PackageBuild::required_members)
    /// is where the set is declared.
    ///
    /// A separate kind from [`Wasm`](Self::Wasm), which was the near-miss: facett's
    /// wheel carries a wasm module, so `kind = "wasm"` looks right and is not — it
    /// would make the forge build a bare `.wasm` and stop, with no wasm-bindgen
    /// step, no wheel, no read-back of the produced zip and no release set. And a
    /// separate kind from [`Exe`](Self::Exe)/[`Lib`](Self::Lib) because `cargo
    /// build` does not build a wheel at all: a package is assembled by its
    /// format's own tool inside a pinned container ([`crate::package`]), and the
    /// only thing cargo contributes is inputs.
    ///
    /// Added 2026-09-06 (HO-T5). Before it, facett's release existed and had no
    /// door: `cargo xtask publish-pip` produced a checkable release set that
    /// `dwarves buildthing facett --variant facet-wrapped-web` could not ask for,
    /// because the forge had no kind to route on. facett's own
    /// `facett-ops/tests/release_manifest.rs` recorded the gap in its module docs
    /// rather than working around it.
    Package => "package" | "wheel" | "pip" | "sdist" | "dist",
}

impl Default for ArtifactKind {
    fn default() -> Self {
        ArtifactKind::Exe
    }
}

impl From<ArtifactKind> for String {
    fn from(k: ArtifactKind) -> Self {
        k.as_str().to_string()
    }
}

impl ArtifactKind {
    /// True for the kinds produced by `cargo build` (Exe/Lib/Wasm/Service) — the
    /// dispatcher routes these through `build_variant`, the rest through the
    /// image/vm builders.
    pub fn is_cargo(&self) -> bool {
        matches!(
            self,
            ArtifactKind::Exe | ArtifactKind::Lib | ArtifactKind::Wasm | ArtifactKind::Service
        )
    }
    /// **True for the kinds that may declare a kernel donor** — the kinds with a
    /// `[build.<kind>.kernel_donor]` table (`[[build.iso]]`, `[[build.installer]]`),
    /// the same two [`BuildVariantSet::kernel_donor_of`] and
    /// [`BuildVariantSet::kernel_donor_consumers`] read. A medium of one of these
    /// kinds with no consumption edge boots the host's kernel, and a UI saying so
    /// asks HERE rather than matching the kind itself (the scene renders what a
    /// declaration says; the kind→capability table lives in one place).
    pub fn takes_kernel_donor(&self) -> bool {
        matches!(self, ArtifactKind::Iso | ArtifactKind::Installer)
    }
    /// True for kinds whose composition the fat/thin `shape` + `embeds`/`reaches`
    /// machinery applies to (Service, Container).
    pub fn is_service_shaped(&self) -> bool {
        matches!(self, ArtifactKind::Service | ArtifactKind::Container)
    }

    /// **True for the cargo kinds that legitimately produce NO `executable`** in
    /// cargo's artifact stream.
    ///
    /// [`build_variant_observed_with_info`](crate::build_variant_observed_with_info)
    /// only emits an [`Artifact`](crate::progress::BuildEvent::Artifact) event when
    /// cargo's `CompilerArtifact` carries an `executable` path — an rlib and a wasm
    /// module do not. So "exited 0 and produced no artifact", which is the classic
    /// false success for an `exe`, is the CORRECT and only possible outcome for a
    /// `lib`, and any consumer that keys greenness on the artifact list needs to be
    /// able to tell the two apart. draupnir declares three `kind = "lib"` variants; with
    /// no way to ask, every one of them reads as a build that produced nothing.
    ///
    /// This is the ask, not a second copy of the kind list: it is the discriminant
    /// itself, next to [`is_cargo`](Self::is_cargo), so there is one place to change
    /// when cargo starts reporting an executable for a wasm target.
    ///
    /// **[`Package`](Self::Package) is deliberately NOT here**, and the reason is
    /// the same one that narrowed [`product_is_not_a_file`](Self::product_is_not_a_file):
    /// dwarves' `produced_something()` is
    /// `!artifacts.is_empty() || (produced_ref && product_is_not_a_file) ||
    /// produces_no_executable()`, so a kind that answers `true` here is a kind whose
    /// build reads GREEN having written nothing at all. That is right for an rlib,
    /// whose artifact genuinely never appears in cargo's stream. A package's whole
    /// product is files on disk — if none arrived, the build failed, and the record
    /// must say so.
    pub fn produces_no_executable(&self) -> bool {
        matches!(self, ArtifactKind::Lib | ArtifactKind::Wasm)
    }

    /// **True when this kind's product does NOT land as a file on disk**, so a
    /// named handle is the only evidence it exists.
    ///
    /// A [`Container`](ArtifactKind::Container) image lives in the container
    /// store, not in the working tree: there is no path to stat, and the image
    /// reference (`gunnar-scratch:0.1.0`) is the whole of the proof. A consumer
    /// deciding "did anything come out?" therefore has to accept that reference
    /// in place of an artifact list.
    ///
    /// # Why this is a question worth asking, measured 2026-08-05
    ///
    /// `dwarves`' `produced_something()` accepted a produced-reference for EVERY
    /// kind, with a comment naming the container case as the reason. But an
    /// [`Iso`](ArtifactKind::Iso) **is** a file — the ISO is the artifact — and
    /// the blanket exemption let `gunnar-appliance-iso` report
    ///
    /// ```text
    /// 'gunnar-appliance-iso' → iso gunnar-appliance-iso (digest 377b42609bc15eff…)
    /// BUILT 'gunnar-appliance-iso' in 38.5s — 0 artifact(s)
    /// ```
    ///
    /// and exit 0, with no `.iso` anywhere on disk. Run twice it took 38.5 s and
    /// then 0.9 s and printed a DIFFERENT digest each time, for two runs that
    /// wrote nothing — a digest over something that is not the ISO, presented as
    /// the ISO's identity. `dwarves-buildthing-6` ("exited 0 but produced NO
    /// artifact") is precisely the code for that, and it could never fire.
    ///
    /// So the exemption is narrowed to the kinds it was actually written for.
    /// This is the discriminant answering for itself, next to
    /// [`produces_no_executable`](Self::produces_no_executable), rather than a
    /// second copy of the kind list at the call site.
    pub fn product_is_not_a_file(&self) -> bool {
        matches!(self, ArtifactKind::Container)
    }
}

#[cfg(test)]
mod artifact_kind_evidence {
    use super::ArtifactKind;

    /// The narrowing itself, stated as the two cases that differ.
    ///
    /// Seen RED before it was trusted: with `product_is_not_a_file` returning
    /// `true` for every kind (the behaviour this replaces) the `Iso` assertion
    /// below fails, which is the `gunnar-appliance-iso` false success reproduced
    /// in one line and without a forge.
    #[test]
    fn an_iso_is_a_file_and_a_container_image_is_not() {
        assert!(
            ArtifactKind::Container.product_is_not_a_file(),
            "a container image lives in the container store, so its reference is \
             the only evidence there is"
        );
        assert!(
            !ArtifactKind::Iso.product_is_not_a_file(),
            "an ISO IS a file; accepting a bare digest for one is how a build that \
             wrote no media reported BUILT and exited 0"
        );
        for k in [
            ArtifactKind::Exe,
            ArtifactKind::Lib,
            ArtifactKind::Wasm,
            ArtifactKind::Service,
            // A built kernel is a `bzImage` ON DISK (+ .config + pin file) — the
            // exemption stays narrowed to the container store.
            ArtifactKind::Kernel,
        ] {
            assert!(
                !k.product_is_not_a_file(),
                "{k:?} produces a file; only the container store is exempt"
            );
        }
    }
}

/// One concrete build point: a fully-resolved `cargo build` target.
///
/// Serializes as a `[[build.variant]]` table:
/// ```toml
/// [[build.variant]]
/// name = "gui"                 # the handle you pass to `nornir build --variant`
/// package = "holger-ui"        # cargo `-p`
/// bin = "holger-ui"            # cargo `--bin`
/// features = ["gui"]           # cargo `--features`
/// # open = ["gpu"]             # features THIS row leaves switchable (its own axis)
/// no_default_features = false  # cargo `--no-default-features`
/// profile = "release"          # cargo `--profile`
/// # target = "x86_64-unknown-linux-musl"  # cargo `--target`
/// # cfg = "holger_dist"                    # RUSTFLAGS --cfg
/// # member = "nornir"          # the suite member this artifact IS
/// # shape = "fat"              # fat-service / thin-standalone / mixed
/// # embeds = ["holger", "skade"]   # siblings compiled IN (fat)
/// # reaches = ["dwarves"]          # siblings reached over gRPC (never embedded)
/// # boot-logo = "brand/acme.png"   # this ISO's own boot mark (⇒ KORP_BOOT_LOGO)
/// ```
/// **A key this struct does not know is a REFUSAL, not a shrug.** MEASURED
/// 2026-08-26 across all 21 `.nornir/build-variants.toml` in the constellation:
/// zero unknown keys on a variant row, in zero repos — so this costs nothing
/// today and buys the thing `manifest_dir` cost. Published `nornir-build-thing`
/// 0.1.8 has no `manifest_dir` field and no `deny_unknown_fields`, so skade's
/// declaration parsed and was THROWN AWAY SILENTLY; a guard that asked the struct
/// would have read `None` and gone green. With this on, the same skew fails to
/// load the file instead — the right failure, and a loud one.
///
/// Serde honours `alias`, so `test-packages`/`test_packages`, `linkage`/`shape`
/// and `default`→`is_default` all keep working. Precedent in this crate:
/// `options.rs:85`, `options.rs:168`, `params.rs:254`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BuildVariant {
    /// Stable handle for the variant (`nornir build --variant <name>`).
    pub name: String,
    /// Workspace package for `-p` (omit for a single-crate repo).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub package: Option<String>,
    /// `[[bin]]` target for `--bin` (omit to build the package's default bin).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bin: Option<String>,
    /// **A cargo EXAMPLE target** for `--example` — the artifact is
    /// `<triple>/<profile>/examples/<example>.wasm` (or the native example binary).
    /// Mutually exclusive with [`bin`](Self::bin) and refused on `kind = "lib"`: an
    /// example is its own target, so a row that names one builds exactly it. Exists
    /// so a fixture that lives as an example (facett's `panel` WebGPU guard) is a
    /// declared row instead of an argv spelled in an xtask.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub example: Option<String>,
    /// Features to enable (`--features a,b`).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub features: Vec<String>,
    /// **The SIBLING packages whose tests this variant's test run also covers** —
    /// `test-packages = ["holger-server-lib"]`, extra `-p` selections that
    /// [`test_args`](Self::test_args) adds and [`cargo_args`](Self::cargo_args)
    /// ignores.
    ///
    /// ## Why the test surface needed a key of its own
    ///
    /// A brokkr `tests` step runs the VARIANT'S argv with `build` swapped for
    /// `test`, so it inherits `-p <package>` and covers exactly one package.
    /// MEASURED 2026-08-25: gunnar reaches 265 of 1114 tests (24 %), holger 210 of
    /// 827 (25 %) — and `holger-server-lib` alone is **297 tests, 36 % of holger's
    /// suite**, the crate its three tiers are tiers *of*. The three tier variants
    /// build it, link it, and ship it, and no tier step has ever run one of its
    /// tests.
    ///
    /// Inheriting the variant's *feature* resolution is right and stays: a test run
    /// whose features drift from the build's tests a configuration the forge does
    /// not ship. But **scoping features and scoping packages are different
    /// questions**, and the original design conflated them. This key separates
    /// them: the features stay the variant's, the package selection widens.
    ///
    /// ## Why this is not a second configuration
    ///
    /// The extra packages are compiled BY THE BUILD ALREADY — they are in the
    /// variant package's own dependency closure, with feature values cargo derived
    /// from the variant's `--features`. `test_args` therefore renders the selection
    /// as `-p <package> -p <sibling>` with the features **qualified to the variant's
    /// package** (`--features holger-server-cli/full`), so unification hands the
    /// sibling exactly the feature set the shipped artifact links. Verified against
    /// cargo 2026-08-25: `cargo test -p a -p b --features a/full` is accepted and
    /// runs `b`'s tests under the features `a/full` propagates.
    ///
    /// What the *declaration* cannot check is that the sibling really is in that
    /// closure — that needs `cargo metadata`, which [`BuildVariantSet::validate`]
    /// deliberately does not run. Naming a package OUTSIDE the closure makes the
    /// test run a configuration the build does not ship (the outsider drags its own
    /// default features into the unified graph), which is the exact drift this
    /// design exists to prevent. `nornir_testmatrix::testsurface` computes the
    /// closure and reports both halves: an outsider named here, and a closure
    /// member named nowhere.
    ///
    /// Empty ⇒ today's behaviour, unchanged: the test step covers the variant's own
    /// package and nothing else. The default is deliberately NOT "the whole
    /// closure" — a default that silently widens when somebody adds a dependency
    /// makes a new member's broken tests fail a variant step that has nothing to do
    /// with it, and blames the wrong row.
    #[serde(
        rename = "test-packages",
        alias = "test_packages",
        default,
        skip_serializing_if = "Vec::is_empty"
    )]
    pub test_packages: Vec<String>,
    /// **The features THIS variant leaves open to whoever builds it** — the row's
    /// own switchable axis, declared as `open = ["gpu"]`.
    ///
    /// [`BuildVariantSet::offers`] is the repo's whole feature SPACE: the set a
    /// declaration may draw from, and the thing `validate` rejects a stray feature
    /// against. It is emphatically NOT a list of switches every variant has. A
    /// build surface that reads it as one draws a switch on every row for every
    /// feature any row could use — a GPU toggle on a server that has only ever had
    /// a CPU path — and a switch that cannot honestly be flipped is the same defect
    /// as a column nobody declared.
    ///
    /// So the switchable axis belongs to the ROW: one assembly *can* have a GPU and
    /// says so here; its sibling only ever has a CPU and stays silent, and silence
    /// means no widget at all rather than a greyed-out one.
    ///
    /// Empty ⇒ nothing to flip: everything about this row is settled by its
    /// declaration. That is the correct reading for every repo today, and it is why
    /// the default is empty rather than "whatever the repo offers".
    ///
    /// Held by [`BuildVariantSet::validate`] to two rules: an entry must be in the
    /// repo's `offers` (when the repo declares one), and it must not also sit in
    /// [`features`](Self::features) — a feature cannot be both decided and open, and
    /// a declaration that says it is would render as a fixed value and a switch for
    /// one key.
    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
    pub open: BTreeSet<String>,
    /// **Consumption edges** (`[build.variant.edges.<feature>]`) — a root
    /// feature of this variant bound to another build thing's artifact,
    /// `repo/variant : typ`, type-checked at declaration time against the
    /// donor's own declaration. The general form of an installer's / iso's
    /// `kernel_donor`. See [`EdgeDecl`](crate::tree::EdgeDecl).
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub edges: BTreeMap<String, crate::tree::EdgeDecl>,
    /// **Declared PRESETS** (`[build.variant.presets.<name>]`) — named bundles
    /// over this variant's `open` features (and edges), the root `preset` val;
    /// see [`InstallerBuild::presets`](crate::installer::InstallerBuild::presets).
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub presets: crate::tree::Presets,
    /// **The bundle this build was made with** — set by the door when it
    /// expands `--opt preset=<name>` through the tree (`SettingsTree::
    /// expand_opts`), never by the declaration; rides the descriptor so the
    /// receipt says which bundle AND what it meant, exactly as
    /// [`InstallerBuild::applied_preset`](crate::installer::InstallerBuild::applied_preset)
    /// does for a medium.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub applied_preset: Option<crate::tree::AppliedPreset>,
    /// Emit `--no-default-features`.
    #[serde(default, skip_serializing_if = "is_false")]
    pub no_default_features: bool,
    /// Emit `--all-features` — set by a door from the operator's choice
    /// ([`CargoFeatures`]), never declared.
    #[serde(default, skip_serializing_if = "is_false")]
    pub all_features: bool,
    /// Build profile (`--profile`).
    #[serde(default, skip_serializing_if = "is_dev_profile")]
    pub profile: Profile,
    /// Cross-compile target triple (`--target`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
    /// **Where this variant's package lives, when it is NOT a member of the
    /// repo-root workspace** — the directory whose `Cargo.toml` owns it, relative to
    /// the repo root, rendered as `--manifest-path <dir>/Cargo.toml`. Omit for the
    /// normal case.
    ///
    /// This exists because the suite has a deliberate DETACHED-WORKSPACE pattern:
    /// skade keeps `skade/`, `rest/`, `grpc/` and `serve/` out of the core library's
    /// workspace (and out of its published tarball) precisely to keep that manifest
    /// and lockfile clean, and knut `exclude`s its wasm guest because it only builds
    /// for `wasm32-unknown-unknown`. A `-p <package>` rooted at the repo root can
    /// never reach any of them, whatever features it carries, so a consumer that
    /// compiles declared variants (`nornir-testmatrix`'s target-compile aspect) had
    /// no way to run those configurations and reported them as artifacts that do not
    /// build.
    ///
    /// **Not `member`, and not derivable from it.** [`member`](Self::member) is the
    /// SUITE member an artifact IS — a node in the suite dep graph and its
    /// system-user / service name — a logical handle, not a path. skade declares
    /// `member = "skade"` on both its variants while `skade-remote`'s package
    /// `skade-katalog-serve` lives in `serve/`, so reading `member` as a directory
    /// would be right by accident for one and wrong for the other.
    ///
    /// ⚠ **IT WAS RENDERED NOWHERE UNTIL 2026-08-26.** The field parsed, validated,
    /// and neither [`cargo_args`](Self::cargo_args) nor [`test_args`](Self::test_args)
    /// ever emitted `--manifest-path` — so every word above described an intent the
    /// argv did not carry. Fixed in [`push_manifest_path`](Self::push_manifest_path),
    /// which both writers now call.
    ///
    /// ⚠ **AND THE PUBLISHED CRATE STILL CANNOT SEE IT.** skade is the only
    /// crates.io consumer (`skade/skade/Cargo.toml:206`, `version = "0.1.4"`), and
    /// published `nornir-build-thing` 0.1.8 has zero occurrences of `manifest_dir` —
    /// so skade's two declared lines resolve against a struct with no such field and
    /// are dropped. skade's guard therefore reads raw TOML, which is CORRECT as
    /// written and for the reason it states: asking a struct that cannot represent
    /// the key guarantees `None` and guarantees a green.
    ///
    /// That reader is a BRIDGE and should carry an expiry: **delete it when a
    /// release ≥ the one carrying this field is published and skade's requirement
    /// reaches it.** It already shows the scar of being a second parser — it must
    /// skip `#` lines because skade's own prose quotes `manifest_dir = "serve"`.
    /// (`buildgate::read_allowlist` was the counter-example: same shape, no expiry,
    /// and it quietly became a permanent second door. It is gone now.)
    ///
    /// One correction to how that hand-off was described to me: it will NOT make
    /// skade's old version "fail to load its own file". `deny_unknown_fields` and
    /// this field land in the SAME release, so there is no published version that
    /// has the closure without the field. Old versions keep silently dropping it;
    /// new ones read it. Strictly better than the failure that was predicted, and
    /// worth stating so nobody plans a migration around a break that cannot happen.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub manifest_dir: Option<String>,
    /// **The sibling checkouts THIS row's build reaches** — directory names under
    /// the repo's parent, like `[build.toolchain] siblings`, but per row.
    ///
    /// Empty (the default, and every row written before 2026-09-28) ⇒ the row
    /// builds with the toolchain's repo-wide list, unchanged. Non-empty ⇒ it
    /// REPLACES that list for this row: only these are mounted, only these must
    /// be checked out, and the escaping-path-dep check scans the ONE workspace
    /// rooted at the row's manifest ([`manifest_dir`](Self::manifest_dir), else
    /// the repo), skipping nested workspaces cargo does not load. A
    /// `manifest_dir` that is a member of an enclosing workspace must list that
    /// workspace's reach too.
    ///
    /// Why a row needs its own: monetize-impl builds holger's plugin from
    /// `../holger` and gunnar's from `../gunnar`. One repo-wide list is the UNION
    /// of both closures, so building holger's row demanded a gunnar checkout —
    /// and holger may have no dependency on the gunnar repo at all (owner rule
    /// 2026-09-27). The row's closure is what `cargo metadata` of ITS manifest
    /// loads ([`crate::cargo_container::metadata_sibling_closure`]), not the
    /// repo's.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub siblings: Vec<String>,
    /// An extra `--cfg <cfg>`, threaded through `RUSTFLAGS` at build time.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cfg: Option<String>,
    /// The **suite member** this artifact IS (e.g. `"nornir"`). Names the node in
    /// the suite dep graph for the fat/thin asymmetry rule, and is the dedicated
    /// system-user / service name the [`crate::service`] seam wraps it under.
    /// Omit for a plain (non-suite) cargo variant.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub member: Option<String>,
    /// **How this variant talks to its siblings** — the fat/thin LINKAGE axis.
    ///
    /// Declared as `linkage = "fat" | "thin" | "mixed" | "not-applicable"`;
    /// `shape = …` is the historical spelling of the same key and still parses.
    /// FAT is the default and the normal case — thin is the supported exception —
    /// so an omitted key reads as fat, which is why an artifact the axis does not
    /// apply to must say [`ServiceShape::NotApplicable`] out loud rather than stay
    /// silent. `embeds` says WHICH siblings; this says HOW.
    #[serde(
        rename = "linkage",
        alias = "shape",
        default,
        skip_serializing_if = "is_fat_shape"
    )]
    pub shape: ServiceShape,
    /// **What this variant IS to the product** — the role axis a menu groups by
    /// (`role = "server" | "client" | "solo" | "library" | "tool"`).
    ///
    /// Optional: a single-role repo may omit it. When any variant declares one,
    /// [`BuildVariantSet::check_defaults`] holds every declared role to exactly one
    /// [`is_default`](Self::is_default).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub role: Option<crate::plugin::Role>,
    /// **Which DELIVERIES this variant ships in** — the SUITE axis (Swedish
    /// *utgåva*), orthogonal to [`role`](Self::role) and to
    /// [`member`](Self::member).
    ///
    /// ⚠ Not the same "suite" as [`member`](Self::member) /
    /// [`BuildVariantSet::downstream`], which name the nordisk *constellation* a
    /// binary belongs to. This axis is about DELIVERIES: a repo can ship more than
    /// one product out of one crate graph. holger ships its full artifact registry
    /// AND, from the same crates, a co-maintainer's Secure Update Gateway MVP — an intake
    /// agent, a bundle tool, a verifier and a zero-egress protected node. Those are
    /// not "some of holger's variants"; they are a separate deliverable with its own
    /// normal case.
    ///
    /// **A LIST, not a name**, because a variant can ship in more than one delivery:
    /// holger's `agent-online` and `holger-mikro` are in both the MVP and the full
    /// product. A scalar would force a copy of the variant per delivery, and two copies
    /// of a build point is exactly the drift this crate exists to remove. A variant
    /// in N suites is a member of N `(suite, role)` groups
    /// ([`BuildVariantSet::groups`]) and is held to the default rule in every one.
    ///
    /// Without this key the only thing separating two deliveries is a naming
    /// convention, and a convention is not checkable: `features(MVP) ⊆ features(full)`
    /// cannot be stated, and — worse — [`check_defaults`](BuildVariantSet::check_defaults)
    /// used to group by role ALONE, so two deliveries could not each name their own
    /// default server. The file still validated and handed back the OTHER suite's
    /// default, leaving one delivery silently defaultless. Grouping on `(suite, role)`
    /// closes that.
    ///
    /// Empty ⇒ the variant belongs to the repo's single unnamed delivery
    /// ([`UNSUITED`]), which is the correct and unchanged reading for every repo that
    /// ships one product (korp, nornir, dwarves, modgunn, znippy today).
    ///
    /// ```toml
    /// [[build.variant]]
    /// name   = "agent-online"
    /// suites = ["full", "mvp"]   # ships in both deliveries
    /// ```
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub suites: Vec<String>,
    /// **The normal case for this variant's role** — `default = true`.
    ///
    /// Exactly one per role. A menu of nine variants with nothing marking which is
    /// normal makes every caller guess, and fat-vs-thin is not a preference: fat is
    /// the normal case and thin the deliberate exception.
    #[serde(rename = "default", default, skip_serializing_if = "is_false")]
    pub is_default: bool,
    /// The **artifact kind** this variant produces (default [`ArtifactKind::Exe`]).
    /// Orthogonal to [`shape`](Self::shape) and to
    /// `(package × bin × features × profile × target)`.
    #[serde(default, skip_serializing_if = "is_exe_kind")]
    pub kind: ArtifactKind,
    /// For `kind = "container"`: the `[[build.container]]` this variant delegates
    /// to (a thin unifying node so a UI can list every kind in one build list).
    /// Ignored for other kinds.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub container_ref: Option<String>,
    /// For `kind = "vm"`: the `[[build.vm]]` this variant delegates to. Ignored
    /// for other kinds.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub vm_ref: Option<String>,
    /// Sibling members **compiled IN** (embedded) by a fat variant. The asymmetry
    /// rule ([`BuildVariantSet::validate`]) allows a member to embed only what is
    /// DOWNSTREAM of it in the dep graph.
    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
    pub embeds: BTreeSet<String>,
    /// Sibling members **reached over gRPC** (never compiled in). Unconstrained by
    /// the dep graph — a member may reach any peer. Must be disjoint from `embeds`.
    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
    pub reaches: BTreeSet<String>,
    /// **The boot mark this variant burns into its ISO** — `boot-logo =
    /// "brand/acme-mark.png"`, relative to the repo root (an absolute path is taken
    /// as-is).
    ///
    /// This is the per-variant half of korp's customer boot-logo override. A
    /// customer shipping an own-branded appliance declares the mark ON THE VARIANT
    /// that builds their ISO, next to its features and profile, instead of exporting
    /// an environment variable by hand at every call site or — as before — editing
    /// three `build.rs` files. [`BuildVariant::build`] exports it to the child cargo
    /// as `KORP_BOOT_LOGO`, which is the ONE thing
    /// [`korp_installer::splash`](https://git.gunnar.rs/nordisk/edda) reads; the
    /// declaration is the home, the variable is only the transport, so there is no
    /// second resolution path to keep in sync.
    ///
    /// Omitted ⇒ nothing is exported and korp's own `assets/korp-logo.webp` is
    /// baked, which is the correct default and stays the default. A path that does
    /// not exist or does not decode never fails the build: korp's splash step falls
    /// back to an empty blob and warns, because a bad logo must not stop an
    /// appliance from booting.
    #[serde(
        rename = "boot-logo",
        alias = "boot_logo",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub boot_logo: Option<String>,
    /// **This variant's artifact is signed** — `sign = { purpose = "plugin" }`.
    ///
    /// The forge's signing step writes `<artifact>.p7s` (detached CMS over the
    /// blake3 digest) and `<artifact>.blake3` beside every artifact the build
    /// produced, on the host side of the container boundary. See
    /// [`crate::sign`]. Absent ⇒ unsigned, which is what every variant written
    /// before 2026-09-25 is.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sign: Option<crate::sign::SignSpec>,
}

/// **A delegating variant does not get to declare edges of its own.**
///
/// `kind = "container"` / `kind = "vm"` rows exist to POINT at a descriptor
/// (`container_ref` / `vm_ref`), and the artifact's consumption edges belong to
/// that descriptor — which is where [`BuildVariantSet::edges_of`] now reads
/// them. Letting the pointer row carry a second set would give one artifact two
/// edge lists that nothing reconciles, and whichever lookup ran first would win
/// silently. Same rule as `edges.kernel` against `kernel_donor`: one spelling.
fn refuse_delegated_edges(
    variant: &str,
    field: &str,
    edges: &BTreeMap<String, crate::tree::EdgeDecl>,
) -> Result<()> {
    if edges.is_empty() {
        return Ok(());
    }
    Err(anyhow!(
        "variant '{variant}': a delegating row (`{field}`) declares edges {} — put them on the \
         descriptor it points at, which is where the artifact's edges are read from; one spelling \
         per edge",
        edges.keys().cloned().collect::<Vec<_>>().join(", ")
    ))
}

fn is_dev_profile(p: &Profile) -> bool {
    *p == Profile::Dev
}

fn is_fat_shape(s: &ServiceShape) -> bool {
    *s == ServiceShape::FatService
}

fn is_exe_kind(k: &ArtifactKind) -> bool {
    *k == ArtifactKind::Exe
}

fn is_false(b: &bool) -> bool {
    !*b
}

/// The label an EMPTY [`BuildVariant::suites`] groups under — the repo's single
/// unnamed delivery. Kept as one constant so the grouping key, the guards and the
/// menu all spell it the same way. Angle-bracketed like `<unroled>` so it cannot
/// collide with a name a repo would plausibly declare.
pub const UNSUITED: &str = "<the delivery>";

/// The label a variant with no [`BuildVariant::role`] groups under.
const UNROLED: &str = "<unroled>";

/// **The operator's cargo feature choice for ONE build** — what `dwarves buildthing
/// --features a,b --no-default-features --all-features`, the ⚒ button and the
/// forge's `BuildThing` RPC carry. Applied on top of the declared variant
/// ([`BuildVariant::with_cargo_features`]): the declared features stay, the chosen
/// ones are added, and the flags reach cargo exactly as given — so the provenance
/// (`inputs.features`, `inputs.cargo_args`) names what was built.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CargoFeatures {
    /// `--features a,b` (each entry one feature; a comma list is split).
    #[serde(default)]
    pub features: Vec<String>,
    /// `--no-default-features`.
    #[serde(default)]
    pub no_default_features: bool,
    /// `--all-features`.
    #[serde(default)]
    pub all_features: bool,
}

impl CargoFeatures {
    /// Nothing chosen: the declared variant builds as declared.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.features.is_empty() && !self.no_default_features && !self.all_features
    }

    /// Add `--features` values, splitting comma lists, trimming, dropping empties
    /// and repeats (order kept).
    pub fn add_features<S: AsRef<str>>(&mut self, values: impl IntoIterator<Item = S>) {
        for v in values {
            for f in v.as_ref().split(',').map(str::trim).filter(|f| !f.is_empty()) {
                if !self.features.iter().any(|x| x == f) {
                    self.features.push(f.to_string());
                }
            }
        }
    }

    /// The flags as cargo spells them (`--features a,b --no-default-features
    /// --all-features`), for a log line or a receipt.
    #[must_use]
    pub fn argv(&self) -> Vec<String> {
        let mut a = Vec::new();
        if !self.features.is_empty() {
            a.push("--features".to_string());
            a.push(self.features.join(","));
        }
        if self.no_default_features {
            a.push("--no-default-features".to_string());
        }
        if self.all_features {
            a.push("--all-features".to_string());
        }
        a
    }
}

impl BuildVariant {
    /// This variant with the operator's [`CargoFeatures`] on top: chosen features
    /// appended to the declared ones (no repeats) and taken out of `open` (a picked
    /// switch is a decided value), the two flags OR-ed in.
    #[must_use]
    pub fn with_cargo_features(&self, cf: &CargoFeatures) -> BuildVariant {
        let mut v = self.clone();
        for f in &cf.features {
            if !v.features.iter().any(|x| x == f) {
                v.features.push(f.clone());
            }
            // chosen = decided: an OPEN feature the operator picked is no longer a
            // switch (validation refuses a feature that is both)
            v.open.remove(f);
        }
        v.no_default_features |= cf.no_default_features;
        v.all_features |= cf.all_features;
        v
    }

    /// The deliveries this variant ships in, in declaration order — or a single
    /// [`UNSUITED`] when it declares none.
    ///
    /// Blank and duplicate entries are dropped HERE rather than silently tolerated
    /// downstream; [`BuildVariantSet::check_suites`] reports a blank entry as a
    /// declaration problem, so dropping it does not hide it.
    pub fn suite_keys(&self) -> Vec<&str> {
        let mut out: Vec<&str> = Vec::new();
        for s in &self.suites {
            let s = s.trim();
            if !s.is_empty() && !out.contains(&s) {
                out.push(s);
            }
        }
        if out.is_empty() {
            out.push(UNSUITED);
        }
        out
    }

    /// The role label this variant groups under ([`UNROLED`] when it declares none).
    pub fn role_key(&self) -> &'static str {
        self.role.map(|r| r.as_str()).unwrap_or(UNROLED)
    }

    /// **The grouping keys this variant belongs to**: one `(suite, role)` per
    /// delivery it ships in.
    ///
    /// THE one writer of that key. [`BuildVariantSet::check_defaults`], the suite
    /// guards and the menu builder ([`crate::plugin::BuildThing::roles`]) all route
    /// through [`BuildVariantSet::groups`], which routes through here — so "which
    /// group is this in" cannot drift between the checker and the thing it checks.
    /// Two copies that have to agree is what let a delivery go silently defaultless
    /// in the first place.
    ///
    /// A variant in N suites returns N keys and is counted in N groups. That is the
    /// whole point of the list: `agent-online` is the MVP's default client AND the
    /// full product's, and a one-group reading would let the MVP's server group be
    /// empty of defaults without anything saying so.
    pub fn group_keys(&self) -> Vec<(&str, &'static str)> {
        let role = self.role_key();
        self.suite_keys().into_iter().map(|s| (s, role)).collect()
    }

    /// Materialize the exact `cargo` argv for this variant (starting with the
    /// `build` subcommand). Deterministic order: `build -p --bin --features
    /// --no-default-features --profile --target`.
    pub fn cargo_args(&self) -> Vec<String> {
        let mut a = vec!["build".to_string()];
        if let Some(p) = &self.package {
            a.push("-p".to_string());
            a.push(p.clone());
        }
        if let Some(b) = &self.bin {
            a.push("--bin".to_string());
            a.push(b.clone());
        }
        if let Some(e) = &self.example {
            a.push("--example".to_string());
            a.push(e.clone());
        }
        // A `Lib` kind builds the crate library (`--lib`), never a bin. `Wasm`
        // needs no extra arg here — its mandatory `--target wasm32-*` rides through
        // the `target` block below (validation guarantees it is present).
        if self.kind == ArtifactKind::Lib {
            a.push("--lib".to_string());
        }
        if !self.features.is_empty() {
            a.push("--features".to_string());
            a.push(self.features.join(","));
        }
        if self.no_default_features {
            a.push("--no-default-features".to_string());
        }
        if self.all_features {
            a.push("--all-features".to_string());
        }
        match &self.profile {
            Profile::Dev => {}
            Profile::Release => {
                a.push("--profile".to_string());
                a.push("release".to_string());
            }
            Profile::Custom(name) => {
                a.push("--profile".to_string());
                a.push(name.clone());
            }
        }
        if let Some(t) = &self.target {
            a.push("--target".to_string());
            a.push(t.clone());
        }
        self.push_manifest_path(&mut a);
        a
    }

    /// Append `--manifest-path <manifest_dir>/Cargo.toml` when the variant declares
    /// a [`manifest_dir`](Self::manifest_dir).
    ///
    /// ⚠ **THE FIELD WAS DECLARED, VALIDATED, AND RENDERED NOWHERE.** It has been
    /// on this struct since the detached-workspace work and `nornir-testmatrix`'s
    /// `VariantSpec` already renders it (`targetcheck.rs:249`) — but the two argv
    /// writers HERE, the one authority on what the forge actually runs, never
    /// emitted it. A repo declaring `manifest_dir` therefore got a build of
    /// whatever workspace `repo_root` happened to be, and the declaration changed
    /// nothing about the command.
    ///
    /// It rides BOTH writers deliberately. A test run that resolves a different
    /// workspace than the build it is meant to cover is the same defect
    /// `test_args` was created to end, in a new spelling.
    fn push_manifest_path(&self, a: &mut Vec<String>) {
        if let Some(d) = self.manifest_dir.as_deref().map(str::trim).filter(|d| !d.is_empty()) {
            a.push("--manifest-path".to_string());
            // Joined, not formatted: a declared `foo/` and a declared `foo` must
            // resolve to the same manifest.
            a.push(std::path::Path::new(d).join("Cargo.toml").to_string_lossy().into_owned());
        }
    }

    /// Human-readable one-liner, e.g. `cargo build -p holger-ui --bin holger-ui
    /// --features gui --profile release`.
    pub fn command_line(&self) -> String {
        format!("cargo {}", self.cargo_args().join(" "))
    }

    /// Materialize the exact `cargo test` argv for this variant — **the one
    /// writer** of what a forge test step runs.
    ///
    /// It existed nowhere. `dwarves`' `RealRunner::test_one` built it inline by
    /// taking [`cargo_args`](Self::cargo_args), overwriting `argv[0]` with `test`
    /// and filtering `--bin`/`--lib` back out of the vector it had just asked for
    /// (`dwarves/crates/dwarves/src/runner.rs:181`). That is the second copy of the
    /// variant→argv rule, living in the consumer, and it is where the
    /// one-package-per-test-step defect is spelled: the `-p` came along with
    /// everything else because nothing had ever asked whether it should.
    ///
    /// Differences from [`cargo_args`](Self::cargo_args), each for a reason:
    ///
    /// * verb `test`, not `build`;
    /// * **no `--bin` / `--lib`** — those select ONE build target, and a test run
    ///   wants the package's test targets (a `--lib` run compiles the lib's unit
    ///   tests but skips every `tests/*.rs`);
    /// * **extra `-p` for each [`test_packages`](Self::test_packages) entry**, the
    ///   sibling test surface the variant declares;
    /// * features **qualified** as `<package>/<feature>` as soon as more than one
    ///   package is selected, because an unqualified `--features` across a
    ///   multi-package selection is resolved against whichever selected package
    ///   happens to declare the name — right by accident for a unique feature name
    ///   and silently wrong for a name two members share.
    ///
    /// Profile, target and `--no-default-features` ride through unchanged, so the
    /// tests run in the configuration the artifact ships.
    pub fn test_args(&self) -> Vec<String> {
        let mut a = vec!["test".to_string()];
        if let Some(p) = &self.package {
            a.push("-p".to_string());
            a.push(p.clone());
        }
        for extra in &self.test_packages {
            a.push("-p".to_string());
            a.push(extra.clone());
        }
        if !self.features.is_empty() {
            a.push("--features".to_string());
            // Qualify only when the selection is wider than one package AND we have
            // a package name to qualify WITH. A single-crate repo declares no
            // `package`, so there is nothing to prefix and nothing ambiguous.
            let qualify = !self.test_packages.is_empty() && self.package.is_some();
            a.push(if qualify {
                let pkg = self.package.as_deref().unwrap_or_default();
                self.features
                    .iter()
                    .map(|f| format!("{pkg}/{f}"))
                    .collect::<Vec<_>>()
                    .join(",")
            } else {
                self.features.join(",")
            });
        }
        if self.no_default_features {
            a.push("--no-default-features".to_string());
        }
        if self.all_features {
            a.push("--all-features".to_string());
        }
        match &self.profile {
            Profile::Dev => {}
            Profile::Release => {
                a.push("--profile".to_string());
                a.push("release".to_string());
            }
            Profile::Custom(name) => {
                a.push("--profile".to_string());
                a.push(name.clone());
            }
        }
        if let Some(t) = &self.target {
            a.push("--target".to_string());
            a.push(t.clone());
        }
        self.push_manifest_path(&mut a);
        a
    }

    /// Human-readable one-liner for the test run, e.g.
    /// `cargo test -p holger-server-cli -p holger-server-lib --features holger-server-cli/full`.
    pub fn test_command_line(&self) -> String {
        format!("cargo {}", self.test_args().join(" "))
    }

    /// The environment variable a [`BuildVariant::boot_logo`] declaration travels
    /// to the build scripts in. Mirrors `korp_installer::splash::LOGO_ENV` — the
    /// two constants are the same wire and must not drift.
    pub const BOOT_LOGO_ENV: &'static str = "KORP_BOOT_LOGO";

    /// The absolute boot-logo path this variant declares, resolved against
    /// `repo_root`. `None` when the variant declares no mark (⇒ korp's default).
    pub fn boot_logo_path(&self, repo_root: &Path) -> Option<PathBuf> {
        let declared = self.boot_logo.as_deref()?.trim();
        if declared.is_empty() {
            return None;
        }
        let p = Path::new(declared);
        Some(if p.is_absolute() {
            p.to_path_buf()
        } else {
            repo_root.join(p)
        })
    }

    /// Validate against `set`, then run `cargo build …` in `repo_root`.
    ///
    /// An optional [`BuildVariant::cfg`] is passed through by appending
    /// `--cfg <cfg>` to any inherited `RUSTFLAGS`. An optional
    /// [`BuildVariant::boot_logo`] is exported as [`Self::BOOT_LOGO_ENV`] so korp's
    /// splash build step bakes the customer's mark instead of korp's raven. Fails if
    /// the variant is invalid for the set or if cargo exits non-zero.
    /// Where this variant's artifact lands, relative to the repo root.
    ///
    /// `target/[<triple>/]<profile-dir>/<bin>`, with `dev` spelled `debug`
    /// because that is what cargo calls the directory and not what it calls the
    /// profile. `None` for a variant with no `bin`: it builds the package's
    /// default target, which cannot be resolved without `cargo metadata`, and
    /// guessing a path that then does not exist would turn a build into a
    /// "missing artifact" for the wrong reason.
    ///
    /// The **one** writer of this convention. `plugin::BuildThing::plan` used to
    /// spell it out a second time for `expected_artifacts`, and two copies of a
    /// path rule is how a plan starts expecting a file the build never writes.
    #[must_use]
    pub fn artifact_rel_path(&self) -> Option<PathBuf> {
        Some(PathBuf::from("target").join(self.artifact_under_target()?))
    }

    /// The artifact's path **below the target directory** —
    /// `[<triple>/]<profile-dir>/<bin>`.
    ///
    /// Split from [`artifact_rel_path`](Self::artifact_rel_path) because the
    /// target directory is not always `<repo>/target`: `CARGO_TARGET_DIR` moves
    /// it, and a caller that has to go and *find* the artifact has to honour
    /// that. Everything under it is the same, so there is still one place that
    /// knows the convention.
    #[must_use]
    pub fn artifact_under_target(&self) -> Option<PathBuf> {
        let bin = self.bin.as_ref()?;
        let profile_dir = match String::from(self.profile.clone()).as_str() {
            "dev" => "debug".to_string(),
            other => other.to_string(),
        };
        Some(match &self.target {
            Some(t) => PathBuf::from(t).join(profile_dir).join(bin),
            None => PathBuf::from(profile_dir).join(bin),
        })
    }

    /// [`build`](Self::build) with extra environment for the cargo invocation.
    ///
    /// The one caller that needs it is the attesting delivery, which sets
    /// `CARGO_PROFILE_<PROFILE>_STRIP=none` so the compilation keeps the symbol
    /// table it is about to be audited on. Kept as a parameter on the real
    /// builder rather than a second builder: a delivery that compiled through a
    /// different function from the one every other path uses would be a
    /// different build, which is the whole thing being guarded against.
    ///
    /// # Errors
    ///
    /// As [`build`](Self::build).
    pub fn build_with_env(
        &self,
        set: &BuildVariantSet,
        repo_root: &Path,
        env: &[(String, String)],
    ) -> Result<()> {
        self.build_inner(set, repo_root, env)
    }

    pub fn build(&self, set: &BuildVariantSet, repo_root: &Path) -> Result<()> {
        self.build_inner(set, repo_root, &[])
    }

    fn build_inner(
        &self,
        set: &BuildVariantSet,
        repo_root: &Path,
        extra_env: &[(String, String)],
    ) -> Result<()> {
        set.validate(self)?;
        let args = self.cargo_args();
        let mut cmd = Command::new("cargo");
        cmd.current_dir(repo_root).args(&args);
        if let Some(cfg) = &self.cfg {
            let inherited = std::env::var("RUSTFLAGS").unwrap_or_default();
            let flags = format!("{inherited} --cfg {cfg}");
            cmd.env("RUSTFLAGS", flags.trim());
        }
        if let Some(logo) = self.boot_logo_path(repo_root) {
            cmd.env(Self::BOOT_LOGO_ENV, logo);
        }
        for (k, v) in extra_env {
            cmd.env(k, v);
        }
        let status = cmd
            .status()
            .with_context(|| format!("spawn cargo for variant '{}'", self.name))?;
        if !status.success() {
            return Err(anyhow!(
                "variant '{}' failed: `{}` exited with {}",
                self.name,
                self.command_line(),
                status
            ));
        }
        Ok(())
    }
}

/// **One package's cargo `[features]` table**, as declared — the manifest side of
/// the subset rule.
///
/// Read by [`BuildVariantSet::cargo_feature_tables`] and consumed by
/// [`BuildVariantSet::check_suites_resolved`]. Deliberately NOT a resolved graph: it
/// is the declaration verbatim, so the closure walk is a pure function over it and
/// can be tested without a repo on disk.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FeatureTable {
    /// `feature name → the list it enables`, including the `default` entry.
    pub features: BTreeMap<String, Vec<String>>,
    /// The crate's `default` list, hoisted out for the `no_default_features = false`
    /// case. Empty when the manifest declares no default.
    pub default: Vec<String>,
}

impl FeatureTable {
    /// **The transitive closure of a feature selection.**
    ///
    /// Expands any token that is a key in [`features`](Self::features); keeps every
    /// other token opaque — `dep:serde` and `egui/serde` never expand and are
    /// perfectly comparable across two suites as-is. `with_defaults` adds the crate
    /// `default` list first, which is what makes a variant carrying NO explicit
    /// features (holger's `server-local`) resolve to the four features cargo actually
    /// builds instead of to the empty set. Cycles terminate: a token is expanded at
    /// most once.
    /// **The CAPABILITIES a selection resolves to** — [`closure`](Self::closure) with
    /// every composite ALIAS dropped.
    ///
    /// A composite (`mvp-server = ["mini", "secure_update"]`) is a NAME for a tier,
    /// not a capability: it compiles nothing by itself. Two deliveries spell their
    /// tiers with different composite names on purpose, so comparing those names asks
    /// "did you pick the same label?" when the question is "did you enable more than
    /// the product does?".
    ///
    /// Measured 2026-08-02: with composites kept, holger's MVP reported `mvp-server`
    /// and `gui-mini` as capability escapes — the two tier labels themselves, and
    /// nothing else. Their expansions were already inside ORIGINAL's closure. Dropping
    /// the aliases leaves exactly the leaves and the opaque `dep:` / `crate/feat`
    /// tokens, which is what actually reaches cargo.
    ///
    /// A key whose list is EMPTY (`bench = []`) is a leaf, not an alias — it is a
    /// real marker a `#[cfg]` reads — so it is kept.
    pub fn leaf_closure(&self, selected: &[String], with_defaults: bool) -> BTreeSet<String> {
        self.closure(selected, with_defaults)
            .into_iter()
            .filter(|f| !self.features.get(f).is_some_and(|deps| !deps.is_empty()))
            .collect()
    }

    pub fn closure(&self, selected: &[String], with_defaults: bool) -> BTreeSet<String> {
        let mut out: BTreeSet<String> = BTreeSet::new();
        let mut queue: Vec<String> = selected.to_vec();
        if with_defaults {
            queue.extend(self.default.iter().cloned());
        }
        while let Some(f) = queue.pop() {
            if !out.insert(f.clone()) {
                continue;
            }
            if let Some(deps) = self.features.get(&f) {
                queue.extend(deps.iter().cloned());
            }
        }
        out
    }
}

/// One **declared buildable**, whatever its kind — the unified row.
///
/// See the [crate-root vocabulary](crate#the-vocabulary): a *variant* is the NOUN
/// (one declared buildable, one row in the list, it has a name) and *kind* is a
/// FIELD on it naming the shape of artifact it produces. This type is that pair,
/// plus a borrow of the concrete per-kind payload so nothing is flattened into a
/// god-struct.
///
/// Produced only by [`BuildVariantSet::declarations`] — the ONE enumeration of a
/// set's five storage collections. Every unified question
/// ([`declared`](BuildVariantSet::declared),
/// [`declared_names`](BuildVariantSet::declared_names),
/// [`declared_count`](BuildVariantSet::declared_count),
/// [`kind_of`](BuildVariantSet::kind_of), [`artifacts`](BuildVariantSet::artifacts))
/// is answered from it, so no call site has to remember that there are five places
/// to look.
///
/// # The POINTER VARIANT is one row, not two
///
/// The declared idiom for a non-cargo artifact is a PAIR: a `[[build.variant]]`
/// row carrying the `suites` tag and the `kind`, plus the sugar cell carrying the
/// descriptor. holger writes exactly that for `holger-installer` and
/// `holger-mvp-appliance`; korp writes it for `korp-appliance-iso`. Both halves
/// describe ONE buildable, so [`declarations`](BuildVariantSet::declarations)
/// emits ONE row for them, with [`variant`](Self::variant) borrowing the cargo row
/// and [`payload`](Self::payload) borrowing the descriptor cell.
///
/// Before 2026-08-23 the two halves were enumerated independently. MEASURED on the
/// real files that day: holger's `declared_count()` answered **18** for sixteen
/// buildables and `declared_names()` listed `holger-installer` and
/// `holger-mvp-appliance` TWICE (korp: 10 for nine, `korp-appliance-iso` twice) —
/// numbers that reached `dwarves buildthing --list`, its `variant_count` JSON and
/// the GUI picker. Worse, `declared()` returned the FIRST match, which is the cargo
/// row, so `as_installer()` / `as_container()` / `as_iso()` answered `None` on
/// every paired declaration: the descriptor was unreachable through the one lookup
/// that exists to reach it.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Declared<'a> {
    /// The declared name — the handle a human types.
    pub name: &'a str,
    /// The shape of artifact this row produces.
    pub kind: ArtifactKind,
    /// The `[[build.variant]]` row behind this declaration, when there is one — a
    /// plain cargo row, or the POINTER VARIANT of a paired declaration. `None` for
    /// a sugar cell no variant row claims.
    pub variant: Option<&'a BuildVariant>,
    /// The concrete descriptor this row carries: the sugar cell when one is
    /// resolved (see [`BuildVariantSet::payload_of`]), else the variant row itself.
    pub payload: DeclaredPayload<'a>,
}

/// The concrete descriptor behind a [`Declared`] row.
///
/// Deliberately an enum of BORROWS of the existing per-kind types rather than a
/// merged struct: an [`IsoBuild`](crate::iso::IsoBuild)'s boot payload and a
/// [`ContainerBuild`](crate::container::ContainerBuild)'s layer list are not the
/// same fields wearing different names, and flattening them would lose exactly the
/// detail the builders need.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DeclaredPayload<'a> {
    /// Declared as a `[[build.variant]]` row (any `kind`).
    Variant(&'a BuildVariant),
    /// Declared as a `[[build.container]]` row (sugar for `kind = "container"`).
    Container(&'a crate::container::ContainerBuild),
    /// Declared as a `[[build.vm]]` row (sugar for `kind = "vm"`).
    Vm(&'a crate::vm::VmBuild),
    /// Declared as a `[[build.iso]]` row (sugar for `kind = "iso"`).
    Iso(&'a crate::iso::IsoBuild),
    /// Declared as a `[[build.installer]]` row (sugar for `kind = "installer"`).
    Installer(&'a crate::installer::InstallerBuild),
    /// Declared as a `[[build.kernel]]` row (sugar for `kind = "kernel"`).
    Kernel(&'a crate::kernel::KernelBuild),
    /// Declared as a `[[build.package]]` row (sugar for `kind = "package"`).
    Package(&'a crate::package::PackageBuild),
}

impl<'a> Declared<'a> {
    /// The `[[build.variant]]` row behind this declaration, when there is one.
    /// `None` only for a sugar cell that no variant row claims.
    ///
    /// A POINTER VARIANT paired with its sugar cell answers `Some` here AND `Some`
    /// from the per-kind accessor below — one row, both halves reachable.
    pub fn as_variant(&self) -> Option<&'a BuildVariant> {
        self.variant
    }
    /// The container descriptor, when this row is a `[[build.container]]`.
    pub fn as_container(&self) -> Option<&'a crate::container::ContainerBuild> {
        match self.payload {
            DeclaredPayload::Container(c) => Some(c),
            _ => None,
        }
    }
    /// The vm descriptor, when this row is a `[[build.vm]]`.
    pub fn as_vm(&self) -> Option<&'a crate::vm::VmBuild> {
        match self.payload {
            DeclaredPayload::Vm(v) => Some(v),
            _ => None,
        }
    }
    /// The iso descriptor, when this row is a `[[build.iso]]`.
    pub fn as_iso(&self) -> Option<&'a crate::iso::IsoBuild> {
        match self.payload {
            DeclaredPayload::Iso(i) => Some(i),
            _ => None,
        }
    }
    /// The installer descriptor, when this row is a `[[build.installer]]`.
    pub fn as_installer(&self) -> Option<&'a crate::installer::InstallerBuild> {
        match self.payload {
            DeclaredPayload::Installer(i) => Some(i),
            _ => None,
        }
    }
    /// The kernel descriptor, when this row is a `[[build.kernel]]`.
    pub fn as_kernel(&self) -> Option<&'a crate::kernel::KernelBuild> {
        match self.payload {
            DeclaredPayload::Kernel(k) => Some(k),
            _ => None,
        }
    }
    /// The package descriptor, when this row is a `[[build.package]]`.
    pub fn as_package(&self) -> Option<&'a crate::package::PackageBuild> {
        match self.payload {
            DeclaredPayload::Package(p) => Some(p),
            _ => None,
        }
    }

    /// **The row as a [`BuildVariant`] node** — borrowed when it already is one,
    /// synthesized (name + kind, nothing else) when it was declared through a sugar
    /// section.
    ///
    /// The synthesized node is the unifying handle the rest of the crate already
    /// understands: [`output_of`](crate::output::output_of) and
    /// [`assemble`](crate::assemble::assemble) resolve a `kind = "iso"` /
    /// `kind = "installer"` variant to the declared variant OF THE SAME NAME, so a
    /// `[[build.installer]]` and a `[[build.variant]] kind = "installer"` land on
    /// exactly the same builder. One writer for "what does a picker put in its
    /// candidate slot", instead of one per UI.
    pub fn as_variant_node(&self) -> std::borrow::Cow<'a, BuildVariant> {
        match self.variant {
            Some(v) => std::borrow::Cow::Borrowed(v),
            None => std::borrow::Cow::Owned(BuildVariant {
                name: self.name.to_string(),
                kind: self.kind,
                ..BuildVariant::default()
            }),
        }
    }
}

/// The space of valid build choices a repo offers.
///
/// **Vocabulary** — see the [crate root](crate#the-vocabulary). Every row this set
/// holds is a *variant*: one declared buildable with a name and a `kind`. The six
/// storage collections below (`variants`, `containers`, `vms`, `isos`,
/// `installers`, `kernels`) are STORAGE, not six different nouns; ask them anything unified
/// through [`declarations`](Self::declarations) and its derivatives
/// ([`declared`](Self::declared), [`declared_names`](Self::declared_names),
/// [`declared_count`](Self::declared_count), [`kind_of`](Self::kind_of),
/// [`artifacts`](Self::artifacts)), never by searching one collection and hoping.
///
/// Loads from a `[build]` table (see the module docs). Validation catches the
/// three classes of "you can't build that here":
/// * a feature the repo never `offers`;
/// * two features from the same `exclusive` group at once;
/// * a `--bin` whose `required` features aren't all enabled.

/// **One entry in an allowlist table** — the reason, or the wreckage of a key
/// that was written without quotes.
///
/// `allow_unbuilt_bins` and `allow_dark_test_files` are keyed by FILE NAME, and a
/// file name contains a dot. `foo.rs = "why"` is not one key: TOML reads it as a
/// dotted key and builds `foo = { rs = "why" }`. Typing the map's value as a
/// plain `String` cannot represent that, so serde would reject the whole file
/// with "invalid type: map, expected a string" — turning a precise, fixable
/// declaration mistake into a parse failure for every consumer of the file.
///
/// This enum is what lets the STRUCT hold the malformed shape, which is what
/// lets the struct be the only reader. See
/// [`BuildVariantSet::allow_table`] for the split back into reasons and
/// problems, and [`AllowEntry::problem`] for the message.
/// `Eq` is deliberately ABSENT: the malformed arm holds a [`toml::Value`], which
/// can carry a float, and a float is not reflexively equal to itself. Claiming
/// `Eq` here would be a lie the compiler happens not to check. `PartialEq` is
/// what this type honestly has, and it is all any caller needs.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AllowEntry {
    /// The normal case: a non-empty reason.
    Reason(String),
    /// Anything else the file put there. Held rather than rejected so the gate
    /// can NAME it; see [`problem`](Self::problem).
    Malformed(toml::Value),
}

impl AllowEntry {
    /// The reason, when this entry is one.
    pub fn reason(&self) -> Option<&str> {
        match self {
            AllowEntry::Reason(r) => Some(r),
            AllowEntry::Malformed(_) => None,
        }
    }

    /// The operator-facing problem, when this entry is not a reason. The dotted-key
    /// case reconstructs the name the author meant and shows the quoting that fixes
    /// it — the whole point of keeping the malformed shape instead of refusing it.
    pub fn problem(&self, key: &str) -> Option<String> {
        let AllowEntry::Malformed(v) = self else { return None };
        Some(match v.as_table() {
            Some(sub) => format!(
                "`{key}` is a TABLE, not a reason — an unquoted file name is a TOML \
                 dotted key. Write `\"{key}.{}\" = \"…\"` with the quotes.",
                sub.keys().next().map(String::as_str).unwrap_or("…"),
            ),
            None => format!("`{key}`'s reason is not a string"),
        })
    }
}

/// **Closed too** — but only once the two allowlist tables above became fields.
/// Turning this on before that would have failed edda, gunnar and korp on
/// `allow_unbuilt_bins`, a key THIS CRATE was reading through
/// `buildgate::read_allowlist`'s raw side door. That ordering is the whole
/// lesson: a schema cannot be closed while something else is still answering for
/// it.
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BuildVariantSet {
    /// The feature space this repo offers. When non-empty, any variant feature
    /// outside this set is rejected. Empty means "unconstrained" (accept any).
    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
    pub offers: BTreeSet<String>,
    /// Mutually-exclusive feature groups: at most one feature per group may be
    /// enabled by a single variant (e.g. `["gui", "headless"]`).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub exclusive: Vec<Vec<String>>,
    /// Per-bin required features — mirrors Cargo `[[bin]] required-features`
    /// so a bad combo is rejected up front with a clear message.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub required: BTreeMap<String, Vec<String>>,
    /// **The honest allowlist for the feature-rot gate.** Features the repo
    /// `offers` (or its `Cargo.toml` declares) that NO variant builds ON PURPOSE,
    /// each mapped to a one-line REASON. A feature offered-but-unbuilt is ROT — it
    /// is the exact shape that let korp's `spark41` accumulate three invisible
    /// compile breaks — UNLESS it appears here with a reason, so the gate
    /// ([`crate::feature_axis::FeatureAxis::gate`]) can tell "known, accepted debt"
    /// from "silently rotting". The reason must be non-empty: an excuse with no
    /// words is not an excuse. An entry for a feature a variant now DOES build is a
    /// stale excuse and the gate fails on it too (keeping the allowlist itself from
    /// rotting).
    ///
    /// Loads from a `[build.allow_unbuilt]` table (`feature = "why nobody builds it"`).
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub allow_unbuilt: BTreeMap<String, String>,
    /// **Declared `[[bin]]` targets NO variant builds, on purpose** — keyed by bin
    /// name, valued with the reason. The bin-side twin of
    /// [`allow_unbuilt`](Self::allow_unbuilt), consumed by
    /// [`buildgate::tally_bins`](crate::buildgate::tally_bins).
    ///
    /// ⚠ **THIS FIELD DID NOT EXIST UNTIL 2026-08-26, AND THE TABLE WORKED
    /// ANYWAY** — which is the defect worth recording. Three repos declare it
    /// (edda 2 entries, gunnar 4, korp 2) and the schema had never heard of it.
    /// It worked because THIS CRATE read it through a second door:
    /// `buildgate::read_allowlist` re-opened the file as raw `toml::Value` and
    /// bypassed the struct entirely. A declaration honoured by a second parser
    /// the schema knows nothing about — inside the crate that DEFINES the
    /// schema, and the reason `deny_unknown_fields` could not be turned on here
    /// without breaking three working repos.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub allow_unbuilt_bins: BTreeMap<String, AllowEntry>,
    /// **Test files deliberately left dark** — keyed by file name, valued with the
    /// reason. Read by the dark-test gate
    /// ([`buildgate`](crate::buildgate)); declared by NO repo today, which is
    /// exactly why it belongs in the struct: an undeclared reader is how the
    /// field above went five months without a schema.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub allow_dark_test_files: BTreeMap<String, AllowEntry>,
    /// The suite **dependency graph**, as an embed-allowlist: for each member,
    /// the set of siblings strictly DOWNSTREAM of it — the only siblings a fat
    /// variant of that member may compile IN (`embeds`). Encodes the suite
    /// asymmetry: `dwarves = ["nornir", ...]` lets dwarves embed nornir, while
    /// nornir's row omits `dwarves` so a fat nornir that embeds dwarves (an
    /// upstream member — a dep-graph inversion) is rejected. Empty ⇒
    /// unconstrained (embeds accepted as-is), mirroring an empty `offers`.
    ///
    /// Loads from a `[build.downstream]` table (`member = ["sibling", …]`).
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub downstream: BTreeMap<String, BTreeSet<String>>,
    /// **The delivery the others are carved OUT of** — `base_suite = "full"`.
    ///
    /// When a repo ships more than one delivery out of one crate graph (see
    /// [`BuildVariant::suites`]), one of them is the whole product and the rest are
    /// subsets of it. Naming it gives [`check_suites`](Self::check_suites) a subject
    /// for `features(other) ⊆ features(base)` — "the MVP must not turn on anything
    /// the full product does not have".
    ///
    /// Deliberately **NOT inferred**. Guessing the base from variant counts, or from
    /// a name like `"full"`, would make the guard's subject depend on how the file
    /// happens to be written — and a guard whose subject you cannot see is the
    /// silent-green shape this axis exists to remove. Required as soon as a second
    /// suite appears; meaningless (and ignored) for a single-delivery repo.
    ///
    /// Loads from `[build] base_suite = "full"`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub base_suite: Option<String>,

    /// **The deliveries are SIBLINGS, not carve-outs.** Turns the subset rule off,
    /// out loud.
    ///
    /// Without this a repo has only two states: a base the others are carved out
    /// of, or a missing base, which is REPORTED as a defect. A repo whose
    /// deliveries are deliberately independent had no way to say so, and its only
    /// exits were to keep a `base_suite` nothing checks — a declaration that lies —
    /// or to bend the product until the rule held again.
    ///
    /// holger, 2026-09-20, is the case that made this exist. `mvp-server-metered`
    /// turns on `metered`, a commercial-only capability no ORIGINAL row declares,
    /// so MVP stopped being a subset of ORIGINAL. The owner ruled that the
    /// containment was never a requirement: the MVP has to WORK, not to be
    /// contained. That is a real shape — a paid edition is not a smaller edition —
    /// and the model had no word for it.
    ///
    /// Opt-IN, and a whole word somebody has to write, because the subset rule
    /// catches a real class of defect: a carved-out delivery quietly growing
    /// capability the product does not have. Turning it off must be a decision
    /// somebody made, not a field somebody forgot. With it set, `base_suite` is
    /// meaningless and declaring one anyway is REFUSED rather than ignored — two
    /// answers to "which is the base" is how a stale declaration survives.
    ///
    /// Loads from `[build] independent_suites = true`.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub independent_suites: bool,
    /// The named, declared variants.
    #[serde(default, rename = "variant", skip_serializing_if = "Vec::is_empty")]
    pub variants: Vec<BuildVariant>,
    /// The declared **image / appliance** builds — the container target kind,
    /// a sibling of the cargo `variants` above (`[[build.container]]` tables). A
    /// repo can declare zero of these (a pure cargo repo) or chain a cargo
    /// variant's artifact into an image layer (`from_variant`).
    #[serde(default, rename = "container", skip_serializing_if = "Vec::is_empty")]
    pub containers: Vec<crate::container::ContainerBuild>,
    /// The declared **VM / appliance boot** builds — the bootable target kind, a
    /// sibling of `variants` and `containers` (`[[build.vm]]` tables). A repo can
    /// declare zero of these, or chain a cargo variant's artifact into the VM's
    /// boot image (`from_variant`) — the "build holger, then boot it as a VM" chain.
    #[serde(default, rename = "vm", skip_serializing_if = "Vec::is_empty")]
    pub vms: Vec<crate::vm::VmBuild>,
    /// The declared **bootable-ISO** builds — the ISO target kind, a sibling of
    /// `variants`, `containers` and `vms` (`[[build.iso]]` tables). A repo can
    /// declare zero of these, or chain a cargo variant's artifact onto the ISO's
    /// boot payload (`from_variant`) — the "build it, then master an installer ISO"
    /// chain.
    #[serde(default, rename = "iso", skip_serializing_if = "Vec::is_empty")]
    pub isos: Vec<crate::iso::IsoBuild>,
    /// The declared **disk-installer** builds — the installer target kind, a
    /// sibling of `variants`, `containers`, `vms` and `isos` (`[[build.installer]]`
    /// tables). Each masters an *installer* ISO (via
    /// [`InstallerBuild::installer_iso`](crate::installer::InstallerBuild::installer_iso))
    /// that boots the `korp-installer` PID-1, lays a GPT + ESP, streams the app
    /// `.znippy` onto disk and reboots — the `nornir build --installer <name>` path.
    /// A repo can declare zero of these, or chain the cargo variant that produces the
    /// PID-1 payload (`installer_variant` → the ISO's `from_variant`).
    #[serde(default, rename = "installer", skip_serializing_if = "Vec::is_empty")]
    pub installers: Vec<crate::installer::InstallerBuild>,
    /// The declared **built-kernel** builds — the kernel target kind, a sibling
    /// of the five collections above (`[[build.kernel]]` tables). Each compiles
    /// a PINNED vanilla Linux tarball inside a toolchain container under a named
    /// hardening profile, producing `bzImage` + `.config` + a pin file
    /// ([`KernelBuild`](crate::kernel::KernelBuild)).
    #[serde(default, rename = "kernel", skip_serializing_if = "Vec::is_empty")]
    pub kernels: Vec<crate::kernel::KernelBuild>,
    /// The declared **distribution package** builds — a produced FILE SET rather
    /// than a single artifact (`[[build.package]]` tables). A pip release is a
    /// wheel AND an sdist AND the `SHA256SUMS` a downloader checks them with AND a
    /// provenance record over the produced bytes; any three of those without the
    /// fourth is not a release anybody can verify, which is why the SET is what
    /// [`PackageBuild`](crate::package::PackageBuild) declares.
    #[serde(default, rename = "package", skip_serializing_if = "Vec::is_empty")]
    pub packages: Vec<crate::package::PackageBuild>,
    /// The declared **wasm cells** — a `kind = "wasm"` variant's TYPED post-build
    /// stages (`[[build.wasm]]` tables). A wasm cell is what turns cargo's raw
    /// `<stem>.wasm` into a bundle a consumer can load: wasm-bindgen's `_bg` split
    /// and JS glue, and the component manifest the crate prints about itself.
    ///
    /// OPTIONAL, and the absence is meaningful rather than a gap: every
    /// `kind = "wasm"` variant that predates this collection declares no cell, and
    /// for those the kind means what it always meant — a plain cargo wasm build
    /// ([`WasmBuild`](crate::wasm::WasmBuild)).
    #[serde(default, rename = "wasm", skip_serializing_if = "Vec::is_empty")]
    pub wasms: Vec<crate::wasm::WasmBuild>,
    /// The declared **browser web apps** — a `kind = "wasm"` variant built into a
    /// directory a browser loads (`[[build.web_app]]` tables): the page, the
    /// wasm-bindgen glue and the assets its `<link data-trunk …>` tags name.
    /// Claimed by the variant of the SAME NAME
    /// ([`WebAppBuild`](crate::web_app::WebAppBuild)).
    #[serde(default, rename = "web_app", skip_serializing_if = "Vec::is_empty")]
    pub web_apps: Vec<crate::web_app::WebAppBuild>,
    /// **The toolchain container the repo's CARGO variants compile in**
    /// (`[build.toolchain]`).
    ///
    /// Absent — the historical state of every repo — means the cargo variants
    /// spawn `cargo` in the host's environment, and the build's outcome is
    /// decided by whichever box the forge landed on. That is not hypothetical:
    /// `dwarves buildthing gunnar --variant gunnar-server` FAILED in 1.8 s on
    /// oden with `error[E0463]: can't find crate for core`, because every gunnar
    /// variant targets musl and that box's rustup had no musl std.
    ///
    /// Present ⇒ every cargo variant in the repo goes through the pinned image
    /// ([`CargoToolchain`](crate::cargo_container::CargoToolchain)). It is
    /// repo-level and NOT per-variant on purpose: "which box can build this" is
    /// a property of the whole delivery, and a per-variant spelling would let
    /// one variant quietly keep the host habit while the repo claimed to be
    /// hermetic.
    ///
    /// Sits beside the six build-kind collections rather than inside them
    /// because it is not a kind — it is the ENVIRONMENT the `variants`
    /// collection runs in, the way `offers` is the feature space they draw from.
    #[serde(default, rename = "toolchain", skip_serializing_if = "Option::is_none")]
    pub toolchain: Option<crate::cargo_container::CargoToolchain>,
}

/// Newtype wrapper so the SAME `[build]` schema parses whether it's embedded in
/// `nornir.toml` or lives in a standalone `.nornir/build-variants.toml`.
///
/// **Deliberately NOT `deny_unknown_fields`, and that is now load-bearing.** A
/// `nornir.toml` carries a dozen root tables this struct knows nothing about, so
/// the root has to be open — and because it is open, the root is the only place
/// in this format where a version marker can be added without every published
/// reader back to 0.1.3 refusing the whole file over it. That is why
/// [`schema_header`](Self::schema_header) lives here rather than inside
/// `[build]`, which IS closed. See [`crate::schema`], R1.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct BuildConfig {
    /// **`[nornir-build-thing] schema = N`** — the schema this recipe needs a
    /// reader to understand ([`crate::schema`], R1/R2).
    ///
    /// A real field rather than a key the loader peeks at and then leaves lying
    /// around, because R3 refuses on any ignored `build.*` path and the version
    /// marker may not be the first thing the drop detector trips over. Absent ⇒
    /// [`RECIPE_SCHEMA_FLOOR`](crate::schema::RECIPE_SCHEMA_FLOOR), which is
    /// every recipe written before 2026-08-30.
    #[serde(
        default,
        rename = "nornir-build-thing",
        skip_serializing_if = "Option::is_none"
    )]
    pub schema_header: Option<crate::schema::SchemaHeader>,
    #[serde(default)]
    pub build: BuildVariantSet,
}

impl BuildConfig {
    /// **The ONE way to build a `BuildConfig` for EMISSION**, and the reason R1
    /// cannot be forgotten by a writer.
    ///
    /// The header is computed from the set
    /// ([`schema::required_by`](crate::schema::required_by)), never chosen: a
    /// recipe that declares nothing past the floor emits no header at all and
    /// stays readable by every generation back to 0.1.3, and a recipe that DOES
    /// declare something new says so without anyone remembering to.
    ///
    /// Deliberately not `Default` + struct literal: an emitter that spelled
    /// `BuildConfig { build, ..Default::default() }` would compile, emit no
    /// header, and hand an old reader a file it then swallows in silence —
    /// which is the exact 2026-08-30 defect, arriving by a new road.
    pub fn emitting(set: &BuildVariantSet) -> Self {
        let (schema, _why) = crate::schema::required_by(set);
        BuildConfig {
            schema_header: (schema > crate::schema::RECIPE_SCHEMA_FLOOR)
                .then_some(crate::schema::SchemaHeader { schema }),
            build: set.clone(),
        }
    }
}

impl BuildVariantSet {
    /// This set with the cargo variant `name` built with `cf` on top
    /// ([`BuildVariant::with_cargo_features`]). An empty choice is the set as it is;
    /// a choice for a name that is no CARGO variant of the set is refused BY NAME —
    /// a feature has nowhere to go on an image / iso / installer row.
    pub fn with_cargo_features(&self, name: &str, cf: &CargoFeatures) -> Result<BuildVariantSet> {
        if cf.is_empty() {
            return Ok(self.clone());
        }
        let mut set = self.clone();
        match set.variants.iter_mut().find(|v| v.name == name) {
            Some(v) if v.kind.is_cargo() => {
                *v = v.with_cargo_features(cf);
                Ok(set)
            }
            Some(v) => bail!(
                "'{name}' is a {} build, not a cargo one: `{}` has nowhere to go",
                v.kind.as_str(),
                cf.argv().join(" ")
            ),
            None => bail!("'{name}' is no cargo variant of this declaration: `{}` has nowhere to go", cf.argv().join(" ")),
        }
    }

    /// **The one reader of an allowlist table**, split into the reasons the gate
    /// honours and the problems it must report.
    ///
    /// `table` is the declared table name (`allow_unbuilt_bins` /
    /// `allow_dark_test_files`). An unknown name yields two empties rather than a
    /// panic: the caller is naming a table, and a typo there should not take the
    /// build down — but it also cannot silently pass, because an unknown table
    /// has no entries and the gate has nothing to allow.
    pub fn allow_table(&self, table: &str) -> (BTreeMap<String, String>, Vec<String>) {
        let src = match table {
            "allow_unbuilt_bins" => &self.allow_unbuilt_bins,
            "allow_dark_test_files" => &self.allow_dark_test_files,
            _ => return (BTreeMap::new(), Vec::new()),
        };
        let mut ok = BTreeMap::new();
        let mut bad = Vec::new();
        for (k, v) in src {
            match v.reason() {
                Some(r) => {
                    ok.insert(k.clone(), r.to_string());
                }
                None => bad.extend(v.problem(k)),
            }
        }
        (ok, bad)
    }

    /// Parse a `[build]` document (the standalone-file / `nornir.toml` schema).
    ///
    /// **THE reader, and it now refuses to read PART of a recipe.** Three gates,
    /// in the order that makes each refusal name its own fix
    /// ([`crate::schema`]):
    ///
    /// 1. **R2** — a recipe declaring a schema past this reader's is refused BY
    ///    NAME, with the version to upgrade to, before serde sees a byte. The
    ///    alternative — measured on gunnar's committed recipe against published
    ///    0.1.10 — is `unknown field kernel_requires, expected one of name,
    ///    label, …`, which names a field the operator never wrote and a line
    ///    number in a file the code emitted.
    /// 2. **serde** — an unknown key on a CLOSED type is a typo or a hand-edit
    ///    of an emitted file. Unchanged, and still loud.
    /// 3. **R3** — an unknown key on an OPEN type (`InstallerBuild`, `IsoBuild`,
    ///    `ContainerBuild`) used to be dropped in silence. It is now a refusal
    ///    listing every dropped path. This is the gate that would have caught
    ///    the four paths published 0.1.10 swallowed out of gunnar's recipe
    ///    (`build.installer.{0,1}.{target,data}`) while `tests/toml_parity.rs`
    ///    was green: a mirror compared against a loader that discards what it
    ///    cannot parse agrees about nothing.
    ///
    /// One deserialize, not two: `serde_ignored` wraps the same pass that builds
    /// the set. There is no second parser to drift from this one.
    pub fn from_toml_str(text: &str) -> Result<Self> {
        crate::schema::check_readable(text)?;
        let mut ignored: Vec<String> = Vec::new();
        // toml >= 0.9: building the deserializer parses, so it can fail here.
        let de = toml::Deserializer::parse(text).map_err(|e| anyhow::anyhow!("{e}"))?;
        let cfg: BuildConfig = serde_ignored::deserialize(de, |path| {
            let p = path.to_string();
            if p == "build" || p.starts_with("build.") {
                ignored.push(p);
            }
        })
        .context("parse [build] variants")?;
        ignored.sort();
        ignored.dedup();
        crate::schema::refuse_dropped("this recipe", &ignored)?;
        Ok(cfg.build)
    }

    /// Load a standalone build-variants file (schema rooted at `[build]`).
    pub fn load_file(path: &Path) -> Result<Self> {
        let text =
            std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
        Self::from_toml_str(&text).with_context(|| format!("parse {}", path.display()))
    }

    /// The conventional standalone location: `<repo_root>/.nornir/build-variants.toml`.
    pub fn conventional_path(repo_root: &Path) -> PathBuf {
        repo_root.join(".nornir").join("build-variants.toml")
    }

    /// Discover + load `<repo_root>/.nornir/build-variants.toml` if present.
    /// `Ok(None)` when the repo declares no standalone variant file.
    pub fn discover(repo_root: &Path) -> Result<Option<Self>> {
        let path = Self::conventional_path(repo_root);
        if path.is_file() {
            Ok(Some(Self::load_file(&path)?))
        } else {
            Ok(None)
        }
    }

    /// **THE enumeration of everything this set declares** — one [`Declared`] row
    /// per declared buildable, across all six storage collections, in declaration
    /// order (`[[build.variant]]`, then the sugar sections `container`, `vm`,
    /// `iso`, `installer`, `kernel`).
    ///
    /// This is the ONE writer for "what does this repo declare". It exists because
    /// the alternative — five parallel searches at every call site — is the twin
    /// this suite forbids, and it had already cost real behaviour: a declared
    /// `[[build.installer]]` could not be found by name at all, because
    /// [`get`](Self::get) searches `self.variants` and nothing else. Add a sixth
    /// storage collection and this function is the only place that has to learn
    /// about it.
    ///
    /// Note the sugar sections carry their kind IMPLICITLY (an `[[build.iso]]` row
    /// is `kind = "iso"`, always), while a `[[build.variant]]` row carries whatever
    /// its `kind` field says — including `container` / `vm` / `iso` / `installer`.
    /// Both spellings land here identically; see the
    /// [crate-root vocabulary](crate#the-vocabulary).
    ///
    /// **A PAIRED declaration is ONE row.** When a variant row resolves a sugar
    /// cell through [`payload_of`](Self::payload_of) — the pointer-variant idiom
    /// holger and korp both write — the cell is that row's payload and is NOT
    /// enumerated again. See [`Declared`] for the measured cost of the two-row
    /// version this replaced.
    pub fn declarations(&self) -> Vec<Declared<'_>> {
        let mut out: Vec<Declared<'_>> = Vec::with_capacity(
            self.variants.len()
                + self.containers.len()
                + self.vms.len()
                + self.isos.len()
                + self.installers.len()
                + self.kernels.len()
                + self.packages.len(),
        );
        out.extend(self.variants.iter().map(|v| Declared {
            name: v.name.as_str(),
            kind: v.kind,
            variant: Some(v),
            payload: self
                .payload_of(v)
                .unwrap_or(DeclaredPayload::Variant(v)),
        }));
        // Only the sugar cells NO pointer variant claims get a row of their own.
        out.extend(self.unclaimed_containers().map(|c| Declared {
            name: c.name.as_str(),
            kind: ArtifactKind::Container,
            variant: None,
            payload: DeclaredPayload::Container(c),
        }));
        out.extend(self.unclaimed_vms().map(|v| Declared {
            name: v.name.as_str(),
            kind: ArtifactKind::Vm,
            variant: None,
            payload: DeclaredPayload::Vm(v),
        }));
        out.extend(self.unclaimed_isos().map(|i| Declared {
            name: i.name.as_str(),
            kind: ArtifactKind::Iso,
            variant: None,
            payload: DeclaredPayload::Iso(i),
        }));
        out.extend(self.unclaimed_installers().map(|i| Declared {
            name: i.name.as_str(),
            kind: ArtifactKind::Installer,
            variant: None,
            payload: DeclaredPayload::Installer(i),
        }));
        out.extend(self.unclaimed_kernels().map(|k| Declared {
            name: k.name.as_str(),
            kind: ArtifactKind::Kernel,
            variant: None,
            payload: DeclaredPayload::Kernel(k),
        }));
        out.extend(self.unclaimed_packages().map(|p| Declared {
            name: p.name.as_str(),
            kind: ArtifactKind::Package,
            variant: None,
            payload: DeclaredPayload::Package(p),
        }));
        out
    }

    /// **The sugar cell a `[[build.variant]]` row IS the pointer for** — the ONE
    /// writer of the pointer-variant resolution table.
    ///
    /// It is the same table [`validate_kind`](Self::validate_kind) validates and
    /// [`output_of`](crate::output::output_of) resolves, spelled once:
    ///
    /// | variant `kind` | the cell it points at |
    /// |---|---|
    /// | `Container` | the `[[build.container]]` named by `container_ref` |
    /// | `Vm` | the `[[build.vm]]` named by `vm_ref` |
    /// | `Iso` / `Installer` / `Kernel` | the cell of the SAME NAME (no ref field) |
    /// | `Exe` / `Lib` / `Wasm` / `Service` | none — a cargo row is its own payload |
    ///
    /// `None` also when the pointer DANGLES (a `container_ref` naming nothing, an
    /// `installer` variant with no cell). That is a declaration defect
    /// `validate_kind` reports for the ref kinds; here it just means the row has no
    /// descriptor to carry, and the row survives so the defect stays visible in the
    /// build list instead of vanishing from it.
    pub fn payload_of(&self, v: &BuildVariant) -> Option<DeclaredPayload<'_>> {
        match v.kind {
            ArtifactKind::Container => self
                .containers
                .iter()
                .find(|c| Some(c.name.as_str()) == v.container_ref.as_deref())
                .map(DeclaredPayload::Container),
            ArtifactKind::Vm => self
                .vms
                .iter()
                .find(|x| Some(x.name.as_str()) == v.vm_ref.as_deref())
                .map(DeclaredPayload::Vm),
            ArtifactKind::Iso => self
                .isos
                .iter()
                .find(|i| i.name == v.name)
                .map(DeclaredPayload::Iso),
            ArtifactKind::Installer => self
                .installers
                .iter()
                .find(|i| i.name == v.name)
                .map(DeclaredPayload::Installer),
            ArtifactKind::Kernel => self
                .kernels
                .iter()
                .find(|k| k.name == v.name)
                .map(DeclaredPayload::Kernel),
            ArtifactKind::Package => self
                .packages
                .iter()
                .find(|p| p.name == v.name)
                .map(DeclaredPayload::Package),
            ArtifactKind::Exe | ArtifactKind::Lib | ArtifactKind::Wasm | ArtifactKind::Service => {
                None
            }
        }
    }

    /// True when some `[[build.variant]]` row is the pointer for the sugar cell
    /// `name` of `kind` — i.e. the cell is already somebody's payload.
    fn cell_is_claimed(&self, kind: ArtifactKind, name: &str) -> bool {
        self.variants.iter().any(|v| {
            v.kind == kind
                && match kind {
                    ArtifactKind::Container => v.container_ref.as_deref() == Some(name),
                    ArtifactKind::Vm => v.vm_ref.as_deref() == Some(name),
                    _ => v.name == name,
                }
        })
    }

    fn unclaimed_containers(&self) -> impl Iterator<Item = &crate::container::ContainerBuild> {
        self.containers
            .iter()
            .filter(|c| !self.cell_is_claimed(ArtifactKind::Container, &c.name))
    }
    fn unclaimed_vms(&self) -> impl Iterator<Item = &crate::vm::VmBuild> {
        self.vms
            .iter()
            .filter(|v| !self.cell_is_claimed(ArtifactKind::Vm, &v.name))
    }
    fn unclaimed_isos(&self) -> impl Iterator<Item = &crate::iso::IsoBuild> {
        self.isos
            .iter()
            .filter(|i| !self.cell_is_claimed(ArtifactKind::Iso, &i.name))
    }
    fn unclaimed_installers(&self) -> impl Iterator<Item = &crate::installer::InstallerBuild> {
        self.installers
            .iter()
            .filter(|i| !self.cell_is_claimed(ArtifactKind::Installer, &i.name))
    }
    fn unclaimed_kernels(&self) -> impl Iterator<Item = &crate::kernel::KernelBuild> {
        self.kernels
            .iter()
            .filter(|k| !self.cell_is_claimed(ArtifactKind::Kernel, &k.name))
    }

    fn unclaimed_packages(&self) -> impl Iterator<Item = &crate::package::PackageBuild> {
        self.packages
            .iter()
            .filter(|p| !self.cell_is_claimed(ArtifactKind::Package, &p.name))
    }

    /// **Give me the declared variant named `name`, whatever its kind.** The one
    /// lookup a CLI / UI / forge should reach for; [`get`](Self::get) is the
    /// narrower "…and it must be a `[[build.variant]]` row" question.
    pub fn declared(&self, name: &str) -> Option<Declared<'_>> {
        self.declarations().into_iter().find(|d| d.name == name)
    }

    /// [`declared`](Self::declared) or a helpful error listing every declared name
    /// (not just the cargo ones — the list that used to omit the very row the
    /// caller asked for).
    pub fn require_declared(&self, name: &str) -> Result<Declared<'_>> {
        self.declared(name).ok_or_else(|| {
            let known = self.declared_names();
            anyhow!(
                "no build variant '{}'; declared: {}",
                name,
                if known.is_empty() {
                    "<none declared>".to_string()
                } else {
                    known.join(", ")
                }
            )
        })
    }

    /// Every declared name, in declaration order — the honest build list.
    pub fn declared_names(&self) -> Vec<&str> {
        self.declarations().into_iter().map(|d| d.name).collect()
    }

    /// **How many buildables this repo declares** — the number a "N variant(s)
    /// declared" line must print. Summing `self.variants` alone under-reports every
    /// container, vm, iso and installer in the file.
    ///
    /// A PROJECTION of [`declarations`](Self::declarations), not a second sum: the
    /// six-collection sum it used to be over-reported every PAIRED declaration
    /// (holger 18 for sixteen buildables, korp 10 for nine — measured 2026-08-23),
    /// and a count that disagrees with the list beside it is the drift this crate
    /// exists to prevent.
    pub fn declared_count(&self) -> usize {
        self.declarations().len()
    }

    /// The `[[build.variant]]`-row names, in declaration order.
    ///
    /// ⚠ This is the CARGO-ROW collection, not the repo's build list — a declared
    /// `[[build.container]]` / `[[build.iso]]` / `[[build.installer]]` is a variant
    /// too and does not appear here. Use [`declared_names`](Self::declared_names)
    /// for "what can I build in this repo".
    pub fn names(&self) -> Vec<&str> {
        self.variants.iter().map(|v| v.name.as_str()).collect()
    }

    /// Look up a `[[build.variant]]` row by name.
    ///
    /// ⚠ Searches only the cargo rows. A declared installer / container / iso / vm
    /// is invisible here BY DESIGN — this function's job is to hand back a
    /// [`BuildVariant`]. For "the declared thing called `name`, whatever it is",
    /// use [`declared`](Self::declared).
    pub fn get(&self, name: &str) -> Option<&BuildVariant> {
        self.variants.iter().find(|v| v.name == name)
    }

    /// Look up a variant by name, or a helpful error listing the valid names.
    pub fn require(&self, name: &str) -> Result<&BuildVariant> {
        self.get(name).ok_or_else(|| {
            anyhow!(
                "no build variant '{}'; known variants: {}",
                name,
                if self.variants.is_empty() {
                    "<none declared>".to_string()
                } else {
                    self.names().join(", ")
                }
            )
        })
    }

    /// Validate a single variant against this set's rules.
    ///
    /// Rejects: a feature not in `offers` (when `offers` is declared), an
    /// [`open`](BuildVariant::open) feature that is not offered or that the variant
    /// already enables, two features from one `exclusive` group, and a `--bin`
    /// missing any of its `required` features.
    pub fn validate(&self, v: &BuildVariant) -> Result<()> {
        // Per-row `siblings` are directory names under the repo's parent — one
        // component each, never a path — and a cargo row's business only.
        if !v.siblings.is_empty() {
            if !v.kind.is_cargo() {
                return Err(anyhow!(
                    "variant '{}': `siblings` on a kind '{}' row — per-row sibling checkouts \
                     are mounted for a cargo build; an assembled kind mounts none",
                    v.name,
                    v.kind.as_str()
                ));
            }
            let mut seen = BTreeSet::new();
            for s in &v.siblings {
                let one_component = !s.is_empty()
                    && s != "."
                    && s != ".."
                    && !s.contains('/')
                    && !s.contains('\\');
                if !one_component {
                    return Err(anyhow!(
                        "variant '{}': sibling `{s}` is not a directory name beside the repo — \
                         a sibling is ONE path component (`edda`, not `../edda`)",
                        v.name
                    ));
                }
                if !seen.insert(s.as_str()) {
                    return Err(anyhow!("variant '{}': sibling `{s}` is declared twice", v.name));
                }
            }
        }
        // `sign` is honoured by the forge on the CARGO kinds (and a web app, which
        // is a wasm variant). An assembled kind declaring it would be a signature
        // promised and never written, so it is refused here, by name.
        if let Some(s) = &v.sign {
            if !v.kind.is_cargo() {
                return Err(anyhow!(
                    "variant '{}': `sign = {{ purpose = \"{}\" }}` on a kind '{}' row — the signing \
                     step signs what a cargo build (exe/lib/wasm/service) or a web app produces; an \
                     assembled kind is not signed by it, and declaring it would promise a \
                     signature nothing writes",
                    v.name,
                    s.purpose.as_str(),
                    v.kind.as_str()
                ));
            }
        }
        // 0. the edge rows are well-formed, and no edge is also an `open` feature
        //    (a feature cannot be both switchable here and consumed from elsewhere).
        crate::tree::validate_edge_rows(&format!("variant '{}'", v.name), None, &v.edges, &[])?;
        for f in v.edges.keys() {
            if v.open.contains(f) || v.features.contains(f) {
                return Err(anyhow!(
                    "variant '{}': '{f}' is declared twice — as a consumption edge and as a \
                     cargo feature of this variant; a feature is one or the other",
                    v.name
                ));
            }
        }
        crate::tree::SettingsTree::of_variant(v).validate_presets()?;
        // 1. every enabled feature must be offered (skip when unconstrained).
        if !self.offers.is_empty() {
            for f in &v.features {
                if !self.offers.contains(f) {
                    return Err(anyhow!(
                        "variant '{}': feature '{}' is not offered by this repo (offered: {})",
                        v.name,
                        f,
                        self.offers.iter().cloned().collect::<Vec<_>>().join(", ")
                    ));
                }
            }
        }
        // 1b. the row's own OPEN axis: drawn from the same space, and disjoint from
        //     the features the row already decided.
        for f in &v.open {
            if !self.offers.is_empty() && !self.offers.contains(f) {
                return Err(anyhow!(
                    "variant '{}': open feature '{}' is not offered by this repo (offered: {})",
                    v.name,
                    f,
                    self.offers.iter().cloned().collect::<Vec<_>>().join(", ")
                ));
            }
            if v.features.contains(f) {
                return Err(anyhow!(
                    "variant '{}': feature '{}' is both enabled and open — it cannot be a \
                     decided value and a switch at the same time",
                    v.name,
                    f
                ));
            }
        }
        // 1c. the row's declared SIBLING TEST SURFACE.
        //
        // Three rejections, each a way the widened selection would stop being the
        // configuration the build ships — which is the whole reason the test step
        // inherits the variant in the first place.
        if !v.test_packages.is_empty() {
            if v.package.is_none() {
                return Err(anyhow!(
                    "variant '{}': `test-packages` names sibling packages but the variant \
                     declares no `package` of its own, so there is no package to qualify its \
                     `--features` against and cargo would resolve them against whichever \
                     selected member happens to declare the name",
                    v.name
                ));
            }
            if v.no_default_features {
                return Err(anyhow!(
                    "variant '{}': `test-packages` cannot be combined with \
                     `no_default_features`. `--no-default-features` applies to EVERY selected \
                     package, so the siblings would be tested with their defaults off — but \
                     the build links them with whatever their dependency EDGE says, which is \
                     defaults-on unless that edge sets `default-features = false`. The test \
                     run would be a configuration this repo does not ship, which is the drift \
                     inheriting the variant exists to prevent.",
                    v.name
                ));
            }
            if !v.kind.is_cargo() {
                return Err(anyhow!(
                    "variant '{}': `test-packages` is a cargo test selection, but this row is \
                     kind `{}`, which cargo does not build at all",
                    v.name,
                    v.kind.as_str()
                ));
            }
            let own = v.package.as_deref().unwrap_or_default();
            let mut seen = BTreeSet::new();
            for p in &v.test_packages {
                if p.trim().is_empty() {
                    return Err(anyhow!("variant '{}': empty `test-packages` entry", v.name));
                }
                if p == own {
                    return Err(anyhow!(
                        "variant '{}': `test-packages` repeats the variant's own package \
                         '{p}' — it is already selected",
                        v.name
                    ));
                }
                if !seen.insert(p.clone()) {
                    return Err(anyhow!(
                        "variant '{}': `test-packages` names '{p}' twice",
                        v.name
                    ));
                }
            }
        }
        // 2. mutually-exclusive groups.
        for group in &self.exclusive {
            let hits: Vec<&String> = group.iter().filter(|g| v.features.contains(g)).collect();
            if hits.len() > 1 {
                return Err(anyhow!(
                    "variant '{}': features {:?} are mutually exclusive",
                    v.name,
                    hits
                ));
            }
        }
        // 3. Cargo required-features for the selected bin.
        if let Some(bin) = &v.bin {
            if let Some(req) = self.required.get(bin) {
                for r in req {
                    if !v.features.contains(r) {
                        return Err(anyhow!(
                            "variant '{}': bin '{}' requires feature '{}' but it is not enabled \
                             (Cargo required-features)",
                            v.name,
                            bin,
                            r
                        ));
                    }
                }
            }
        }
        // 4. fat/thin shape coherence + the suite ASYMMETRY rule.
        self.validate_shape(v)?;
        // 5. artifact-kind coherence (kind × target / service / bin / composition).
        self.validate_kind(v)?;
        Ok(())
    }

    /// The **artifact-kind** half of [`validate`](Self::validate): enforce that a
    /// variant's [`kind`](BuildVariant::kind) agrees with its target, service
    /// identity, bin, delegation, and composition fields. Orthogonal to
    /// [`validate_shape`](Self::validate_shape) — the two axes have independent
    /// validators.
    ///
    /// The coherence matrix:
    ///
    /// | kind | requires | forbids |
    /// |---|---|---|
    /// | `Exe` | — | `embeds` (composes nothing) |
    /// | `Lib` | — | `bin`, `embeds` |
    /// | `Wasm` | `target = wasm32-*` | `embeds` |
    /// | `Service` | `member` | a wasm target |
    /// | `Container` | `container_ref` → declared | — |
    /// | `Vm` | `vm_ref` → declared | — |
    ///
    /// `embeds` (compiled-in siblings) is only meaningful for the service-shaped
    /// kinds (`Service`, `Container`); a non-service that `embeds` is incoherent.
    /// `reaches` (a remote gRPC dependency) stays allowed for any kind — an `Exe`
    /// UI/tool may be a thin client of a remote owner without composing anything in.
    pub fn validate_kind(&self, v: &BuildVariant) -> Result<()> {
        // An example is its own cargo target: it cannot ALSO be a `--bin`, and a
        // library row builds the lib and nothing else.
        if v.example.is_some() && v.bin.is_some() {
            return Err(anyhow!(
                "variant '{}': `example` and `bin` both name the target to build — pick one",
                v.name
            ));
        }
        if v.example.is_some() && v.kind == ArtifactKind::Lib {
            return Err(anyhow!(
                "variant '{}': kind 'lib' builds the package library and may not select an `example`",
                v.name
            ));
        }
        match v.kind {
            // **A DELIBERATE REVERSAL (2026-08-15).** This arm used to REFUSE
            // `kind = "installer"` on a variant. Its words, verbatim, were
            // "kind 'installer' is not an atom kind — declare an
            // [[build.installer]] cell instead". Under the vocabulary ruling
            // (see the crate root) `kind` is a FIELD on a variant, and
            // `installer` is one of the six legal values; `[[build.installer]]`
            // is permanent SUGAR for it, not a rival noun. So the refusal is
            // gone.
            //
            // Like `Iso`, an installer variant carries its handle in its own
            // NAME (there is no delegating `installer_ref` field):
            // `output_of` and `assemble::dispatch` resolve it to the declared
            // `[[build.installer]]` of the same name. That makes it a leaf here,
            // with no extra constraint — exactly the `Iso` and `Exe` shape.
            ArtifactKind::Installer => {}
            // Wasm ⇒ a wasm32 target is mandatory.
            ArtifactKind::Wasm => {
                let t = v.target.as_deref().unwrap_or_default();
                if !t.starts_with("wasm32") {
                    return Err(anyhow!(
                        "variant '{}': kind 'wasm' requires a wasm32 `target` \
                         (e.g. wasm32-unknown-unknown), got '{}'",
                        v.name,
                        if t.is_empty() { "<none>" } else { t }
                    ));
                }
            }
            // Service ⇒ must resolve a ServiceSpec identity: needs a `member` (the
            // dedicated system-user name skidbladnir installs it under).
            ArtifactKind::Service => {
                if v.member.as_deref().unwrap_or("").trim().is_empty() {
                    return Err(anyhow!(
                        "variant '{}': kind 'service' requires a `member` \
                         (the dedicated system-user / ServiceSpec identity)",
                        v.name
                    ));
                }
                // A service is a native daemon, never a wasm module.
                if v.target
                    .as_deref()
                    .map_or(false, |t| t.starts_with("wasm32"))
                {
                    return Err(anyhow!(
                        "variant '{}': a 'service' is a native daemon, not a wasm target",
                        v.name
                    ));
                }
            }
            // Lib ⇒ builds the crate library, so no `--bin` may be selected.
            ArtifactKind::Lib => {
                if v.bin.is_some() {
                    return Err(anyhow!(
                        "variant '{}': kind 'lib' builds the package library and \
                         may not select a `bin`",
                        v.name
                    ));
                }
                // A library composes nothing and runs nowhere, so the fat/thin axis
                // has no answer for it — and the DEFAULT answer is 'fat', a positive
                // claim of self-containment. Silence would therefore read as a claim
                // nobody made. Say it does not apply.
                if v.shape.is_applicable() {
                    return Err(anyhow!(
                        "variant '{}': kind 'lib' has no fat/thin linkage — it is linked by \
                         whoever consumes it — but declares linkage '{}'. Say \
                         `linkage = \"not-applicable\"`, which is NOT the same as omitting \
                         the key (an omitted linkage reads as 'fat').",
                        v.name,
                        v.shape.as_str()
                    ));
                }
            }
            // Delegating nodes must point at a declared descriptor.
            ArtifactKind::Container => {
                let name = v.container_ref.as_deref().ok_or_else(|| {
                    anyhow!(
                        "variant '{}': kind 'container' needs `container_ref = \"…\"` \
                         (the [[build.container]] it delegates to)",
                        v.name
                    )
                })?;
                // Name the CONSUMER, not just the dangling pointer: `require_container`
                // is shared by every caller and cannot know who asked, and a refusal
                // that says only "no container build 'nowhere'" leaves the reader to
                // grep for who names it.
                // ONE string, not a context chain: callers read this through
                // `to_string()` as often as through `{:#}`, and a chain hides the
                // inner half from the first of those.
                self.require_container(name)
                    .map_err(|e| anyhow!("variant '{}': container_ref: {e}", v.name))?;
                refuse_delegated_edges(&v.name, "container_ref", &v.edges)?;
            }
            ArtifactKind::Vm => {
                let name = v.vm_ref.as_deref().ok_or_else(|| {
                    anyhow!(
                        "variant '{}': kind 'vm' needs `vm_ref = \"…\"` \
                         (the [[build.vm]] it delegates to)",
                        v.name
                    )
                })?;
                self.require_vm(name)?;
                refuse_delegated_edges(&v.name, "vm_ref", &v.edges)?;
            }
            // A bootable ISO is declared directly as a `[[build.iso]]` table
            // (validated via [`validate_iso`](Self::validate_iso)), not as a cargo
            // variant delegating to one — so a `kind = "iso"` variant carries no
            // extra constraint here (the analogue of the leaf `Exe`).
            ArtifactKind::Iso => {}
            // Same shape as `Iso`/`Installer`: a `kind = "kernel"` variant carries
            // its handle in its own NAME and resolves to the declared
            // `[[build.kernel]]` of the same name (validated via
            // [`validate_kernel`](Self::validate_kernel)) — a leaf here.
            ArtifactKind::Kernel => {}
            // **A package is the one same-name kind that REFUSES a missing cell**,
            // and the difference from `Iso`/`Installer`/`Kernel` above is worth
            // stating rather than being read as an inconsistency.
            //
            // Those three can be leaves because a missing cell still leaves them
            // something true to say: `output_of` falls back to naming the variant
            // itself, and the row survives in the build list carrying a handle. A
            // package's OUTPUT IS THE MEMBER SET — the distribution name, the
            // directory, and which of wheel/sdist/sums/provenance must be there.
            // None of that can be inferred from a variant name, so a `kind =
            // "package"` row without its cell is not a row with a dangling
            // pointer; it is a release nobody described, and describing it anyway
            // would mean inventing a distribution name and asserting a set.
            ArtifactKind::Package => {
                if !self.packages.iter().any(|p| p.name == v.name) {
                    return Err(anyhow!(
                        "variant '{}': kind 'package' needs a [[build.package]] cell                          of the SAME NAME (the iso/installer/kernel rule — there is no                          delegating package_ref field). The cell is where the produced                          set is declared: the distribution name, the out_dir, and                          whether an sdist is part of the release. Without it there is                          nothing to say what this build produces",
                        v.name
                    ));
                }
            }
            ArtifactKind::Exe => {}
        }
        // Composition (embeds) is meaningful for the service-shaped kinds AND for
        // a plain `exe`. The old rule refused it on `exe` ("composes no service"),
        // and two repos hit that wall with artifacts that genuinely compile a
        // sibling in: dwarves-solo wrote `embeds = []` under a comment calling the
        // refusal "a SCHEMA limit and not a claim" and put the truth in prose, and
        // korp's window-having atoms (korp / korp-solo) were mis-declared
        // `kind = "service"` partly because service was the only kind that could
        // carry their real `embeds = ["skade"]`. The ?2 UI-drive ruling
        // (2026-08-17) moves those atoms to `exe`, so `embeds` follows: it names
        // what the LINKER KEPT, which is a fact about any executable artifact, not
        // about service management. The real coherence rules are kind-independent
        // and live in `validate_shape` (thin never embeds, embeds ∩ reaches empty,
        // downstream direction only). Still refused where it is genuinely
        // incoherent: a lib / wasm module / delegating pointer kind links no
        // sibling binary in.
        if !v.embeds.is_empty()
            && !v.kind.is_service_shaped()
            && v.kind != ArtifactKind::Exe
        {
            return Err(anyhow!(
                "variant '{}': `embeds` requires kind 'service', 'container' or 'exe'; \
                 kind '{}' composes nothing in",
                v.name,
                v.kind.as_str()
            ));
        }
        Ok(())
    }

    /// The fat/thin half of [`validate`](Self::validate): shape coherence and the
    /// suite **asymmetry** rule — a member may embed only what is DOWNSTREAM of it.
    ///
    /// Rejects:
    /// * `embeds` ∩ `reaches` non-empty (a sibling can't be both compiled-in and
    ///   reached over gRPC);
    /// * a [`ServiceShape::ThinStandalone`] variant that nonetheless `embeds`
    ///   anything (thin compiles in NO siblings);
    /// * a fat/mixed variant that embeds a sibling NOT downstream of its `member`
    ///   in [`downstream`](Self::downstream) — the dep-graph inversion (e.g. nornir
    ///   embedding dwarves). Skipped when the graph is empty (unconstrained).
    pub fn validate_shape(&self, v: &BuildVariant) -> Result<()> {
        // embeds and reaches must be disjoint.
        let both: Vec<&String> = v.embeds.intersection(&v.reaches).collect();
        if !both.is_empty() {
            return Err(anyhow!(
                "variant '{}': sibling(s) {:?} are BOTH embedded and reached over gRPC \
                 (embeds and reaches must be disjoint)",
                v.name,
                both
            ));
        }
        // 'not-applicable' is a claim that this artifact has no sibling relationship
        // at all. If it embeds or reaches one, the axis plainly DOES apply and the
        // declaration is hiding a real runtime requirement behind an opt-out.
        if v.shape == ServiceShape::NotApplicable && !(v.embeds.is_empty() && v.reaches.is_empty())
        {
            return Err(anyhow!(
                "variant '{}': linkage 'not-applicable' but it embeds {:?} and reaches {:?} — \
                 the axis applies to anything with a sibling edge; declare 'fat', 'thin' or \
                 'mixed'",
                v.name,
                v.embeds.iter().collect::<Vec<_>>(),
                v.reaches.iter().collect::<Vec<_>>()
            ));
        }
        // A thin standalone compiles in nothing.
        if v.shape == ServiceShape::ThinStandalone && !v.embeds.is_empty() {
            return Err(anyhow!(
                "variant '{}': shape 'thin' embeds {:?}, but a thin/standalone build compiles \
                 in NO siblings (move them to `reaches`, or use shape 'fat'/'mixed')",
                v.name,
                v.embeds.iter().collect::<Vec<_>>()
            ));
        }
        // The asymmetry rule: embed only downstream. Only enforced when the suite
        // graph is declared AND this variant embeds something.
        if !self.downstream.is_empty() && !v.embeds.is_empty() {
            let member = v.member.as_deref().ok_or_else(|| {
                anyhow!(
                    "variant '{}': embeds {:?} but declares no `member`, so its position in the \
                     suite dep graph is unknown (set `member = \"…\"`)",
                    v.name,
                    v.embeds.iter().collect::<Vec<_>>()
                )
            })?;
            // Empty allowlist when the member is absent from the graph → any embed
            // is an inversion.
            let allowed = self.downstream.get(member);
            for e in &v.embeds {
                let ok = allowed.map(|d| d.contains(e)).unwrap_or(false);
                if !ok {
                    return Err(anyhow!(
                        "variant '{}': member '{}' may not embed '{}' — it is not DOWNSTREAM of \
                         '{}' in the suite dep graph (embedding an upstream/peer member inverts \
                         the graph; reach it over gRPC instead). Downstream of '{}': {}",
                        v.name,
                        member,
                        e,
                        member,
                        member,
                        allowed
                            .map(|d| if d.is_empty() {
                                "<none>".to_string()
                            } else {
                                d.iter().cloned().collect::<Vec<_>>().join(", ")
                            })
                            .unwrap_or_else(|| "<member not in graph>".to_string())
                    ));
                }
            }
        }
        Ok(())
    }

    /// The declared image/appliance build names, in declaration order.
    pub fn container_names(&self) -> Vec<&str> {
        self.containers.iter().map(|c| c.name.as_str()).collect()
    }

    /// Look up an image/appliance build by name.
    pub fn container(&self, name: &str) -> Option<&crate::container::ContainerBuild> {
        self.containers.iter().find(|c| c.name == name)
    }

    /// Look up an image build by name, or a helpful error listing the known ones.
    pub fn require_container(&self, name: &str) -> Result<&crate::container::ContainerBuild> {
        self.container(name).ok_or_else(|| {
            anyhow!(
                "no container build '{}'; known images: {}",
                name,
                if self.containers.is_empty() {
                    "<none declared>".to_string()
                } else {
                    self.container_names().join(", ")
                }
            )
        })
    }

    /// The declared VM/appliance build names, in declaration order.
    pub fn vm_names(&self) -> Vec<&str> {
        self.vms.iter().map(|vm| vm.name.as_str()).collect()
    }

    /// Look up a VM/appliance build by name.
    pub fn vm(&self, name: &str) -> Option<&crate::vm::VmBuild> {
        self.vms.iter().find(|vm| vm.name == name)
    }

    /// Look up a VM build by name, or a helpful error listing the known ones.
    /// Mirrors [`require_container`](Self::require_container) so a CLI/UI can
    /// resolve a [`VmBuild`](crate::vm::VmBuild) by name the same way.
    pub fn require_vm(&self, name: &str) -> Result<&crate::vm::VmBuild> {
        self.vm(name).ok_or_else(|| {
            anyhow!(
                "no vm build '{}'; known vms: {}",
                name,
                if self.vms.is_empty() {
                    "<none declared>".to_string()
                } else {
                    self.vm_names().join(", ")
                }
            )
        })
    }

    /// The declared bootable-ISO build names, in declaration order.
    pub fn iso_names(&self) -> Vec<&str> {
        self.isos.iter().map(|iso| iso.name.as_str()).collect()
    }

    /// Look up a bootable-ISO build by name.
    pub fn iso(&self, name: &str) -> Option<&crate::iso::IsoBuild> {
        self.isos.iter().find(|iso| iso.name == name)
    }

    /// Look up an ISO build by name, or a helpful error listing the known ones.
    /// Mirrors [`require_vm`](Self::require_vm).
    pub fn require_iso(&self, name: &str) -> Result<&crate::iso::IsoBuild> {
        self.iso(name).ok_or_else(|| {
            anyhow!(
                "no iso build '{}'; known isos: {}",
                name,
                if self.isos.is_empty() {
                    "<none declared>".to_string()
                } else {
                    self.iso_names().join(", ")
                }
            )
        })
    }

    /// The declared disk-installer build names, in declaration order.
    pub fn installer_names(&self) -> Vec<&str> {
        self.installers.iter().map(|i| i.name.as_str()).collect()
    }

    /// Look up a disk-installer build by name — the `nornir build --installer <name>`
    /// resolver. The returned [`InstallerBuild`](crate::installer::InstallerBuild)
    /// materializes the installer ISO
    /// ([`installer_iso`](crate::installer::InstallerBuild::installer_iso)) and the
    /// KVM boot seam.
    pub fn installer(&self, name: &str) -> Option<&crate::installer::InstallerBuild> {
        self.installers.iter().find(|i| i.name == name)
    }

    /// Look up an installer build by name, or a helpful error listing the known ones.
    /// Mirrors [`require_iso`](Self::require_iso) — the resolver a CLI/UI calls to pick
    /// an installer variant and drive its [`installer_iso`](crate::installer::InstallerBuild::installer_iso).
    pub fn require_installer(&self, name: &str) -> Result<&crate::installer::InstallerBuild> {
        self.installer(name).ok_or_else(|| {
            anyhow!(
                "no installer build '{}'; known installers: {}",
                name,
                if self.installers.is_empty() {
                    "<none declared>".to_string()
                } else {
                    self.installer_names().join(", ")
                }
            )
        })
    }

    /// The declared built-kernel build names, in declaration order.
    pub fn kernel_names(&self) -> Vec<&str> {
        self.kernels.iter().map(|k| k.name.as_str()).collect()
    }

    /// Look up a built-kernel build by name.
    pub fn kernel(&self, name: &str) -> Option<&crate::kernel::KernelBuild> {
        self.kernels.iter().find(|k| k.name == name)
    }

    /// The declared wasm cell names.
    pub fn wasm_names(&self) -> Vec<&str> {
        self.wasms.iter().map(|w| w.name.as_str()).collect()
    }

    /// Look up a wasm cell by name. `None` is the ORDINARY case for a
    /// `kind = "wasm"` variant — it means a plain cargo wasm build, not a defect.
    pub fn wasm(&self, name: &str) -> Option<&crate::wasm::WasmBuild> {
        self.wasms.iter().find(|w| w.name == name)
    }

    /// Look up a browser web app by name. `None` for every wasm variant that is
    /// not one.
    pub fn web_app(&self, name: &str) -> Option<&crate::web_app::WebAppBuild> {
        self.web_apps.iter().find(|w| w.name == name)
    }

    /// Validate a declared web app: its own shape, that the variant of the same
    /// name exists and is `kind = "wasm"`, and that no `[[build.wasm]]` cell
    /// claims the same variant (two post-build roads for one cargo build).
    pub fn validate_web_app(&self, w: &crate::web_app::WebAppBuild) -> Result<()> {
        w.validate()?;
        match self.get(&w.name) {
            Some(v) if v.kind == ArtifactKind::Wasm => {}
            Some(v) => {
                return Err(anyhow!(
                    "web app '{}': the variant of that name is kind '{}', not 'wasm' — a web \
                     app is built from a wasm32 cargo build and there is none",
                    w.name,
                    v.kind.as_str()
                ))
            }
            None => {
                return Err(anyhow!(
                    "web app '{}': no [[build.variant]] of that name (the web app is claimed by \
                     the wasm variant of the SAME NAME) — a web app nobody claims is never built",
                    w.name
                ))
            }
        }
        if self.wasm(&w.name).is_some() {
            return Err(anyhow!(
                "web app '{}': a [[build.wasm]] cell of the same name exists too — one cargo \
                 build, two post-build roads writing bundles; declare one of them",
                w.name
            ));
        }
        Ok(())
    }

    /// Look up a wasm cell by name, or a helpful error listing the known ones.
    pub fn require_wasm(&self, name: &str) -> Result<&crate::wasm::WasmBuild> {
        self.wasm(name).ok_or_else(|| {
            anyhow!(
                "no wasm cell '{}'; known wasm cells: {}",
                name,
                if self.wasms.is_empty() {
                    "<none declared>".to_string()
                } else {
                    self.wasm_names().join(", ")
                }
            )
        })
    }

    /// The declared package cell names.
    pub fn package_names(&self) -> Vec<&str> {
        self.packages.iter().map(|p| p.name.as_str()).collect()
    }

    /// Look up a package cell by name.
    pub fn package(&self, name: &str) -> Option<&crate::package::PackageBuild> {
        self.packages.iter().find(|p| p.name == name)
    }

    /// Look up a package cell by name, or a helpful error listing the known ones.
    /// Mirrors [`require_kernel`](Self::require_kernel) — the resolver `dispatch`
    /// calls once it knows the variant is `kind = "package"`.
    pub fn require_package(&self, name: &str) -> Result<&crate::package::PackageBuild> {
        self.package(name).ok_or_else(|| {
            anyhow!(
                "no package build '{}'; known packages: {}",
                name,
                if self.packages.is_empty() {
                    "<none declared>".to_string()
                } else {
                    self.package_names().join(", ")
                }
            )
        })
    }

    /// Look up a kernel build by name, or a helpful error listing the known ones.
    /// Mirrors [`require_iso`](Self::require_iso) — the resolver `dispatch` calls
    /// for a `kind = "kernel"` variant (the handle rides in the variant name).
    pub fn require_kernel(&self, name: &str) -> Result<&crate::kernel::KernelBuild> {
        self.kernel(name).ok_or_else(|| {
            anyhow!(
                "no kernel build '{}'; known kernels: {}",
                name,
                if self.kernels.is_empty() {
                    "<none declared>".to_string()
                } else {
                    self.kernel_names().join(", ")
                }
            )
        })
    }

    /// Validate a declared kernel build: the descriptor itself — a pinned
    /// tarball sha256 (an unpinned kernel is refused BY NAME), a known
    /// hardening profile, a wired arch/target and a repo-relative fragment.
    /// It chains no cargo variant ([`variant_deps`](crate::kernel::KernelBuild::variant_deps)
    /// is empty by construction), so there is no cross-reference half. Mirrors
    /// [`validate_iso`](Self::validate_iso).
    pub fn validate_kernel(&self, kernel: &crate::kernel::KernelBuild) -> Result<()> {
        kernel.validate()
    }

    /// Validate a declared wasm cell: the descriptor itself, and that a variant of
    /// the SAME NAME is declared and is `kind = "wasm"`.
    ///
    /// The name check is the one that matters. A cell nobody claims is a set of
    /// stages that never run — and because the cell is optional, nothing else would
    /// ever notice: no variant would fail, no build would refuse, and the bundle
    /// would simply not be built while the file says it is.
    pub fn validate_wasm_cell(&self, cell: &crate::wasm::WasmBuild) -> Result<()> {
        cell.validate()?;
        match self.get(&cell.name) {
            Some(v) if v.kind == ArtifactKind::Wasm => Ok(()),
            Some(v) => Err(anyhow!(
                "wasm cell '{}': the variant of that name is kind '{}', not 'wasm' — \
                 the cell's stages run after a wasm cargo build and there is none",
                cell.name,
                v.kind.as_str()
            )),
            None => Err(anyhow!(
                "wasm cell '{}': no [[build.variant]] of that name (the \
                 iso/installer/kernel/package rule — the cell is claimed by the \
                 variant of the SAME NAME). A cell nobody claims is a set of stages \
                 that never run, and nothing else would ever say so",
                cell.name
            )),
        }
    }

    /// Validate a declared package cell: the descriptor itself, and that every
    /// cargo variant it chains for its inputs (`from_variant`) names a declared
    /// variant. Mirrors [`validate_installer`](Self::validate_installer).
    pub fn validate_package(&self, pkg: &crate::package::PackageBuild) -> Result<()> {
        pkg.validate()?;
        for dep in &pkg.from_variant {
            if self.get(dep).is_none() {
                return Err(anyhow!(
                    "package '{}': from_variant '{dep}' names no declared variant — the inputs it stages would never be built",
                    pkg.name
                ));
            }
        }
        // ── THE WASM EDGE, AND IT IS CHECKED ────────────────────────────────
        // Naming a producer is worth nothing on its own: `from_variant` named one
        // too, and the whole 2026-09-06 defect was that it produced something
        // ELSE. So every input this package stages must be a file the named cell
        // SAYS it writes, and a mismatch is refused here rather than discovered by
        // a build that has already reported success.
        if let Some(w) = &pkg.from_wasm {
            let cell = self.wasm(w).ok_or_else(|| {
                anyhow!(
                    "package '{}': from_wasm '{w}' names no declared [[build.wasm]] cell; known wasm cells: {}",
                    pkg.name,
                    if self.wasms.is_empty() {
                        "<none declared>".to_string()
                    } else {
                        self.wasm_names().join(", ")
                    }
                )
            })?;
            let variant = self.get(&cell.name).ok_or_else(|| {
                anyhow!(
                    "package '{}': from_wasm '{w}' names a cell with no variant of that name",
                    pkg.name
                )
            })?;
            let produced = cell.outputs(variant);
            for row in &pkg.inputs {
                let (from, _) = pkg.input_pair(row)?;
                if !produced.contains(&from) {
                    return Err(anyhow!(
                        "package '{}': it stages `{from}`, and the wasm cell '{w}' it is chained to does not produce that file. '{w}' produces: {}. A chain that promises a product its producer does not make is the defect this edge exists to end — it would build cleanly and then refuse for a missing input, one step later, with a success in between",
                        pkg.name,
                        if produced.is_empty() { "<nothing>".to_string() } else { produced.join(", ") }
                    ));
                }
            }
        }
        Ok(())
    }

    /// Validate a declared disk-installer: the descriptor itself (name + volume_id +
    /// a plausible target disk + payload, and the mastered ISO), and that any cargo
    /// variant it chains for its PID-1 payload (`installer_variant` →
    /// [`variant_deps`](crate::installer::InstallerBuild::variant_deps)) names a
    /// declared variant. Mirrors [`validate_iso`](Self::validate_iso).
    pub fn validate_installer(&self, installer: &crate::installer::InstallerBuild) -> Result<()> {
        installer.validate()?;
        for dep in installer.variant_deps() {
            if self.get(dep).is_none() {
                return Err(anyhow!(
                    "installer '{}': chains cargo variant '{}' (its PID-1 payload) which is not \
                     declared (known variants: {})",
                    installer.name,
                    dep,
                    if self.variants.is_empty() {
                        "<none>".to_string()
                    } else {
                        self.names().join(", ")
                    }
                ));
            }
        }
        Ok(())
    }

    /// Validate a container build against this set: the descriptor must be
    /// well-formed AND every cargo variant it chains (`from_variant`) must exist.
    pub fn validate_container(&self, c: &crate::container::ContainerBuild) -> Result<()> {
        c.validate()?;
        for dep in c.variant_deps() {
            if self.get(dep).is_none() {
                return Err(anyhow!(
                    "container '{}': layer chains cargo variant '{}' which is not declared \
                     (known variants: {})",
                    c.name,
                    dep,
                    if self.variants.is_empty() {
                        "<none>".to_string()
                    } else {
                        self.names().join(", ")
                    }
                ));
            }
        }
        Ok(())
    }

    /// [`validate_container`](Self::validate_container) plus the **on-disk** check
    /// a `containerfile = "…"` variant needs: the real Containerfile and its build
    /// context exist under `repo_root`.
    ///
    /// Split from `validate_container` rather than folded into it because the
    /// shape rules must stay callable with no filesystem at all — a build-thing
    /// wasm PLUGIN validates a set it can only see as data. Any caller that HAS a
    /// repo root (the forge, `--list`, the image builder) should prefer this one:
    /// it is the difference between `--list` reporting a variant as buildable and
    /// discovering at build time that the Containerfile was never there.
    pub fn validate_container_in(
        &self,
        c: &crate::container::ContainerBuild,
        repo_root: &Path,
    ) -> Result<()> {
        self.validate_container(c)?;
        c.validate_in(repo_root)
    }

    /// Validate a declared VM boot: the descriptor itself, and that any
    /// `from_variant` it chains names a declared cargo variant (so "build it then
    /// boot it" can't reference a phantom target). Mirrors [`validate_container`].
    pub fn validate_vm(&self, vm: &crate::vm::VmBuild) -> Result<()> {
        vm.validate()?;
        for dep in vm.variant_deps() {
            if self.get(dep).is_none() {
                return Err(anyhow!(
                    "vm '{}': boots cargo variant '{}' which is not declared \
                     (known variants: {})",
                    vm.name,
                    dep,
                    if self.variants.is_empty() {
                        "<none>".to_string()
                    } else {
                        self.names().join(", ")
                    }
                ));
            }
        }
        Ok(())
    }

    /// Validate a declared bootable ISO: the descriptor itself, and that any
    /// `from_variant` it chains names a declared cargo variant (so "build it then
    /// master an ISO around it" can't reference a phantom target). Mirrors
    /// [`validate_vm`](Self::validate_vm).
    pub fn validate_iso(&self, iso: &crate::iso::IsoBuild) -> Result<()> {
        iso.validate()?;
        for dep in iso.variant_deps() {
            if self.get(dep).is_none() {
                return Err(anyhow!(
                    "iso '{}': masters cargo variant '{}' which is not declared \
                     (known variants: {})",
                    iso.name,
                    dep,
                    if self.variants.is_empty() {
                        "<none>".to_string()
                    } else {
                        self.names().join(", ")
                    }
                ));
            }
        }
        Ok(())
    }

    /// **Exactly one `default = true` per declared role**, or the reasons it is not
    /// so. An empty result means well-formed.
    ///
    /// The role axis is optional (a single-role repo may omit it), but the moment a
    /// repo declares roles it must also say which variant in each is the normal
    /// case. `Vec<String>` rather than `Result` because a declaration can be wrong
    /// in several ways at once and a reader wants all of them.
    ///
    /// Variants with no `role` are grouped under the pseudo-role `<unroled>` and
    /// held to the same rule, so a repo cannot escape the check by declining to
    /// name a role — the one escape this whole axis exists to close.
    ///
    /// **Grouped on `(suite, role)`, not on role alone.** A repo shipping two
    /// deliveries out of one crate graph (see [`BuildVariant::suites`]) has two
    /// server groups, each with its own normal case. Grouping by role alone accepted
    /// such a file and handed back the first-declared suite's default, leaving the
    /// other delivery silently defaultless — a green that could never go red. The
    /// grouping comes from [`groups`](Self::groups), the same writer the menu uses.
    ///
    /// A repo that declares no suite yields exactly the old role grouping and, to
    /// the byte, the old messages: the suite is named only when the repo actually
    /// declares one.
    pub fn check_defaults(&self) -> Vec<String> {
        let mut problems = Vec::new();
        let names_suites = self.declares_suites();
        for (suite, role, members) in self.groups() {
            let marked: Vec<&str> = members
                .iter()
                .filter(|v| v.is_default)
                .map(|v| v.name.as_str())
                .collect();
            // Only name the suite when the repo declares one — a single-delivery repo
            // must not start reading "suite '<the delivery>' role 'server'".
            let label = if names_suites {
                format!("suite '{suite}' role '{role}'")
            } else {
                format!("role '{role}'")
            };
            match marked.len() {
                1 => {}
                0 => problems.push(format!(
                    "{} has {} variant(s) and NO `default = true` — the caller has to \
                     guess which is the normal case (fat is normally the answer)",
                    label,
                    members.len()
                )),
                n => problems.push(format!(
                    "{label} marks {n} defaults ({}) — exactly one is the normal case",
                    marked.join(", ")
                )),
            }
        }
        problems
    }

    /// True when any variant declares a [`suite`](BuildVariant::suites) — i.e. this
    /// repo ships more than one delivery and the suite axis is LIVE.
    ///
    /// The way to tell [`check_suites`](Self::check_suites)'s "no problems" from its
    /// "not applicable": both are an empty `Vec`, and only this says which.
    pub fn declares_suites(&self) -> bool {
        self.variants
            .iter()
            .any(|v| v.suites.iter().any(|s| !s.trim().is_empty()))
    }

    /// The declared delivery names, in declaration order. Empty when the repo ships
    /// a single unnamed delivery.
    pub fn suites(&self) -> Vec<&str> {
        let mut out: Vec<&str> = Vec::new();
        for v in &self.variants {
            for s in v.suites.iter().map(|s| s.trim()).filter(|s| !s.is_empty()) {
                if !out.contains(&s) {
                    out.push(s);
                }
            }
        }
        out
    }

    /// The variants of one delivery, in declaration order. `suite` is a declared
    /// name or [`UNSUITED`]. A variant in N suites is returned by N of these calls.
    pub fn in_suite(&self, suite: &str) -> Vec<&BuildVariant> {
        self.variants
            .iter()
            .filter(|v| v.suite_keys().contains(&suite))
            .collect()
    }

    /// **The `(suite, role)` grouping, in declaration order** — the shared spine
    /// under [`check_defaults`](Self::check_defaults), the suite guards and the
    /// menu's [`RoleGroup`](crate::plugin::RoleGroup)s.
    ///
    /// ONE writer, so the checker and the thing it checks cannot disagree about what
    /// a group is. A variant declaring N suites appears in N groups — that is the
    /// difference between this and a scalar suite key, and it is the difference
    /// between catching a defaultless delivery and not.
    ///
    /// Order is first-appearance while scanning variants in declaration order, and
    /// within a variant, its suites in declaration order. A repo that declares no
    /// suite yields exactly the old role grouping, unchanged.
    pub fn groups(&self) -> Vec<(&str, &str, Vec<&BuildVariant>)> {
        let mut order: Vec<(&str, &str)> = Vec::new();
        for v in &self.variants {
            for k in v.group_keys() {
                if !order.contains(&k) {
                    order.push(k);
                }
            }
        }
        order
            .into_iter()
            .map(|(suite, role)| {
                let members: Vec<&BuildVariant> = self
                    .variants
                    .iter()
                    .filter(|v| v.group_keys().contains(&(suite, role)))
                    .collect();
                (suite, role, members)
            })
            .collect()
    }

    /// **The suite axis' own guard** — the reasons the delivery declaration is not
    /// sound. Empty result = well-formed.
    ///
    /// ⚠ **Empty is also what "not applicable" looks like.** A repo declaring fewer
    /// than two suites gets a VACUOUSLY empty answer: there is no second delivery to
    /// be a subset of anything. Ask [`declares_suites`](Self::declares_suites) to
    /// tell the two apart — "we checked and it is fine" and "there was nothing to
    /// check" must never be read as the same sentence.
    ///
    /// Three rules, each because a second delivery is otherwise expressible but not
    /// checkable:
    ///
    /// 1. **No blank names.** `suites = [""]` reads as a delivery whose name is
    ///    nothing; it groups as [`UNSUITED`] and would quietly join the unnamed
    ///    delivery.
    /// 2. **A named base.** As soon as two suites exist, `base_suite` must name one
    ///    of them. Without it rule 3 has no subject — and an unstated subject is how
    ///    a guard ends up passing on nothing.
    /// 3. **`features(other) ⊆ features(base)`.** A carved-out delivery may enable
    ///    fewer features than the whole product, never a feature the whole product
    ///    does not build. A feature only the MVP turns on is a build point nothing
    ///    else compiles — precisely the rot [`crate::feature_axis`] exists to catch,
    ///    reappearing one level up. A variant shipping in BOTH suites is part of the
    ///    base by definition and so can never violate this, which is right.
    ///
    /// ## What this deliberately does NOT claim
    ///
    /// It compares **declared feature sets**. It cannot see an *absence*.
    /// `holger-mvp`'s defining property is ZERO EGRESS, and no feature anywhere
    /// means "and definitely not `reqwest`" — a subset of features says nothing
    /// about which crates the resolver drags in. That needs a MEASURED guard over
    /// the resolved dependency tree; holger already has one
    /// (`holger/build-space/tests/auth_ship_small.rs`). Saying so here keeps this
    /// guard from being read as the stronger claim it is not.
    pub fn check_suites(&self) -> Vec<String> {
        let mut problems = Vec::new();
        for v in &self.variants {
            if v.suites.iter().any(|s| s.trim().is_empty()) {
                problems.push(format!(
                    "variant '{}' declares a BLANK suite name — a delivery with no name is \
                     indistinguishable from declaring none, and would silently join the \
                     repo's unnamed delivery",
                    v.name
                ));
            }
        }
        let suites = self.suites();
        if suites.len() < 2 {
            // 0 suites: the axis is not in use. 1 suite: nothing to be a subset of,
            // and declaring a base for it would assert a containment nobody can
            // violate. Either way this is NOT APPLICABLE, not "checked and clean".
            return problems;
        }
        if self.independent_suites {
            // Declared siblings: there is no base and the subset rule does not
            // apply. Refuse the contradiction rather than picking one, because a
            // `base_suite` left behind beside this flag still names a subject
            // nothing checks.
            if self.base_suite.as_deref().map(str::trim).is_some_and(|b| !b.is_empty()) {
                problems.push(format!(
                    "`independent_suites` says the {} deliveries ({}) are siblings, but \
                     `base_suite` still names one as the base they are carved OUT of — two \
                     answers to which is the base. Drop one.",
                    suites.len(),
                    suites.join(", ")
                ));
            }
            return problems;
        }
        let Some(base) = self
            .base_suite
            .as_deref()
            .map(str::trim)
            .filter(|b| !b.is_empty())
        else {
            problems.push(format!(
                "{} suites are declared ({}) but no `base_suite` names which one the others \
                 are carved OUT of — so `features(other) ⊆ features(base)` has no subject and \
                 nothing checks that a smaller delivery stays a subset of the product",
                suites.len(),
                suites.join(", ")
            ));
            return problems;
        };
        if !suites.contains(&base) {
            problems.push(format!(
                "`base_suite = \"{base}\"` names no declared suite (declared: {})",
                suites.join(", ")
            ));
            return problems;
        }
        // ⚠ THE SUBSET RULE LIVES IN `check_suites_resolved`, NOT HERE — moved
        // 2026-08-02, because comparing DECLARED feature strings is unsound and was
        // provably wrong on the reference repo. holger names one composite feature per
        // tier (`mvp-server` = mini + secure_update, `gui-mini` = gui-core +
        // about-card + mini), so no MVP variant could ever be a string subset of
        // ORIGINAL's declared names even though it IS a capability subset. This
        // function reported two confident false reds and blocked holger from declaring
        // the suites it had already settled. A subset rule needs the cargo
        // `[features]` closure, which needs the manifests, which this `&self`-only
        // method cannot read — so the check moved to where the input exists rather
        // than staying here in a shape that answered the wrong question.
        let _ = base;
        problems
    }

    /// **No two variants of one delivery may write the SAME file.** Empty result =
    /// well-formed.
    ///
    /// # The defect this exists to catch
    ///
    /// A `[[build.variant]]` is a *build point*, and a cargo build point lands at
    /// exactly one path: `target/<triple>/<profile>/<bin>`. When two variants of the
    /// same delivery declare the same `bin` at the same `profile` and `target`, they
    /// are not two artifacts — they are one file, written twice. Whichever built
    /// last is what is on disk, and NOTHING on disk says which one it is.
    ///
    /// Measured 2026-08-01, before this guard existed: korp declared bin `korp`
    /// three times (`korp`, `korp-thin`, `korp-spark41`), nornir declared bin
    /// `nornir` in ALL THREE of its variants, and dwarves declared bin `dwarves`
    /// three times. So "build the fat one and the thin one and compare" was not
    /// something those repos could do at all — a fat build and a thin build could
    /// not coexist on one machine, and a deployer holding the binary could not tell
    /// them apart. The declaration described a matrix the filesystem could not hold.
    ///
    /// holger solved this long before the axis was named: `holger-embedded` /
    /// `holger-mikro` / `holger-mvp` / `holger-full` are distinct `[[bin]]` names
    /// off ONE `main.rs`, precisely so the shapes can sit side by side.
    ///
    /// # The collision key, and why it is not just the bin name
    ///
    /// `(suite, target, profile, package, bin)`.
    ///
    /// * **suite** — two DELIVERIES may reuse a name on purpose. holger ships
    ///   `holger-server` in both MVP and ORIGINAL from different feature sets, and
    ///   that is correct: you build one delivery at a time and the binary reports
    ///   which one it came from in `--version`. Keying on the bin name alone would
    ///   red on the one repo that already had this right.
    /// * **profile** and **target** — `target/release/x` and `target/debug/x` are
    ///   different files, as are two cross-compile triples. dwarves' `dwarves-default`
    ///   variant exists precisely to build the `dev`-profile default surface of the
    ///   same bin, and it is not a collision.
    /// * **package** — belt and braces: two packages cannot emit the same bin name
    ///   into one target dir anyway (cargo rejects it), but keying on it keeps this
    ///   guard's notion of "same file" honest rather than approximate.
    ///
    /// Variants with no `bin` are skipped: they build the package's default bin (or
    /// a lib), which this cannot resolve without cargo metadata. That is a real
    /// limit and it is named here rather than papered over — a repo that leaves
    /// `bin` off two `Exe` variants of one package still has the defect and this
    /// guard will not see it. Declare the `bin`.
    pub fn check_bins(&self) -> Vec<String> {
        // (suite, target, profile, package, bin) -> the variants that claim it, in
        // declaration order.
        let mut order: Vec<(String, String)> = Vec::new();
        let mut claims: std::collections::BTreeMap<String, Vec<&str>> =
            std::collections::BTreeMap::new();
        for v in &self.variants {
            let Some(bin) = v.bin.as_deref().map(str::trim).filter(|b| !b.is_empty()) else {
                continue;
            };
            let target = v.target.as_deref().unwrap_or("<host>");
            let package = v.package.as_deref().unwrap_or("<root>");
            for suite in v.suite_keys() {
                let key = format!(
                    "{suite}\u{1}{target}\u{1}{}\u{1}{package}\u{1}{bin}",
                    v.profile.label()
                );
                let human = format!(
                    "bin '{bin}' (package {package}, profile {}, target {target})",
                    v.profile.label()
                );
                let entry = claims.entry(key.clone()).or_default();
                if entry.is_empty() {
                    order.push((key.clone(), human));
                }
                entry.push(v.name.as_str());
            }
        }
        let names_suites = self.declares_suites();
        let mut problems = Vec::new();
        for (key, human) in order {
            let who = &claims[&key];
            if who.len() < 2 {
                continue;
            }
            let suite = key.split('\u{1}').next().unwrap_or(UNSUITED);
            let where_ = if names_suites && suite != UNSUITED {
                format!("delivery '{suite}': ")
            } else {
                String::new()
            };
            problems.push(format!(
                "{where_}{} variants emit the SAME artifact — {human} — so the last build \
                 silently OVERWRITES the others and nothing on disk says which one you have: {}. \
                 Give each its own `bin` name off the same source (holger: holger-mikro / \
                 holger-full), or fold them into one variant.",
                who.len(),
                who.join(", ")
            ));
        }
        problems
    }

    /// **Every declared `bin` must be a `[[bin]]` cargo can actually build.** Empty
    /// result = well-formed.
    ///
    /// The sibling of [`check_bins`](Self::check_bins), and it caught a defect that
    /// had been live for months: korp renamed its GUI target `korp` → `korp-ui` in
    /// `Cargo.toml` and left three variants declaring `bin = "korp"`. Every one of
    /// them materialized `cargo build -p korp --bin korp …`, which cannot succeed —
    /// and korp's own `tests/build_variants.rs` asserted that exact argv STRING, so
    /// the test was green on an argv that could only ever fail. Nothing compared the
    /// declaration to the manifest, so the drift was invisible from both sides.
    ///
    /// `real` is the set of `[[bin]] name = …` targets the repo's manifests declare.
    /// It is passed IN rather than read here on purpose: this crate is dep-light and
    /// has no cargo-metadata, and the caller (a repo's own build-variants test) is
    /// the one that knows its workspace layout. What must NOT happen is each repo
    /// hand-rolling the comparison — that is the twin this method removes.
    pub fn check_bins_declared<S: AsRef<str>>(&self, real: &[S]) -> Vec<String> {
        let real: BTreeSet<&str> = real.iter().map(|s| s.as_ref()).collect();
        if real.is_empty() {
            return vec![
                "no `[[bin]]` targets were passed in — this check would pass VACUOUSLY \
                 on every variant, which is worse than not running it"
                    .to_string(),
            ];
        }
        let mut problems = Vec::new();
        for v in &self.variants {
            let Some(bin) = v.bin.as_deref().map(str::trim).filter(|b| !b.is_empty()) else {
                continue;
            };
            if !real.contains(bin) {
                problems.push(format!(
                    "variant '{}' declares bin '{}', which no `[[bin]]` target exists for — \
                     `cargo build --bin {}` cannot succeed. Declared targets: {}",
                    v.name,
                    bin,
                    bin,
                    real.iter().copied().collect::<Vec<_>>().join(", ")
                ));
            }
        }
        problems
    }

    /// **Every `[[bin]]` target the repo's manifests declare** — the input
    /// [`check_bins_declared`](Self::check_bins_declared) needs, read once, here.
    ///
    /// Without this every repo's build-variants test hand-rolls the same
    /// Cargo.toml walk, and four copies of a parser is the twin this crate exists
    /// to remove (LAW 5). Worse, the obvious shortcut — hardcoding the bin names in
    /// the test — recreates exactly the second declaration the guard is meant to
    /// catch: korp's test already hardcoded a variant roster and stayed green while
    /// the manifest renamed a bin out from under it.
    ///
    /// Walks `repo_root` for `Cargo.toml` files, skipping `target/`, `.git/`,
    /// `vendor/` and any `.claude/worktrees` — build output and vendored sources are
    /// not this repo's declaration. Collects, per manifest, every explicit
    /// `[[bin]] name = …` **and every bin cargo AUTO-DISCOVERS from the source
    /// layout**, which the manifest text never mentions.
    ///
    /// # Why it parses instead of asking cargo
    ///
    /// `cargo metadata --format-version 1` answers this exactly, and the
    /// `cargo_metadata` crate is the idiomatic way in. It is not used here for two
    /// reasons that both had to hold: the LAW forbids shelling out and
    /// `cargo_metadata` is a subprocess wrapper around the `cargo` binary; and the
    /// call sites are guards — `check_bins_declared`, `tally_bins`,
    /// `coverage_only_failures` — that run over a repo checkout in contexts where
    /// invoking cargo means a lock wait, a network resolve of the whole dependency
    /// graph, and a failure mode ("no cargo on PATH", "offline") that would turn a
    /// membership question into a build. So the rule is spelled out here instead,
    /// in full, once. **If a third half-correct special case is ever tempting, that
    /// is the signal to pay the cargo-metadata cost rather than guess again** — a
    /// partial rule is what made this function lie.
    ///
    /// # Cargo's documented rule, implemented whole
    ///
    /// Per the Cargo Book, *Target auto-discovery*:
    ///
    /// * `src/main.rs` ⇒ a bin named after the **package**;
    /// * `src/bin/<name>.rs` ⇒ a bin named `<name>`;
    /// * `src/bin/<name>/main.rs` ⇒ a bin named `<name>`;
    /// * an explicit `[[bin]]` **does not** switch discovery off — the two sets
    ///   union. A `[[bin]]` only suppresses the auto-discovered target that would
    ///   share its `name` or its `path` (this is how `[[bin]] name = "korp-ui"
    ///   path = "src/main.rs"` stops a package-named `korp` bin existing);
    /// * `autobins = false` in `[package]` is the one real opt-out;
    /// * edition 2015 (the default when `edition` is absent) turns discovery off as
    ///   soon as any `[[bin]]` is declared. Every manifest in this constellation is
    ///   2018+, but the rule is cheap and its absence is a silent over-report.
    ///
    /// # The defect this replaces
    ///
    /// The old body handled exactly one auto-discovery case — `src/main.rs`, and
    /// only when the manifest declared **no** `[[bin]]` at all — and was blind to
    /// `src/bin/` entirely. Measured 2026-08-25 by running both bodies over every
    /// sibling checkout, that hid SIX binaries in two repos: edda's
    /// `nornir-bench-redprobe` and `fleet_mock_server`, and facett's
    /// `errcode_registry_json`, `errcode_registry_md`, `osm2d_demo` and its
    /// package-named `facett-demo` bin. korp's `korp-probe` was the seventh until
    /// korp worked around it. The failure is one-sided in two ways, and both are
    /// bad: `check_bins_declared` prints "`cargo build --bin X` cannot succeed"
    /// about a bin cargo builds (korp: five of nine `tests/build_variants.rs`
    /// cases red on main since `8023cb1`), while `tally_bins` and
    /// `coverage_only_failures` just never count the target — a hole that goes
    /// unreported is a guard reporting success it did not measure.
    ///
    /// Returns names in sorted order with duplicates removed: this is a membership
    /// oracle, not a per-package map. Two packages cannot emit one bin name into one
    /// target dir anyway (cargo rejects it), so flattening loses nothing the caller
    /// can act on.
    pub fn cargo_bin_targets(repo_root: &Path) -> Result<Vec<String>> {
        /// Every bin one manifest yields: explicit `[[bin]]` names ∪ the targets
        /// cargo auto-discovers from `pkg_dir`'s source layout.
        fn bins_of_manifest(doc: &toml::Value, pkg_dir: &Path, out: &mut BTreeSet<String>) {
            // A virtual workspace manifest has no `[package]` and therefore no
            // targets of its own — its members are separate manifests this walk
            // visits in their own right.
            let Some(pkg) = doc.get("package") else {
                return;
            };
            let pkg_name = pkg.get("name").and_then(|n| n.as_str());

            // Explicit `[[bin]]`s, remembering the NAMES and PATHS they claim so an
            // auto-discovered target with the same identity is not counted twice.
            let mut claimed_names: BTreeSet<String> = BTreeSet::new();
            let mut claimed_paths: BTreeSet<String> = BTreeSet::new();
            let mut declared_any = false;
            if let Some(bins) = doc.get("bin").and_then(|b| b.as_array()) {
                for b in bins {
                    declared_any = true;
                    if let Some(n) = b.get("name").and_then(|n| n.as_str()) {
                        out.insert(n.to_string());
                        claimed_names.insert(n.to_string());
                    }
                    if let Some(path) = b.get("path").and_then(|p| p.as_str()) {
                        // Manifest paths are always `/`-separated and relative to
                        // the package dir; normalise a leading `./` so
                        // `./src/main.rs` and `src/main.rs` claim the same file.
                        claimed_paths
                            .insert(path.trim_start_matches("./").replace('\\', "/").to_string());
                    }
                }
            }

            // `autobins = false` — the explicit opt-out.
            if pkg.get("autobins").and_then(|a| a.as_bool()) == Some(false) {
                return;
            }
            // Edition 2015 (also the default when `edition` is absent) disables
            // auto-discovery once any target section is declared. `edition.workspace
            // = true` is not a string, and a workspace-inheriting manifest is 2018+
            // by construction, so an unreadable edition means "modern".
            let edition_2015 = match pkg.get("edition") {
                Some(toml::Value::String(e)) => e == "2015",
                Some(_) => false,
                None => true,
            };
            if edition_2015 && declared_any {
                return;
            }

            let discovered = |name: String, rel: &str, out: &mut BTreeSet<String>| {
                if !claimed_names.contains(&name) && !claimed_paths.contains(rel) {
                    out.insert(name);
                }
            };

            // src/main.rs => a bin named after the package.
            if let Some(pkg_name) = pkg_name {
                if pkg_dir.join("src").join("main.rs").is_file() {
                    discovered(pkg_name.to_string(), "src/main.rs", out);
                }
            }
            // src/bin/<name>.rs and src/bin/<name>/main.rs.
            let bin_dir = pkg_dir.join("src").join("bin");
            if let Ok(rd) = std::fs::read_dir(&bin_dir) {
                for e in rd.flatten() {
                    let p = e.path();
                    let file = e.file_name();
                    let file = file.to_string_lossy();
                    if p.is_dir() {
                        if p.join("main.rs").is_file() {
                            let rel = format!("src/bin/{file}/main.rs");
                            discovered(file.to_string(), &rel, out);
                        }
                    } else if let Some(stem) = file.strip_suffix(".rs") {
                        let rel = format!("src/bin/{file}");
                        discovered(stem.to_string(), &rel, out);
                    }
                }
            }
        }

        fn walk(dir: &Path, out: &mut BTreeSet<String>, depth: usize) -> Result<()> {
            if depth > 8 {
                return Ok(());
            }
            let rd = match std::fs::read_dir(dir) {
                Ok(rd) => rd,
                Err(_) => return Ok(()), // unreadable subtree is not a declaration
            };
            for e in rd.flatten() {
                let p = e.path();
                let name = e.file_name();
                let name = name.to_string_lossy();
                if p.is_dir() {
                    if matches!(name.as_ref(), "target" | ".git" | "vendor" | "node_modules")
                        || name.starts_with('.')
                    {
                        continue;
                    }
                    walk(&p, out, depth + 1)?;
                } else if name == "Cargo.toml" {
                    let text = std::fs::read_to_string(&p)
                        .with_context(|| format!("read {}", p.display()))?;
                    let doc: toml::Value = match toml::from_str(&text) {
                        Ok(d) => d,
                        // A malformed manifest is somebody else's error; skipping it
                        // silently would shrink the oracle, so say so and go on.
                        Err(e) => {
                            eprintln!("cargo_bin_targets: skipping {} ({e})", p.display());
                            continue;
                        }
                    };
                    if let Some(pkg_dir) = p.parent() {
                        bins_of_manifest(&doc, pkg_dir, out);
                    }
                }
            }
            Ok(())
        }
        let mut out = BTreeSet::new();
        walk(repo_root, &mut out, 0)?;
        Ok(out.into_iter().collect())
    }

    /// **Every package's `[features]` table** — the input
    /// [`check_suites_resolved`](Self::check_suites_resolved) needs, read once, here.
    ///
    /// Same walk and same exclusions as
    /// [`cargo_bin_targets`](Self::cargo_bin_targets) (one writer of the "what counts
    /// as this repo's manifests" rule), keyed by `[package] name` so a variant's
    /// `package` field selects its table.
    pub fn cargo_feature_tables(repo_root: &Path) -> Result<BTreeMap<String, FeatureTable>> {
        fn walk(dir: &Path, out: &mut BTreeMap<String, FeatureTable>, depth: usize) -> Result<()> {
            if depth > 8 {
                return Ok(());
            }
            let rd = match std::fs::read_dir(dir) {
                Ok(rd) => rd,
                Err(_) => return Ok(()),
            };
            for e in rd.flatten() {
                let p = e.path();
                let name = e.file_name();
                let name = name.to_string_lossy();
                if p.is_dir() {
                    if matches!(name.as_ref(), "target" | ".git" | "vendor" | "node_modules")
                        || name.starts_with('.')
                    {
                        continue;
                    }
                    walk(&p, out, depth + 1)?;
                } else if name == "Cargo.toml" {
                    let Ok(text) = std::fs::read_to_string(&p) else {
                        continue;
                    };
                    let Ok(doc) = toml::from_str::<toml::Value>(&text) else {
                        eprintln!("cargo_feature_tables: skipping {}", p.display());
                        continue;
                    };
                    let Some(pkg) = doc
                        .get("package")
                        .and_then(|p| p.get("name"))
                        .and_then(|n| n.as_str())
                    else {
                        continue;
                    };
                    let mut features = BTreeMap::new();
                    let mut default = Vec::new();
                    if let Some(tbl) = doc.get("features").and_then(|f| f.as_table()) {
                        for (k, v) in tbl {
                            let deps: Vec<String> = v
                                .as_array()
                                .map(|a| {
                                    a.iter()
                                        .filter_map(|x| x.as_str().map(str::to_string))
                                        .collect()
                                })
                                .unwrap_or_default();
                            if k == "default" {
                                default = deps.clone();
                            }
                            features.insert(k.clone(), deps);
                        }
                    }
                    out.insert(pkg.to_string(), FeatureTable { features, default });
                }
            }
            Ok(())
        }
        let mut out = BTreeMap::new();
        walk(repo_root, &mut out, 0)?;
        Ok(out)
    }

    /// **`features(carved-out suite) ⊆ features(base suite)`, over RESOLVED cargo
    /// features.** Empty result = well-formed.
    ///
    /// # Why this exists and [`check_suites`](Self::check_suites) could not do it
    ///
    /// `check_suites` compares the feature strings a variant DECLARES. That is
    /// unsound the moment a repo names one composite feature per tier, and it made
    /// the subset rule unsatisfiable for the exact repo the suite model was designed
    /// from. Measured 2026-08-02, holger's first declaration of its two settled
    /// suites:
    ///
    /// ```text
    /// suite 'MVP' variant 'mvp-server' enables feature 'mvp-server', which the base
    ///   suite 'ORIGINAL' never builds (base builds: embedded, full, gui, …)
    /// suite 'MVP' variant 'ui-mini' enables feature 'gui-mini', which the base suite
    ///   'ORIGINAL' never builds
    /// ```
    ///
    /// Both reds are FALSE. `mvp-server = ["mini", "secure_update"]` and ORIGINAL's
    /// `server-local` inherits the crate default `["mini", "online", "bench",
    /// "secure_update"]`, so MVP's server is a capability subset of ORIGINAL's — the
    /// two deliveries simply spell their tiers with different composite NAMES, which
    /// is what a composite is for. holger's suites went undeclared for a day because
    /// declaring them correctly reddened a guard that was comparing the wrong thing.
    ///
    /// So: expand each variant's selection through its package's `[features]` table
    /// (transitively, plus the crate `default` set when `no_default_features` is
    /// false) and compare the CLOSURES. `dep:x` and `crate/feat` tokens are kept
    /// opaque — they never expand, and they compare across suites exactly as well as
    /// a plain name does.
    ///
    /// A variant naming no `package` cannot be resolved, and that is REPORTED rather
    /// than skipped: silently dropping the unresolvable case is how a subset rule
    /// goes vacuous.
    pub fn check_suites_resolved(&self, tables: &BTreeMap<String, FeatureTable>) -> Vec<String> {
        let mut problems = Vec::new();
        let suites = self.suites();
        if suites.len() < 2 {
            return problems;
        }
        if self.independent_suites {
            // Declared siblings — no containment is claimed, so there is nothing
            // here to violate. `check_suites` has already refused a `base_suite`
            // left beside the flag.
            return problems;
        }
        let Some(base) = self
            .base_suite
            .as_deref()
            .map(str::trim)
            .filter(|b| !b.is_empty())
        else {
            // `check_suites` already reports the missing base; do not say it twice.
            return problems;
        };
        let resolve = |v: &BuildVariant| -> Option<BTreeSet<String>> {
            let t = tables.get(v.package.as_deref()?)?;
            Some(t.leaf_closure(&v.features, !v.no_default_features))
        };
        // A DELEGATING variant compiles nothing of its own: a tagged pointer of
        // kind container / vm / iso / installer carries no cargo argv (no
        // `package`, no `features`), so it HAS no feature closure — its payload
        // cell chains `from_variant` / `variant` rows that are themselves
        // declared and pass through this very check under their own suites.
        // Skipping these is not a silent cap: every cargo-shaped kind (exe,
        // service, lib, wasm) with an unreadable package is still reported.
        // Without the skip, tagging a payload cell's pointer variant into a
        // carved-out suite (holger-mvp-appliance / holger-installer, 2026-08-17)
        // reads as "cannot check" — a red about a closure that does not exist.
        let delegates = |v: &BuildVariant| {
            matches!(
                v.kind,
                ArtifactKind::Container
                    | ArtifactKind::Vm
                    | ArtifactKind::Iso
                    | ArtifactKind::Installer
                    | ArtifactKind::Kernel
            )
        };
        let mut base_closure: BTreeSet<String> = BTreeSet::new();
        for v in self.in_suite(base) {
            if delegates(v) {
                continue;
            }
            match resolve(v) {
                Some(c) => base_closure.extend(c),
                None => problems.push(format!(
                    "base suite '{base}' variant '{}' names package {:?}, which has no \
                     readable [features] table — the base closure is INCOMPLETE, so a \
                     subset check against it would pass things it should not",
                    v.name, v.package
                )),
            }
        }
        for suite in suites {
            if suite == base {
                continue;
            }
            for v in self.in_suite(suite) {
                if delegates(v) {
                    continue;
                }
                let Some(c) = resolve(v) else {
                    problems.push(format!(
                        "suite '{suite}' variant '{}' names package {:?}, which has no \
                         readable [features] table — cannot check it is a subset of '{base}'",
                        v.name, v.package
                    ));
                    continue;
                };
                let missing: Vec<&str> = c
                    .iter()
                    .filter(|f| !base_closure.contains(*f))
                    .map(|s| s.as_str())
                    .collect();
                if !missing.is_empty() {
                    problems.push(format!(
                        "suite '{suite}' variant '{}' resolves to feature(s) {} that the base \
                         suite '{base}' never builds — a carved-out delivery must be a SUBSET \
                         of the product",
                        v.name,
                        missing.join(", ")
                    ));
                }
            }
        }
        problems
    }

    /// The variant marked `default = true` in `role` (`None` for the unroled group).
    ///
    /// Ambiguous once a repo declares more than one delivery — two suites can each
    /// hold a default server and this returns whichever was declared first. Use
    /// [`default_in`](Self::default_in) there; this stays for single-delivery repos,
    /// which is every caller that predates the suite axis.
    pub fn default_for_role(&self, role: Option<crate::plugin::Role>) -> Option<&BuildVariant> {
        self.variants
            .iter()
            .find(|v| v.is_default && v.role == role)
    }

    /// The variant marked `default = true` in one `(suite, role)` group — the
    /// suite-aware resolver. `suite` is a declared name or [`UNSUITED`].
    pub fn default_in(
        &self,
        suite: &str,
        role: Option<crate::plugin::Role>,
    ) -> Option<&BuildVariant> {
        self.variants
            .iter()
            .find(|v| v.is_default && v.role == role && v.suite_keys().contains(&suite))
    }

    /// Validate every declared variant, container, vm, iso AND installer — a
    /// config-time sanity gate.
    pub fn validate_all(&self) -> Result<()> {
        for v in &self.variants {
            self.validate(v)?;
        }
        for c in &self.containers {
            self.validate_container(c)?;
        }
        for vm in &self.vms {
            self.validate_vm(vm)?;
        }
        for iso in &self.isos {
            self.validate_iso(iso)?;
        }
        for installer in &self.installers {
            self.validate_installer(installer)?;
        }
        for kernel in &self.kernels {
            self.validate_kernel(kernel)?;
        }
        for pkg in &self.packages {
            self.validate_package(pkg)?;
        }
        for w in &self.wasms {
            self.validate_wasm_cell(w)?;
        }
        for w in &self.web_apps {
            self.validate_web_app(w)?;
        }
        Ok(())
    }

    /// [`validate_all`](Self::validate_all) against a real checkout: every shape
    /// rule, plus the on-disk existence of every container variant's declared
    /// `containerfile` + `context` ([`validate_container_in`](Self::validate_container_in)).
    ///
    /// The one to call from anything that enumerates buildable targets for a
    /// human, so `--list` cannot advertise a variant whose Containerfile is missing.
    pub fn validate_all_in(&self, repo_root: &Path) -> Result<()> {
        self.validate_all()?;
        for c in &self.containers {
            c.validate_in(repo_root)?;
        }
        // A kernel cell's declared `kconfig_fragment` must be in this checkout —
        // the same "refuse before `--list` advertises it" rule as a Containerfile.
        for k in &self.kernels {
            k.validate_in(repo_root)?;
        }
        // Every consumption edge is TYPE-CHECKED against the donor beside this
        // checkout — the constellation layout rule every cross-repo seam uses.
        self.validate_edges_in(repo_root)
    }

    // ── consumption edges ────────────────────────────────────────────────────

    /// **Every edge a declared variant carries**, as whole
    /// [`ConsumptionEdge`](crate::tree::ConsumptionEdge)s in root order — the
    /// `kernel_donor` row as the `kernel` edge first, then
    /// `[…edges.<feature>]`. Empty for a variant that consumes nothing, `None`
    /// for an undeclared name. One walk for every kind, so the CLI, the pane
    /// and the type check read the same list.
    ///
    /// This is the general shape; [`edges_of`](Self::edges_of) is the same list
    /// as `(feature, row)` pairs for the readers written before the feature sat
    /// on the value.
    pub fn consumption_edges(&self, variant: &str) -> Option<Vec<crate::tree::ConsumptionEdge>> {
        self.edges_decls_of(variant)
    }

    /// [`consumption_edges`](Self::consumption_edges) as `(feature, row)` pairs
    /// — the shape the forge's `buildthing` listing and every other 0.1.x
    /// reader already destructures.

The file goes on; the console shows all of it.