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:#?}");
}
}
}