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

mikronova/modgunn

crates/modgunn-build-thing/src/recipe.rs

//! **The recipe** — modgunn's `impl BuildThing`, the ONE declaration of what
//! this repository builds. `.nornir/build-variants.toml` is emitted from
//! [`ModgunnBuildThing::variants`] (see [`crate::emit`]); nothing here reads it.
//!
//! # ONE atom, and four named fictions
//!
//! `workspace_nordisk/.nornir/build-matrix-2026-08-02.md` gives every member the
//! same five shapes — `<x>-server` ⭐ · `<x>` · `<x>-cli` · `<x>-ui` ·
//! `<x>-solo`. Rickard's rule (2026-08-03): *"bara de som är markerade
//! suite=ORIGINAL i matrisen skall ha built things"* — carrying the suite tag is
//! what CLAIMS build-thing status, and an atom that claims it and cannot build is
//! not a small documentation defect. It is the worst kind of declaration: a
//! promise the repo cannot keep.
//!
//! So this recipe declares **one** row, and names the other four as fictions
//! rather than shipping stubs for them. Each gap was MEASURED on this checkout,
//! and `tests/build_variants.rs` asserts every one as a POSITIVE claim so it
//! cannot quietly turn back into a phantom the day somebody copies a sibling
//! repo's file:
//!
//! * **`modgunn-server` — there is no daemon.** No listener and no serve loop:
//!   the only `TcpListener`/`serve(` in the crate is inside `#[cfg(test)]` in
//!   `src/service.rs`, where a test binds a throwaway port to prove skidbladnir's
//!   health probe can see an open socket. `src/main.rs` is a slice-pattern
//!   dispatch over `args()`; every verb returns and the process exits.
//!   `src/service.rs` is `#[cfg(feature = "framework")]` and no row enables it,
//!   so "it wraps as a skidbladnir ServiceSpec" is false for every artifact this
//!   recipe builds. And even with the feature on, the declared `ExecStart` names
//!   `world-delta watch`, a verb `src/main.rs` does not have. Making it real is
//!   PRODUCT WORK, not a declaration fix.
//! * **`modgunn-cli` — a thin modgunn binary is impossible by construction.**
//!   `<x>-cli` means "talks gRPC to a server, holds no local store"; modgunn has
//!   no wire at all (zero tonic/prost/nornir-service in the manifest), and the
//!   one `[[bin]]` it ships carries `required-features = ["warehouse", "scan"]`
//!   while `warehouse = ["dep:skade", …]` — the sole executable modgunn can
//!   produce REQUIRES the dependency that makes it fat. Naming the bin under a
//!   lean feature set errors; dropping the bin gives the silent form, where cargo
//!   SKIPS the target and the build succeeds having produced no executable, which
//!   is exactly how the old `modgunn-thin` atom passed for months while building
//!   nothing.
//! * **`modgunn-ui` — no UI**, measured from the manifest: zero egui / eframe /
//!   tauri / iced / slint / wasm-bindgen / winit / wgpu. modgunn is a detection
//!   and collection platform driven by a human or a script.
//! * **`modgunn-solo` — nothing to embed, nothing to show.** `solo` is the UI
//!   with its server compiled in, and both halves are missing.
//!
//! # The asymmetry: modgunn must NOT embed nornir or holger
//!
//! They depend on modgunn — modgunn exists to break their cycle — so they are
//! UPSTREAM of it, and a fat modgunn that compiled either in would invert the
//! suite dependency graph. [`downstream`] is the embed-allowlist that encodes it:
//! `skade` only. A variant declaring `embeds = ["nornir"]` is rejected by
//! `BuildVariantSet::validate`.
//!
//! # The six declared-but-never-built features
//!
//! They were prose in the hand-written file and are DATA now
//! ([`allow_unbuilt`]), which matters because prose is not something a gate can
//! read. The measurement behind them is the reason to care: on 2026-08-01 each
//! was compiled one at a time with `cargo check -p modgunn --features <f>`, and
//! **two of six were actually broken** — `warehouse-sql` and `test-support`,
//! both with the same error, because `warehouse` alone already satisfies the
//! bin's `required-features` while the ⭐ atom always turns `scip` on as well, so
//! cargo built the bin in a configuration no declared atom ever produces. 33 %,
//! not a theoretical risk.

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

use nornir_build_thing::contract::Coordinate;
use nornir_build_thing::plugin::{BuildThing, Manifest, Role, ABI_VERSION};
use nornir_build_thing::variant::{
    ArtifactKind, BuildVariant, BuildVariantSet, Profile, ServiceShape,
};

/// The crate a receipt names as its recipe.
pub const KRATE: &str = "modgunn-build-thing";

/// The content hash over this crate's sources, computed by `build.rs`.
pub const CONTENT_HASH: &str = env!("MODGUNN_BUILD_THING_CONTENT_HASH");

/// The one delivery. ORIGINAL is the only suite name that may appear anywhere in
/// this repo; `suite_cli::resolve` fails loudly on any other.
pub const SUITE: &str = "ORIGINAL";

/// modgunn's build thing.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ModgunnBuildThing;

impl ModgunnBuildThing {
    /// The coordinate a receipt names this recipe by: `krate:version:hash`.
    #[must_use]
    pub fn coordinate() -> Coordinate {
        Coordinate {
            krate: KRATE.to_string(),
            version: env!("CARGO_PKG_VERSION").to_string(),
            hash: Some(CONTENT_HASH.to_string()),
        }
    }
}

fn s(x: &str) -> String {
    x.to_string()
}

/// **⭐ The fat client — the one atom modgunn genuinely has.**
///
/// The matrix row it fills: `ORIGINAL modgunn client fat modgunn`. A batch CLI
/// that does one job and exits.
///
/// `linkage = fat` is verified from cargo and not from the name: `warehouse =
/// ["dep:skade", …]`, so this build genuinely compiles the Iceberg engine in and
/// needs no other process running — which is what the matrix means by a fat
/// client. It is also the ONLY linkage a modgunn binary can have, which is why
/// there is no `-cli` row.
///
/// `no_default_features` plus the explicit list is a CORRECTION, not a change of
/// artifact: an older declaration left defaults on, so the build also carried
/// `provenance-verify` (and its `nornir-build-thing` + `znippy-common/sign`
/// closure) without saying so. The list below is exactly `default ∪ declared`,
/// so the bytes are the same and the declaration is the whole truth.
pub fn variants() -> Vec<BuildVariant> {
    vec![BuildVariant {
        name: s("modgunn"),
        role: Some(Role::Client),
        is_default: true,
        suites: vec![s(SUITE)],
        member: Some(s("modgunn")),
        package: Some(s("modgunn")),
        bin: Some(s("modgunn")),
        kind: ArtifactKind::Exe,
        shape: ServiceShape::FatService,
        no_default_features: true,
        features: vec![
            s("scan"),
            s("scip"),
            s("warehouse"),
            s("rustsec"),
            s("provenance-verify"),
        ],
        profile: Profile::Release,
        ..BuildVariant::default()
    }]
}

/// modgunn's real feature space — exactly the `[features]` table minus
/// `default`, which is an alias and not a surface.
fn offers() -> Vec<&'static str> {
    vec![
        "scan",
        "scip",
        "warehouse",
        "warehouse-sql",
        "rustsec",
        "manifests",
        "provenance-verify",
        "framework",
        "framework-oci",
        "testmatrix",
        "test-support",
    ]
}

/// **The six features no row builds, each with a written reason.**
///
/// No atom is invented for them: an atom is a thing somebody SHIPS, and none of
/// these is shipped as an artifact of its own. `manifests` and `framework` carry
/// real shipped CLI verbs (`modgunn ingest`, `modgunn tools|demo|service`) and
/// are the first candidates the day modgunn ships a second delivery.
pub fn allow_unbuilt() -> BTreeMap<String, String> {
    [
        ("warehouse-sql", "the skade-sql arm of the Iceberg seam. Not shipped as an artifact of its own; it is the feature that was BROKEN and unnoticed until 2026-08-01 (`no method named reachable_vuln_findings` — cargo built the bin under warehouse-without-scip, a configuration no declared atom produces). Fixed then, and listed here so the gate keeps asking."),
        ("manifests", "the manifest/SBOM ingest surface behind `modgunn ingest`. A real shipped CLI verb of the ⭐ atom's binary, not an artifact of its own — and the first candidate for a row the day modgunn ships a second delivery."),
        ("framework", "the jera + skidbladnir service framework behind `modgunn tools|demo|service`. Deliberately not in the ⭐ atom: `src/service.rs` has no serve loop and its declared ExecStart names a verb main.rs does not have, so a row enabling it would declare a daemon that does not exist."),
        ("framework-oci", "framework plus jera's container-oci arm. Same reason as `framework`, one layer further out: a container backend for a daemon that has not been written."),
        ("testmatrix", "turns the tests' `functional_status` self-reports into real nornir test-matrix rows. A TEST-HARNESS mode, never a delivery."),
        ("test-support", "test-only helpers over `warehouse`. The second of the two features measured BROKEN on 2026-08-01, for the same root cause as `warehouse-sql`; a shipped artifact must not carry test scaffolding."),
    ]
    .into_iter()
    .map(|(k, v)| (s(k), s(v)))
    .collect()
}

/// Cargo `[[bin]] required-features` for the `modgunn` binary, mirrored so a bad
/// feature combo fails fast with a clear message instead of a raw cargo error.
fn required() -> BTreeMap<String, Vec<String>> {
    [("modgunn", vec![s("warehouse"), s("scan")])]
        .into_iter()
        .map(|(k, v)| (s(k), v))
        .collect()
}

/// The suite dependency graph as an embed-allowlist: the siblings strictly
/// DOWNSTREAM of modgunn are the only ones a fat variant may compile IN.
fn downstream() -> BTreeMap<String, BTreeSet<String>> {
    [("modgunn", ["skade"])]
        .into_iter()
        .map(|(k, v)| (s(k), v.into_iter().map(s).collect()))
        .collect()
}

/// **The `[[bin]]` targets no row builds, each with its reason** —
/// `[build.allow_unbuilt_bins]`. ⚒ Build's gate (`buildgate::tally_bins`) counts every
/// bin cargo can see in this tree; MEASURED 2026-10-16 on loki these had neither a
/// row nor a reason (`tests/build_gate.rs` now asks the same question).
fn allow_unbuilt_bins() -> BTreeMap<String, nornir_build_thing::variant::AllowEntry> {
    [
        ("modgunn-build-thing", "this recipe crate's own emit door (`cargo run -p modgunn-build-thing -- emit` regenerates .nornir/build-variants.toml); the forge loads the crate as wasm, the bin is repository tooling, never shipped."),
        ("xtask", "the `cargo xtask` door onto this repo's build thing — repository tooling, never shipped."),
    ]
    .into_iter()
    .map(|(k, v): (&str, &str)| (k.to_string(), nornir_build_thing::variant::AllowEntry::Reason(v.to_string())))
    .collect()
}

impl BuildThing for ModgunnBuildThing {
    fn manifest(&self) -> Manifest {
        Manifest {
            abi: ABI_VERSION,
            repo: s("modgunn"),
            source_crate: s(KRATE),
            summary: "modgunn — the smallest verdict core of the nordisk suite: the \
                      security + SCIP world-delta gatekeeper (Gjallarbrú) that holger \
                      AND nornir both link to break their dependency cycle. One fat \
                      client binary, and four shapes it does not have."
                .to_string(),
        }
    }

    fn variants(&self) -> BuildVariantSet {
        BuildVariantSet {
            base_suite: Some(s(SUITE)),
            offers: offers().into_iter().map(s).collect(),
            allow_unbuilt: allow_unbuilt(),
            allow_unbuilt_bins: allow_unbuilt_bins(),
            required: required(),
            downstream: downstream(),
            variants: variants(),
            ..BuildVariantSet::default()
        }
    }
}

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

    /// **The four fictions stay fictions.** A row named for a shape modgunn does
    /// not have is the promise this repo cannot keep; the day one is added it
    /// must be added deliberately, and this is where it fails first.
    #[test]
    fn the_recipe_declares_the_one_atom_modgunn_has_and_none_it_does_not() {
        let set = ModgunnBuildThing.variants();
        assert_eq!(set.names(), vec!["modgunn"], "modgunn has ONE atom");
        for fiction in ["modgunn-server", "modgunn-cli", "modgunn-ui", "modgunn-solo"] {
            assert!(
                set.get(fiction).is_none(),
                "'{fiction}' is a FICTION measured on this checkout — see the module doc; \
                 declaring it is a promise the repo cannot keep"
            );
        }
    }

    /// **modgunn may not embed what depends on it.** nornir and holger are
    /// UPSTREAM — modgunn exists to break their cycle — so a fat variant that
    /// compiled either in would invert the suite dependency graph. No real
    /// variant declares `embeds` (a `kind = exe` forbids it, and `warehouse`
    /// already IS the skade embed), so the rule is exercised against a synthetic
    /// one rather than left to be discovered.
    #[test]
    fn a_variant_that_embeds_an_upstream_sibling_is_refused() {
        let set = ModgunnBuildThing.variants();
        assert_eq!(
            set.downstream.get("modgunn").map(|d| d.iter().cloned().collect::<Vec<_>>()),
            Some(vec![s("skade")]),
            "the embed-allowlist is skade and nothing else"
        );
        let inverted = BuildVariant {
            name: s("modgunn-inverted"),
            package: Some(s("modgunn")),
            member: Some(s("modgunn")),
            embeds: ["nornir"].into_iter().map(s).collect(),
            ..BuildVariant::default()
        };
        assert!(
            set.validate(&inverted).is_err(),
            "a variant embedding nornir inverts the suite dep graph and must be refused"
        );
    }

    /// The feature axis does not rot: eleven offered, five built, six excused by
    /// name — and a stale excuse (a feature a row now builds) fails too.
    #[test]
    fn the_feature_axis_gate_is_clean() {
        let set = ModgunnBuildThing.variants();
        if let Err(rot) = FeatureAxis::of("modgunn", &set).gate() {
            panic!("the feature axis is rotting: {rot:#?}");
        }
    }
}