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

nordisk/gunnar

crates/gunnar-recipe/src/variants.rs

//! **The variants — every build point gunnar declares, as Rust.**
//!
//! This is `.nornir/build-variants.toml`'s `[[build.variant]]` list, moved in and
//! typed. Nothing was dropped in the move: `tests/toml_parity.rs` reads the toml
//! that is still on disk and asserts it says exactly what this table says, field
//! for field, in both directions.
//!
//! # What being code buys, concretely
//!
//! ```text
//! toml    an atom/column pair nobody allows      fails when the build runs
//! rust    an atom/column pair nobody allows      fails when the crate compiles
//! ```
//!
//! The second is [`EVERY_VARIANT_IS_LEGAL`] below — a `const` assertion over
//! [`VARIANTS`]. Adding
//!
//! ```ignore
//! Variant { atom: Atom::GunnarCli, column: Column::IsoLive, .. }
//! ```
//!
//! does not build. That is the whole argument for dissolving the toml, and it is
//! why the argument is made with a `const fn` rather than a unit test: a test can
//! be skipped, and an `#[ignore]` on it is one line.

use std::collections::{BTreeMap, BTreeSet};

use nornir_build_thing::contract::PaneKind;
use nornir_build_thing::variant::{ArtifactKind, BuildVariant, Profile, ServiceShape};

use crate::atom::{Atom, Column, Suite};

/// One declared build point: an atom, delivered in a column, with every setting
/// the build resolves to.
///
/// A `(atom, column)` pair may appear more than once — `gunnar-server` has two
/// `exe` variants, the plain binary and the systemd service — because the pair does
/// not determine the build on its own. What distinguishes them is
/// [`Variant::resolved_settings`], which is also what the receipt records, so the
/// receipt cannot disagree with the build point it came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Variant {
    /// The build point's stable handle. Identical to the `name` the toml used,
    /// because `xtask`, `installtest` and the bench harness all name these
    /// strings, and a rename would have been a migration hiding inside a
    /// migration.
    pub name: &'static str,
    /// The row.
    pub atom: Atom,
    /// The column.
    pub column: Column,
    /// The deliveries this variant ships in.
    pub suites: &'static [Suite],
    /// The artifact kind. May narrow the column's own kind — see
    /// [`Column::admits`].
    pub kind: ArtifactKind,
    /// Fat, thin, mixed, or inapplicable.
    pub linkage: ServiceShape,
    /// The normal case for this variant's `(suite, role)` group. Exactly one per
    /// group; `BuildVariantSet::check_defaults` holds the built set to it.
    pub is_default: bool,
    /// The suite member this binary IS. `None` for a variant that composes other
    /// variants' artifacts rather than compiling one of its own.
    pub member: Option<&'static str>,
    /// `cargo -p`.
    pub package: Option<&'static str>,
    /// `cargo --bin`.
    pub bin: Option<&'static str>,
    /// `cargo --features`.
    pub features: &'static [&'static str],
    /// `cargo --no-default-features`.
    pub no_default_features: bool,
    /// `cargo --profile`. `""` means the cargo default (`dev`), which is what a
    /// variant that runs no cargo carries.
    pub profile: &'static str,
    /// `cargo --target`.
    pub target: Option<&'static str>,
    /// The `[[build.container]]` this variant delegates to, for `kind = container`.
    pub container_ref: Option<&'static str>,
    /// **The row's own switchable axis** (`BuildVariant::open`): features that
    /// are OFF in this row and that an installer OPTION defaulting off may ADD
    /// (`nornir_build_thing::options::OptionSpec::validate_against_variant`).
    /// Only `dev-narrate` today, on the four payloads of the estate's media —
    /// and the ON build is its own named row (`…-narrate`), so its receipt can
    /// say it is a DEV build.
    pub open: &'static [&'static str],
    /// **Built from another workspace** (`BuildVariant::manifest_dir`, `--manifest-path
    /// <dir>/Cargo.toml`). The four `gunnar-front` rows are, since 2026-10-22: the front
    /// is a DETACHED workspace at `crates/gunnar-front`, because it needs crates only the
    /// sibling checkouts have and a lone clone must load the root workspace. (The
    /// metered front built from monetize-impl's commercial crate, 2026-10-08, is LEGACY
    /// under ONE monetize, owner 13:35Z 2026-10-10.) `None`: the root workspace.
    pub manifest_dir: Option<&'static str>,
    /// The checkouts such a row's `cargo metadata` loads (`BuildVariant::siblings`); empty
    /// for a row of this workspace (the toolchain's own siblings).
    pub siblings: &'static [&'static str],
    /// **The forge signs what this row builds** (`BuildVariant::sign`,
    /// `sign = { purpose = "plugin" }`): `<artifact>.p7s` + `<artifact>.blake3`
    /// beside it, under the plugin-signing certificate issued by the owner's root
    /// key. Only the CLI a customer DOWNLOADS from an estate's site (`gunnar-musl`,
    /// E4 2026-10-11) today.
    pub sign: bool,
}

/// Where the console's crate is, from this repository's root: the gunnar-ui checkout beside
/// it (the row `gunnar-ui` builds `--manifest-path ../gunnar-ui/crates/gunnar-ui/Cargo.toml`).
pub const UI_MANIFEST_DIR: &str = "../gunnar-ui/crates/gunnar-ui";

/// **Every build point gunnar declares.**
///
/// Migrated verbatim from `.nornir/build-variants.toml`. The order is the toml's
/// order, so a diff between the two is readable.
pub const VARIANTS: &[Variant] = &[
    // ══ the products (lane GUN-PRODUCTS, owner 2026-10-30) ═════════════════════
    //
    // Gunnar's three products, one crate each, and each row IS its `cargo install` line
    // (`twins::TWINS`): the crate's defaults, the host's target, the crate's one program.
    // The rows below them are the estate's builds: the appliance, the metered appliance,
    // the front and the media. The MVP suite is exactly these (`cargo xtask build --suite
    // MVP --list`); the retired rows (`gunnar` the client-only git command, the old
    // `gunnar-cli` row, `gunnar-musl`, `gunnar-solo`, `gunnar-service`, `gunnar-open`,
    // `gunnar-open-client`) are in git, and README.md names what each became.
    //
    // gunnar — `cargo install gunnar`: the server (`crates/gunnar`), its AIRGAP default
    // (znippy, SSH and smart HTTP, replication for a twin; no monetize, no outbound client).
    // The (MVP, server) normal case.
    Variant {
        name: "gunnar",
        atom: Atom::GunnarServer,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: true,
        member: Some("gunnar"),
        package: Some("gunnar"),
        bin: Some("gunnar"),
        // The crate's defaults, named out loud (`crates/gunnar/Cargo.toml` `default`), so an
        // edit of the manifest's default shows up as a twin difference instead of moving
        // the row silently; `cargo install gunnar` builds exactly this (`twins::TWINS`).
        features: &["engine-znippy", "http", "replication"],
        no_default_features: true,
        profile: "release",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // gunnar-ui — `cargo install gunnar-ui`: the web console, built from the gunnar-ui
    // checkout beside this one (`crates/gunnar-ui`, a thin bin over gunnar-ui-server with
    // the console bundle built in). A client of gunnar: THIN, it reaches the server's
    // control plane and nothing else.
    Variant {
        name: "gunnar-ui",
        atom: Atom::GunnarUi,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::ThinStandalone,
        is_default: false,
        member: None,
        package: Some("gunnar-ui"),
        bin: Some("gunnar-ui"),
        features: &[],
        no_default_features: false,
        profile: "release",
        target: None,
        container_ref: None,
        manifest_dir: Some(UI_MANIFEST_DIR),
        siblings: &["gunnar-ui"],
        open: &[],
        sign: false,
    },
    // gunnar-cli — `cargo install gunnar-cli`: the command-line tool (`crates/gunnar-cli`),
    // git with the encrypted repositories and the control plane, in one program. THIN: it
    // reaches the server at `--at` and the remote a person names. The (MVP, client) normal
    // case.
    Variant {
        name: "gunnar-cli",
        atom: Atom::GunnarCli,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::ThinStandalone,
        is_default: true,
        member: Some("gunnar"),
        package: Some("gunnar-cli"),
        bin: Some("gunnar-cli"),
        // The crate's defaults, named out loud, as on the row `gunnar`.
        features: &["client", "control"],
        no_default_features: true,
        profile: "release",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ══ the estate's builds ═══════════════════════════════════════════════════
    //
    // ── the service, and the first thing anybody should be able to build ─────
    //
    // musl, because the server is meant to be able to be PID 1 with no operating
    // system under it, and a glibc binary needs one.
    Variant {
        name: "gunnar-server",
        atom: Atom::GunnarServer,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        // The (MVP, server) normal case is the product `gunnar` since GUN-PRODUCTS.
        is_default: false,
        member: Some("gunnar"),
        // `gunnar-cli` until 2026-08-10: the appliance bin moved to its own
        // crate and the variant follows the code. The `server`
        // feature on that crate is a marker with no dependencies — it exists so
        // this variant keeps a non-empty, pairwise-distinct feature set, which is
        // what `by_resolved_features` attributes on.
        package: Some("gunnar-server"),
        bin: Some("gunnar-server"),
        // ── the appliance's diet, NAMED ──────────────────────────────────────
        //
        // `["server"]` until 2026-08-10. `gunnar-znippy` collapsed into
        // `gunnar-server` as `engine-znippy` / `engine-gix`
        // (`locked_on_target.md`, the `gunnar-znippy` collapse), and `no_default_features` below means
        // that crate's `default = ["engine-znippy"]` does not reach this variant.
        // Left unnamed, this atom would build a server with no storage engine
        // linked — the engine-free binary the collapse exists to abolish.
        //
        // `engine-gix` is deliberately absent: this is the musl/scratch image
        // meant to be PID 1 with no operating system under it. Naming the
        // engine here rather than inheriting it means a later edit to the
        // manifest's `default` cannot put gix in the appliance by accident.
        //
        // `mimalloc` is named here for exactly the reason the paragraph above
        // gives about `engine-znippy`: `no_default_features` means the
        // manifest's `default = [..., "mimalloc"]` does NOT reach this variant, and
        // this variant is the one that matters. It is the musl/scratch image that
        // actually serves clones, and musl's own `malloc` is materially worse
        // under contention than the glibc one that already cost 2.2x on clone
        // CPU (`gunnar-server/src/lib.rs` has the grid). Inheriting would have
        // left the appliance — and only the appliance — on the slow allocator
        // while every bench column measured the fast one.
        //
        // It builds here: `libmimalloc-sys` is `cc` plus vendored C with no
        // cmake and no download, `musl-gcc` compiles it, and the result is
        // still `static-pie` with nothing under it. VERIFIED 2026-08-11, not
        // assumed — `MIMALLOC_VERBOSE=1` on the musl artifact prints the
        // allocator's own banner, and a build without the feature prints
        // nothing.
        //
        // The resolved set this produces is `{SERVER, ENGINE_ZNIPPY, MIMALLOC,
        // DHCP_CLIENT, WG_CLIENT}` — every one of those features is spelled
        // `dep:`, which creates no implicit feature, so nothing else joins the
        // closure and `by_resolved_features` still matches by equality.
        //
        // `dhcp-client` and `wg-client` added 2026-08-22. They sit in the
        // crate's `default` list AND here, because `no_default_features` below
        // means the default cannot reach this row: unspelled, the packed
        // appliance would ship with no DHCP client and no tunnel. They are also
        // the two features `[[build.installer.options]]` decides, and an option
        // that defaults ON is REFUSED unless its feature is in this list.
        //
        // ⚠ THIS PARAGRAPH USED TO END "an OFF build resolves a set no variant
        // declares, so it gets `Attribution::NoVariant` and no receipt … teaching
        // the receipt to record applied options is upstream work in
        // `nornir-build-thing`, not something a declaration here can fix." The
        // first half was MEASURED true on 2026-08-27 (the live ISO with
        // `--opt wireguard=off` printed exactly that warning, naming the resolved
        // set `{DHCP_CLIENT, ENGINE_ZNIPPY, MIMALLOC, SERVER}`); the last clause
        // was wrong. A declaration here fixes it, because the thing the receipt
        // could not name was a build point nobody had NAMED — see
        // `gunnar-server-thin` below. The upstream work (recording the applied
        // option on the receipt of THIS row) is a different, still-open thing.
        //
        // `replication` added 2026-08-30 — decided, after
        // a drill measured replication working end to
        // end (60 ms lag; after `SIGKILL` of the primary the standby served 5/5
        // repositories `git fsck` clean) and NOT compiled into this row. It is
        // named here and nowhere else for the same reason `mimalloc` is: it is
        // not in the crate's `default` list, so `no_default_features` below
        // means an unspelled `replication` ships an appliance that cannot keep a
        // standby. It costs +4 packages (365 -> 369, MEASURED 2026-08-30), not
        // the +128 the old all-or-nothing `client` feature would have.
        features: &[
            "server",
            "engine-znippy",
            "mimalloc",
            "clock-fetch",
            "resolver-probe",
            "fleet-pull",
            "mirror-http",
            "sso-egress",
            "dhcp-client",
            "wg-client",
            "replication",
            "grow-runtime",
            "live-app",
        ],
        no_default_features: true,
        profile: "release",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &["dev-narrate"],
        sign: false,
    },
    // ── `gunnar-server` + DEV-NARRATE (2026-09-30) ──────────────────────────────
    //
    // The build point the installer's `dev-narrate` option ARRIVES at when it is
    // ON (`--opt dev-narrate=on`, the kit's dev medium only): the same program
    // plus Skidbladnir nordisk-narrate's sender — the box's boot log sealed to
    // the installer's key over udp/51822 before the installer's link exists. Its
    // own row so the ON binary's receipt NAMES it a dev build; `gunnar-server` keeps
    // `dev-narrate` in `open` (off), and the option forbids `nordisk_narrate`
    // in every OFF (release) build.
    Variant {
        name: "gunnar-server-narrate",
        atom: Atom::GunnarServer,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-server"),
        bin: Some("gunnar-server"),
        features: &[
            "server",
            "engine-znippy",
            "mimalloc",
            "clock-fetch",
            "resolver-probe",
            "fleet-pull",
            "mirror-http",
            "sso-egress",
            "dhcp-client",
            "wg-client",
            "replication",
            "grow-runtime",
            "live-app",
            "dev-narrate",
        ],
        no_default_features: true,
        profile: "dev-media",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the same appliance without the tunnel ────────────────────────────────
    //
    // The row above minus `wg-client`. It is the build point
    // `[[build.iso.options]] wireguard = off` ARRIVES at, and it exists because
    // gunnar's own receipt gate asked for it out loud: without this row that build
    // resolved `{DHCP_CLIENT, ENGINE_ZNIPPY, MIMALLOC, SERVER}`,
    // `by_resolved_features` found nothing, and the shipped binary could not say
    // which build point it was. The option axis was real either way — the symbol
    // read-back found `wg_appliance_core` and `boringtun` gone, 23 247 992 →
    // 22 837 368 B — but a feature set nobody declared is not a product.
    //
    // Same atom as the row above ON PURPOSE: this is not a second program, it is
    // the same appliance with one capability subtracted, and `Atom` is what the
    // program IS. `by_resolved_features` matches on the feature set, which is
    // pairwise distinct, so the two rows stay tellable apart where it matters.
    //
    // `is_default: false`: the default appliance still carries the tunnel (an
    // opt-out ships unless somebody opts out).
    Variant {
        name: "gunnar-server-thin",
        atom: Atom::GunnarServer,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-server"),
        bin: Some("gunnar-server"),
        features: &[
            "server",
            "engine-znippy",
            "mimalloc",
            "clock-fetch",
            "resolver-probe",
            "fleet-pull",
            "mirror-http",
            "sso-egress",
            "dhcp-client",
            "replication",
            "grow-runtime",
        ],
        no_default_features: true,
        profile: "release",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the METERED appliance: the same server, plus what a provider pays for ─
    //
    // `gunnar-server` + `metered`, a licensed cargo feature. Not a second
    // program — same atom, same engine, same diet — but the one row that
    // CONTAINS plan enforcement: the `Entitlement` service, the plan arm in
    // `authorise_service`, the entitlement document, and `monetize-embed`.
    // The community row above has none of that in its symbol table, and
    // `gunnar-build-thing/tests/metered_symbols.rs` reads both binaries to
    // say so; `gunnar-cli/tests/feature_split.rs` says the same of the graph.
    //
    // `is_default: false`, and deliberately: the default appliance is the
    // community one. A service provider builds this row by name.
    Variant {
        name: "gunnar-server-metered",
        atom: Atom::GunnarServer,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-server"),
        bin: Some("gunnar-server"),
        features: &[
            "server",
            "engine-znippy",
            "mimalloc",
            "clock-fetch",
            "resolver-probe",
            "fleet-pull",
            "mirror-http",
            "sso-egress",
            "dhcp-client",
            "wg-client",
            "replication",
            "metered",
            "grow-runtime",
            "live-app",
            // The usage share on the admission gate (2026-09-28, owner: yes).
            // Every metered server row carries it; the budget is a runtime
            // setting (`--admit-limit`, `none` = PassThrough), so an estate
            // that sets none runs today's gate on this binary.
            "admit-share",
        ],
        no_default_features: true,
        profile: "release",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &["dev-narrate"],
        sign: false,
    },
    // ── `gunnar-server-metered` + DEV-NARRATE (2026-09-30) ──────────────────────────────
    //
    // The build point the installer's `dev-narrate` option ARRIVES at when it is
    // ON (`--opt dev-narrate=on`, the kit's dev medium only): the same program
    // plus Skidbladnir nordisk-narrate's sender — the box's boot log sealed to
    // the installer's key over udp/51822 before the installer's link exists. Its
    // own row so the ON binary's receipt NAMES it a dev build; `gunnar-server-metered` keeps
    // `dev-narrate` in `open` (off), and the option forbids `nordisk_narrate`
    // in every OFF (release) build.
    Variant {
        name: "gunnar-server-metered-narrate",
        atom: Atom::GunnarServer,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-server"),
        bin: Some("gunnar-server"),
        features: &[
            "server",
            "engine-znippy",
            "mimalloc",
            "clock-fetch",
            "resolver-probe",
            "fleet-pull",
            "mirror-http",
            "sso-egress",
            "dhcp-client",
            "wg-client",
            "replication",
            "metered",
            "grow-runtime",
            "live-app",
            "admit-share",
            "dev-narrate",
        ],
        no_default_features: true,
        profile: "dev-media",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // `gunnar-server-thin` + `metered`: the metered appliance without the
    // tunnel, for the same `wireguard = off` option the thin row exists for.
    Variant {
        name: "gunnar-server-metered-thin",
        atom: Atom::GunnarServer,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-server"),
        bin: Some("gunnar-server"),
        features: &[
            "server",
            "engine-znippy",
            "mimalloc",
            "clock-fetch",
            "resolver-probe",
            "fleet-pull",
            "mirror-http",
            "sso-egress",
            "dhcp-client",
            "replication",
            "metered",
            "grow-runtime",
            // The usage share on the admission gate (2026-09-28, owner: yes).
            // Every metered server row carries it; the budget is a runtime
            // setting (`--admit-limit`, `none` = PassThrough), so an estate
            // that sets none runs today's gate on this binary.
            "admit-share",
        ],
        no_default_features: true,
        profile: "release",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the command-line tool, built FOR THE FRONT ───────────────────────────
    //
    // `gunnar-cli` as the front carries it and as every Gunnar site serves it under
    // `/download/` (E4, owner 2026-10-11): the SAME program as the product row
    // `gunnar-cli` (`client,control`), static musl because it lands on hosts nobody here
    // pins, and SIGNED: the forge's sign step writes `gunnar-cli.p7s` (CMS over the BLAKE3
    // digest, plugin-signing certificate under the owner's root) and `gunnar-cli.blake3`,
    // and the download page shows how to check both against the published root
    // (`xtask/src/cli_stage.rs` stages them for gunnar-front's build.rs). Until GUN-PRODUCTS
    // (2026-10-30) the download was the client-only `gunnar` bin (row `gunnar-musl`), whose
    // name was also the server's.
    //
    // Its own row rather than an option on the product row: the target is a setting that
    // changes the artifact and its path (`super::by_build_point`).
    Variant {
        name: "gunnar-cli-musl",
        atom: Atom::GunnarCli,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::ThinStandalone,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-cli"),
        bin: Some("gunnar-cli"),
        features: &["client", "control"],
        no_default_features: true,
        profile: "release",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: true,
    },
    // ── the front's control client on a METERED estate ───────────────────────
    //
    // **The row that ends "the estate could see the drift and had no door".**
    //
    // `gunnar control appliance-identity` prints the key this appliance signs
    // actor tickets with — the string a monetize needs in `[products.<id>]
    // appliance_key` before a tenant can renew. It is `#[cfg(feature =
    // "metered")]`, like the `Billing` service it calls, because the community
    // build never mentions money. The row above is `client,control`, so on a
    // METERED estate the verb was compiled out of the FRONT, which is the only
    // box that can ask: the appliance and the twin have no shell.
    //
    // MEASURED 2026-09-20 on the live gunnar.rs front (v1.4.0-353-g482c062):
    // `error: unrecognized subcommand 'appliance-identity'`, while
    // monetize-server on that same box narrated, every poll, that the appliance
    // signs with one key and its own config names another and every genuine
    // tenant renewal would be REFUSED.
    //
    // Its own row rather than a feature added to `gunnar-cli-musl`, because
    // that one is the open delivery and the open build must not gain a
    // money verb; and its own BIN NAME rather than a second resolution of
    // `gunnar-cli`, because a bin name is a file path and two rows naming one
    // path are one artifact written twice (`gunnar-cli/Cargo.toml`, top).
    //
    // It costs nothing to carry: `cargo tree -p gunnar-cli --no-default-features
    // --features client,control` and the same line plus `,metered` both resolve
    // 1656 rows, and neither contains gunnar-server. The feature buys one verb,
    // the proto it calls, and no dependency.
    Variant {
        name: "gunnar-cli-musl-metered",
        atom: Atom::GunnarCli,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::ThinStandalone,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-cli"),
        bin: Some("gunnar-cli-metered"),
        features: &["client", "control", "metered"],
        no_default_features: true,
        profile: "release",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── `gunnar-clock-musl` WAS HERE, and it is GONE (lane L10, 2026-10-21) ──
    //
    // The FRONT host's clock daemon (`gunnar-clock --features bin`, musl). The
    // front is vm1's own medium since 2026-09-25 and keeps the clock as a thread
    // of `gunnar-front`; the kit no longer builds the daemon. Gunnar's
    // `gunnar-clock` crate moved to edda `estate-clock` (its binary is
    // `estate-clock`, built in edda), and the crate is deleted here.
    //
    // ── vm1's one program, the FRONT medium's payload ──────────────────────
    //
    // `gunnar-front` (owner 2026-09-25: main and twin share the gunnar medium,
    // vm1 boots its OWN one). The payload variant of `gunnar-front-installer-open`,
    // as `gunnar-server` is the payload of `gunnar-installer-open`. musl for the
    // reason every PID-1 payload is: korp-start execs it on a disk with no libc.
    // `fat`: it holds its keys and answers; it asks no sibling for anything.
    Variant {
        name: "gunnar-front",
        atom: Atom::GunnarFront,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        // The `(MVP, server)` group's normal case is the product `gunnar`.
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-front"),
        bin: Some("gunnar-front"),
        // `dhcp-client` — the one real choice: a static site builds the front
        // with no DHCP client, as it does gunnar-server (`--no-default-features`
        // then names the set, so the receipt resolver can tell it apart).
        // `console`: the web face (gunnar-ui-server + the built console) on the
        // front's own :443 (2026-09-25) — the community half.
        features: &[
            "dhcp-client",
            "console",
            "forges",
            "clock-fetch",
            "resolver-probe",
            "acme",
            "egress-door",
            "mail",
            "sso",
        ],
        no_default_features: true,
        profile: "release",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        // gunnar-front is a DETACHED workspace (2026-10-22): a lone clone must
        // load the root one, and the front needs crates only siblings have.
        manifest_dir: Some("crates/gunnar-front"),
        siblings: &[],
        open: &["dev-narrate"],
        sign: false,
    },
    // ── `gunnar-front` + DEV-NARRATE (2026-09-30) ──────────────────────────────
    //
    // The build point the installer's `dev-narrate` option ARRIVES at when it is
    // ON (`--opt dev-narrate=on`, the kit's dev medium only): the same program
    // plus Skidbladnir nordisk-narrate's sender — the box's boot log sealed to
    // the installer's key over udp/51822 before the installer's link exists. Its
    // own row so the ON binary's receipt NAMES it a dev build; `gunnar-front` keeps
    // `dev-narrate` in `open` (off), and the option forbids `nordisk_narrate`
    // in every OFF (release) build.
    Variant {
        name: "gunnar-front-narrate",
        atom: Atom::GunnarFront,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-front"),
        bin: Some("gunnar-front"),
        features: &[
            "dhcp-client",
            "console",
            "forges",
            "clock-fetch",
            "resolver-probe",
            "acme",
            "egress-door",
            "mail",
            "sso",
            "dev-narrate",
        ],
        no_default_features: true,
        profile: "dev-media",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        // gunnar-front is a DETACHED workspace (2026-10-22): a lone clone must
        // load the root one, and the front needs crates only siblings have.
        manifest_dir: Some("crates/gunnar-front"),
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the PAID front's payload: gunnar's own front, `metered` ─────────────
    //
    // vm1 of `gunnar-twin-one` (was `gunnar-twin-monetize`): the console's paid half (gunnar-ui-server `metered`) and
    // nothing more. It runs no monetize (owner 13:35Z 2026-10-10, ONE monetize: the estate's
    // boxes call monetize.ignalina.dk's Fleet door); the 2026-10-08 commercial front built
    // from monetize-impl is LEGACY.
    Variant {
        name: "gunnar-front-metered",
        atom: Atom::GunnarFront,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-front"),
        bin: Some("gunnar-front"),
        // `public-site`: gunnar.rs's public face (TASKS-12, owner 2026-10-25), this
        // row and its narrate twin only (`tests/public_site_row.rs`).
        features: &[
            "dhcp-client",
            "metered",
            "public-site",
            "forges",
            "clock-fetch",
            "resolver-probe",
            "acme",
            "egress-door",
            "mail",
            "sso",
        ],
        no_default_features: true,
        profile: "release",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        // gunnar-front is a DETACHED workspace (2026-10-22): a lone clone must
        // load the root one, and the front needs crates only siblings have.
        manifest_dir: Some("crates/gunnar-front"),
        siblings: &[],
        open: &["dev-narrate"],
        sign: false,
    },
    // ── `gunnar-front-metered` + DEV-NARRATE (2026-09-30) ──────────────────────────────
    //
    // The build point the installer's `dev-narrate` option ARRIVES at when it is
    // ON (`--opt dev-narrate=on`, the kit's dev medium only): the same program
    // plus Skidbladnir nordisk-narrate's sender — the box's boot log sealed to
    // the installer's key over udp/51822 before the installer's link exists. Its
    // own row so the ON binary's receipt NAMES it a dev build; `gunnar-front-metered` keeps
    // `dev-narrate` in `open` (off), and the option forbids `nordisk_narrate`
    // in every OFF (release) build.
    Variant {
        name: "gunnar-front-metered-narrate",
        atom: Atom::GunnarFront,
        column: Column::Exe,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Exe,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: Some("gunnar"),
        package: Some("gunnar-front"),
        bin: Some("gunnar-front"),
        features: &[
            "dhcp-client",
            "metered",
            "public-site",
            "forges",
            "clock-fetch",
            "resolver-probe",
            "acme",
            "egress-door",
            "mail",
            "sso",
            "dev-narrate",
        ],
        no_default_features: true,
        profile: "dev-media",
        target: Some("x86_64-unknown-linux-musl"),
        container_ref: None,
        // gunnar-front is a DETACHED workspace (2026-10-22): a lone clone must
        // load the root one, and the front needs crates only siblings have.
        manifest_dir: Some("crates/gunnar-front"),
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the appliance image ──────────────────────────────────────────────────
    Variant {
        name: "gunnar-image",
        atom: Atom::GunnarServer,
        column: Column::Container,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Container,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: Some("gunnar-scratch"),
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the appliance ISO ────────────────────────────────────────────────────
    //
    // `iso-live`, NOT `iso-installer`: its boot parameters end in
    // `serve --no-grpc`, so the medium comes up serving git and writes nothing to
    // a disk. Calling it an installer would put a column label on the receipt
    // that the artifact contradicts the moment somebody boots it.
    Variant {
        name: "gunnar-appliance-iso",
        atom: Atom::GunnarServer,
        column: Column::IsoLive,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Iso,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the per-site installer ISO ───────────────────────────────────────────
    //
    // The pointer variant for the `[[build.installer]]` variant, closing the one
    // gap in the tagged-pointer pattern this table already uses twice
    // (`gunnar-image` → container payload, `gunnar-appliance-iso` → iso
    // payload). Like an iso, an installer variant carries the payload handle in
    // its OWN name — `assemble()` resolves `require_installer(&variant.name)` —
    // so there is no `installer_ref` field to fill.
    Variant {
        name: "gunnar-installer",
        atom: Atom::GunnarServer,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the KEYLESS installer ISO — door B ───────────────────────────────────
    //
    // The pointer variant for the second `[[build.installer]]` row,
    // `gunnar-installer-open` (2026-08-28). Same atom, same column, same
    // payload shape; the ONE product decision that differs is the bootstrap
    // door — no key on the medium, the control plane bound off the loopback,
    // first caller enrols — and that decision lives in the installer row's
    // argv (`recipe::OPEN_KORP_ARGS`), not here. A second matrix cell rather
    // than an `--opt`, because an option IS a cargo feature by construction and
    // the door is argv (reachability.rs has the long form).
    Variant {
        name: "gunnar-installer-open",
        atom: Atom::GunnarServer,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        // The medium's DEV-NARRATE option (default OFF) is this row's own
        // switchable axis: `recipe::dev_narrate_option`.
        open: &["dev-narrate"],
        sign: false,
    },
    // ── the FRONT medium — vm1's own installer ISO ──────────────────────────
    //
    // The pointer variant for `[[build.installer]] gunnar-front-installer-open`
    // (2026-09-25). Its payload is `gunnar-front`, not `gunnar-server`: the two
    // media of an estate are the gunnar medium (main + twin, byte for byte) and
    // this one (vm1). `recipe::front_installer` has the row.
    Variant {
        name: "gunnar-front-installer-open",
        atom: Atom::GunnarFront,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        // The medium's DEV-NARRATE option (default OFF) is this row's own
        // switchable axis: `recipe::dev_narrate_option`.
        open: &["dev-narrate"],
        sign: false,
    },
    // The pointer variant for `[[build.installer]] gunnar-front-installer-metered`
    // (2026-09-25): the paid front medium, payload `gunnar-front-metered`.
    Variant {
        name: "gunnar-front-installer-metered",
        atom: Atom::GunnarFront,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        // The medium's DEV-NARRATE option (default OFF) is this row's own
        // switchable axis: `recipe::dev_narrate_option`.
        open: &["dev-narrate"],
        sign: false,
    },
    // ── the virtio-scsi installer ISO — the SAME door, a different MACHINE ───
    //
    // The pointer variant for the third `[[build.installer]]` row,
    // `gunnar-installer-gce` (2026-08-30). Same atom, same column,
    // same payload, same keyless bootstrap door as the row above: what differs
    // is the machine the medium is built FOR — GCE's Persistent Disk rides
    // virtio-scsi, so the row declares `target.buses ∋ virtio-scsi` and takes
    // a kernel donor whose preset does not veto `CONFIG_SCSI`.
    //
    // A separate cell rather than an `--opt`, and for a sharper reason than the
    // door above: the bus decides the KERNEL, and a kernel is an artifact, not
    // a feature flag. Widening the shared row would put `SCSI BLK_DEV_SD
    // SCSI_VIRTIO` on the UpCloud appliance's 2.4 MB bzImage, which no customer
    // of that machine asked for.
    Variant {
        name: "gunnar-installer-gce",
        atom: Atom::GunnarServer,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the NVMe/ENA installer ISO — the SAME door, AWS Nitro's MACHINE ──────
    //
    // The pointer variant for the fourth `[[build.installer]]` row,
    // `gunnar-installer-aws` (2026-09-02). Same atom, same
    // column, same payload, same keyless bootstrap door as the GCE row above:
    // what differs is again the machine the medium is built FOR. AWS Nitro
    // presents its boot disk as NVMe (`/dev/nvme0n1`, never virtio or IDE) and
    // its only NIC as the Elastic Network Adapter, so the row declares
    // `target.buses ∋ nvme` + `target.nic = ena` and takes the `tunnr-kernel-nvme`
    // donor whose `slim` preset vetoes neither `CONFIG_BLK_DEV_NVME` nor
    // `CONFIG_ENA_ETHERNET`.
    //
    // A separate cell and not an `--opt`, for the GCE row's exact reason: the
    // bus and the NIC decide the KERNEL, and a kernel is an artifact, not a
    // feature flag. Widening the shared row would put the NVMe stack and an ENA
    // driver on the UpCloud appliance's 2.4 MB bzImage, for devices it does not
    // have.
    Variant {
        name: "gunnar-installer-aws",
        atom: Atom::GunnarServer,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // ── the Hyper-V installer ISO — the SAME door, Azure's MACHINE ────────────
    //
    // The pointer variant for the `gunnar-installer-azure` row (2026-09-05).
    // Same atom, same column, same payload, same keyless bootstrap door as the
    // GCE and Nitro rows: what differs is the machine. An Azure Gen2 VM
    // presents every disk as Hyper-V synthetic SCSI (`hv_storvsc`) and its NIC
    // as `hv_netvsc`, both over VMBus, so the row declares `target.buses ∋
    // hv-storvsc` + `target.nic = hv-netvsc` and takes the `tunnr-kernel-hyperv`
    // donor. A separate cell for the same reason as the two before it: the bus
    // and the NIC decide the KERNEL, and a kernel is an artifact.
    Variant {
        name: "gunnar-installer-azure",
        atom: Atom::GunnarServer,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // `gunnar-installer-metered` (2026-09-05). Same atom, same column, same
    // machine, same keyless bootstrap door as the open row: what differs is the
    // BINARY the medium is composed from — `gunnar-server-metered`, the build
    // point that links `monetize-embed` and the entitlement gate.
    //
    // **This cell is what makes the row buildable.** `[[build.installer]]`
    // declares how a medium is composed; the CLI door resolves `--variant` here,
    // so an installer row with no cell beside it is a declaration the door
    // cannot reach — `cli_matrix` iterates `set.variants`, not `set.installers`,
    // and would have gone on being green about a medium nobody could build. The
    // four rows above each have one for exactly this reason.
    //
    // A separate cell and not an `--opt`, for the GCE and Nitro rows' reason
    // one step over: `metered` is a cargo feature, so it selects a different
    // BINARY and therefore a different artifact. An option toggles features
    // within one variant's build; it cannot swap the variant a medium composes.
    Variant {
        name: "gunnar-installer-metered",
        atom: Atom::GunnarServer,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        // The medium's DEV-NARRATE option (default OFF) is this row's own
        // switchable axis: `recipe::dev_narrate_option`.
        open: &["dev-narrate"],
        sign: false,
    },
    // `gunnar-installer-metered-aws` / `-azure` (2026-09-05): the metered
    // BINARY on the AWS Nitro and Azure Gen2 machines — `metered_twin` of the
    // `-aws` and `-azure` rows in `recipe.rs`. Same reason for a cell each as
    // the one above: an installer row with no cell beside it is a medium the
    // CLI door cannot resolve. The kernel is the cloud row's (nvme / hyperv);
    // the cell says nothing about it, the `[[build.installer]]` row does.
    Variant {
        name: "gunnar-installer-metered-aws",
        atom: Atom::GunnarServer,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    Variant {
        name: "gunnar-installer-metered-azure",
        atom: Atom::GunnarServer,
        column: Column::IsoInstaller,
        suites: &[Suite::Mvp],
        kind: ArtifactKind::Installer,
        linkage: ServiceShape::FatService,
        is_default: false,
        member: None,
        package: None,
        bin: None,
        features: &[],
        no_default_features: false,
        profile: "",
        target: None,
        container_ref: None,
        manifest_dir: None,
        siblings: &[],
        open: &[],
        sign: false,
    },
    // `gunnar-twin` STOOD HERE from 2026-09-15 to 2026-09-17, a ninth pointer
    // variant beside a ninth installer row. Both are gone, and the deletion is
    // the fix: the row was `open_twin`'s clone, so its `variant` was
    // `gunnar-server` — the binary `tests/metered_symbols.rs` proves contains no
    // entitlement code — while gunnar.rs's PRIMARY is
    // `gunnar-installer-metered`/`gunnar-server-metered`. A twin exists to
    // become the primary, so a takeover would have swapped the metered binary
    // for one with no plan enforcement in it, silently. The twin installs the
    // primary's own medium with NO role on it (the role is conferred over the
    // wire, `Bootstrap.BecomeTwin`), which makes "the twin's variant equals the
    // primary's" true by construction rather than by care;
    // `tests/twin_is_the_same_medium.rs` holds it that way.
];

/// Every variant in [`VARIANTS`] names an assembly [`Atom::assembles`] allows, and an
/// artifact kind its column admits.
///
/// `const fn`, so the answer is computed while the crate compiles. See
/// [`EVERY_VARIANT_IS_LEGAL`].
#[must_use]
pub const fn every_variant_is_legal() -> bool {
    let mut i = 0;
    while i < VARIANTS.len() {
        let c = VARIANTS[i];
        if !c.atom.supports(c.column) {
            return false;
        }
        if !c.column.admits(c.kind) {
            return false;
        }
        i += 1;
    }
    true
}

/// **The compile error.**
///
/// A variant naming a pair [`Atom::assembles`] refuses — or an artifact kind its
/// column does not admit — stops `cargo build` here, before anything has been
/// built and before dwarves has offered the combination to anybody.
///
/// The message cannot name the offending variant: a `const` panic takes a literal,
/// with no formatting. The test of the same rule in `lib.rs` is the half that
/// names it, and the two call the same `const fn` so they cannot disagree.
pub const EVERY_VARIANT_IS_LEGAL: () = assert!(
    every_variant_is_legal(),
    "a variant in VARIANTS names an assembly Atom::assembles refuses, or an artifact \
     kind its Column does not admit. Run `cargo test -p gunnar-build-thing` — \
     the test of the same rule names which one."
);

/// The variants that fill `(atom, column)`, in declaration order.
///
/// Empty is a real answer, not a missing one: `(gunnar-server, rpm)` is a legal
/// assembly nobody has declared, and `(gunnar-cli, iso-live)` is not an assembly
/// at all. [`Atom::assembles`] is what tells those two apart.
#[must_use]
pub fn variants_at(atom: Atom, column: Column) -> Vec<&'static Variant> {
    VARIANTS
        .iter()
        .filter(|c| c.atom == atom && c.column == column)
        .collect()
}

/// The variant with this name, if any.
#[must_use]
pub fn by_name(name: &str) -> Option<&'static Variant> {
    VARIANTS.iter().find(|c| c.name == name)
}

/// **Which variant a resolved cargo feature set IS** — the lookup the receipt
/// writer runs from inside `build.rs`.
///
/// # Why the features are the key, and not a name somebody passes in
///
/// A build script cannot be told which variant it is building without somebody
/// declaring it a second time, and a second declaration is what the receipt
/// exists to stop trusting. What a build script CAN see is the feature set cargo
/// actually resolved, and the five cargo variants have five distinct feature sets —
/// which is not a coincidence, it is what makes them five different programs out
/// of one `main.rs`. So the resolved features identify the variant by construction.
///
/// [`the_five_cargo_variants_have_pairwise_distinct_feature_sets`] holds that
/// property, and it is the guard that fails if a sixth variant ever collides with
/// an existing one.
///
/// # Exact equality, and a `None` rather than a guess
///
/// `resolved` must equal the variant's declared feature list, normalised. A subset
/// rule would make `--all-features` resolve to whichever variant happened to be
/// biggest, and a receipt naming a variant the build was not is worse than no
/// receipt at all: the first answers the rebuild question wrongly and the second
/// answers it not at all.
///
/// [`the_five_cargo_variants_have_pairwise_distinct_feature_sets`]:
///     #the_five_cargo_variants_have_pairwise_distinct_feature_sets
/// # Only the cargo variants are candidates
///
/// `gunnar-image` and `gunnar-appliance-iso` compile nothing — they compose
/// another variant's artifact — so they declare no features, and an empty resolved
/// set would otherwise resolve to whichever of them came first in the table.
/// That is not a hypothetical; it is what the first version of this function
/// did, and `--no-default-features` with nothing else is a real build. A build
/// script only ever runs inside a cargo compilation, so `package.is_some()` is
/// the honest candidate set.
#[must_use]
pub fn by_resolved_features(resolved: &BTreeSet<String>) -> Option<&'static Variant> {
    let mut hit = VARIANTS
        .iter()
        .filter(|c| c.package.is_some() && c.built_here())
        .filter(|c| &c.normalised_features() == resolved);
    let first = hit.next()?;
    // **Only answer when the features alone settle it.** Where two rows share a
    // feature set and differ by target, this cannot tell them apart and must
    // not pick one: see [`by_build_point`], which is what a caller holding a
    // target should ask.
    if hit.next().is_some() {
        return None;
    }
    Some(first)
}

/// **A build point is (features, target), not features alone.**
///
/// The identity used to be the feature set by itself, and the table could
/// therefore hold only ONE row per set. That is the rule that made the front's
/// operator binaries unbuildable here: `gunnar-cli` for this host and the same
/// `gunnar-cli` built for `x86_64-unknown-linux-musl` are the same features and
/// a different artifact at a different path, so the second one could not be
/// declared and was written out as a cargo line in another repository's xtask
/// instead — the second description of a build that this table exists to be the
/// only one of.
///
/// The model already said so; only the lookup disagreed. `variant → features →
/// SETTINGS → typ`, and the target is a setting: it changes the artifact and
/// its path. [`crate::emit::attribute`] has always taken the actual target and
/// compared it AFTER resolving, which is the same two facts in the wrong order —
/// it could only ever report a mismatch where it can now make a match.
///
/// `None` for the target means "whatever the host is", as everywhere else in
/// this file, so a caller that knows the host passes it and a caller that does
/// not passes `None` and gets the host row.
#[must_use]
pub fn by_build_point(
    resolved: &BTreeSet<String>,
    target: Option<&str>,
) -> Option<&'static Variant> {
    VARIANTS
        .iter()
        .filter(|c| c.package.is_some() && c.built_here())
        .find(|c| &c.normalised_features() == resolved && c.target == target)
}

/// Cargo's own spelling of a feature name in `CARGO_FEATURE_*`: uppercased, with
/// every non-alphanumeric character mapped to `_`.
///
/// The receipt writer reads the resolved set out of the environment, where the
/// names arrive in this form and cannot be mapped back (`a-b` and `a_b` are one
/// variable). So both sides are normalised forward, which is exact.
#[must_use]
pub fn normalise_feature(name: &str) -> String {
    name.chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() {
                c.to_ascii_uppercase()
            } else {
                '_'
            }
        })
        .collect()
}

impl Variant {
    /// The panel the scene opens for this variant's artifact.
    ///
    /// Comes from the variant's KIND rather than its column, which is the only
    /// reason the narrowing in [`Column::admits`] is worth having:
    /// `gunnar-service` and `gunnar-server` are both `exe` variants and want
    /// different panels.
    #[must_use]
    pub const fn pane(&self) -> PaneKind {
        PaneKind::for_artifact(self.kind)
    }

    /// **Built from THIS repository** — every row but the console's (`gunnar-ui`, a
    /// sibling checkout's crate, `manifest_dir` `../gunnar-ui/…`). Only a row built here
    /// can be attributed from a build script's resolved features: the console's builds
    /// write their receipts from their own recipe.
    #[must_use]
    pub fn built_here(&self) -> bool {
        !self.manifest_dir.is_some_and(|d| d.starts_with("../"))
    }

    /// This variant's `--features` list in cargo's `CARGO_FEATURE_*` spelling.
    ///
    /// The declared list must already be closed under gunnar's own feature
    /// implications — `gunnar-service` names `["server", "systemd"]` rather than
    /// `["systemd"]` alone, even though `systemd = [… "server"]` implies it —
    /// because the environment shows the CLOSURE and this is compared to it by
    /// equality. `the_declared_feature_lists_are_closed_under_implication` in
    /// `gunnar-cli` is what holds that, against the real manifest.
    #[must_use]
    pub fn normalised_features(&self) -> BTreeSet<String> {
        self.features.iter().map(|f| normalise_feature(f)).collect()
    }

    /// The cargo profile, resolved. `""` in the table means the cargo default.
    #[must_use]
    pub fn profile_resolved(&self) -> Profile {
        Profile::from(self.profile.to_string())
    }

    /// **Every setting this build resolves to** — the map a receipt records.
    ///
    /// Derived from the variant's own fields rather than listed a second time
    /// beside them. A hand-written settings list is a copy of the build point
    /// that drifts from it silently, and a receipt that records the drifted copy
    /// is worse than one that records nothing: it answers the rebuild question
    /// wrongly.
    ///
    /// Absolute paths and timestamps are deliberately absent, per the receipt's
    /// own rule — they differ between two builds that are otherwise identical.
    #[must_use]
    pub fn resolved_settings(&self) -> BTreeMap<String, String> {
        let mut s = BTreeMap::new();
        s.insert("atom".to_string(), self.atom.as_str().to_string());
        s.insert("column".to_string(), self.column.as_str().to_string());
        // The receipt KEY keeps the retired word: it is a wire literal shipped in
        // every gunnar binary built so far, and a reader comparing receipts across
        // builds must not see two spellings of one setting. Renaming it is a
        // receipt-version decision, not a vocabulary edit.
        s.insert("cell".to_string(), self.name.to_string());
        s.insert("kind".to_string(), String::from(self.kind));
        s.insert("linkage".to_string(), self.linkage.as_str().to_string());
        s.insert(
            "suites".to_string(),
            self.suites
                .iter()
                .map(|x| x.as_str())
                .collect::<Vec<_>>()
                .join(","),
        );
        s.insert("features".to_string(), self.features.join(","));
        s.insert(
            "no-default-features".to_string(),
            self.no_default_features.to_string(),
        );
        s.insert(
            "profile".to_string(),
            self.profile_resolved().label().to_string(),
        );
        // "host" rather than an absent key: a receipt with no `target` cannot be
        // told apart from one written before the key existed.
        s.insert(
            "target".to_string(),
            self.target.unwrap_or("host").to_string(),
        );
        if let Some(p) = self.package {
            s.insert("package".to_string(), p.to_string());
        }
        if let Some(b) = self.bin {
            s.insert("bin".to_string(), b.to_string());
        }
        if let Some(c) = self.container_ref {
            s.insert("container".to_string(), c.to_string());
        }
        s
    }

    /// This variant as the declaration type `nornir-build-thing` validates and
    /// materializes cargo argv from.
    ///
    /// This is the seam: everything downstream — the validator, the cargo argv,
    /// the buildgate, the linkage audit — keeps working on the type it already
    /// works on, and none of it learns that gunnar's declaration stopped being a
    /// file.
    #[must_use]
    pub fn to_variant(&self) -> BuildVariant {
        BuildVariant {
            name: self.name.to_string(),
            package: self.package.map(str::to_string),
            bin: self.bin.map(str::to_string),
            features: self.features.iter().map(|f| (*f).to_string()).collect(),
            no_default_features: self.no_default_features,
            profile: self.profile_resolved(),
            target: self.target.map(str::to_string),
            cfg: None,
            member: self.member.map(str::to_string),
            shape: self.linkage,
            role: Some(self.atom.role()),
            suites: self.suites.iter().map(|s| s.as_str().to_string()).collect(),
            is_default: self.is_default,
            kind: self.kind,
            container_ref: self.container_ref.map(str::to_string),
            vm_ref: None,
            embeds: BTreeSet::new(),
            reaches: BTreeSet::new(),
            open: self.open.iter().map(|f| (*f).to_string()).collect(),
            manifest_dir: self.manifest_dir.map(str::to_string),
            siblings: self.siblings.iter().map(|s| (*s).to_string()).collect(),
            sign: self.sign.then(nornir_build_thing::sign::SignSpec::default),
            ..BuildVariant::default()
        }
    }
}