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

//! **A `linkage` declaration is a claim about the BINARY — this is where it is
//! checked.**
//!
//! [`variant`](crate::variant) lets a build thing declare `linkage = "thin"` or
//! `"fat"`. Nothing in a TOML file makes that true. Three dwarves variants once
//! declared `embeds = [nornir, holger, skade]` while carrying zero nornir symbols,
//! and holger-ui shipped as the "thin" operator client while linking an in-process
//! Iceberg catalog and a real HTTP gateway. [`LinkageAudit`] reads the produced
//! produced artifact's symbol table (pure-Rust via the `object` parser, demangled
//! like `nm -C`) and says whether the declaration survives contact with it.
//!
//! ## Two design properties, both load-bearing
//!
//! * **An unmeasured audit never passes.** "Could not measure" is its own outcome
//!   ([`LinkageVerdict::NotMeasured`]), never folded into "passed". A missing
//!   binary, an artifact that will not parse as an object file, an unreadable
//!   artifact — each is a FAILURE. Conflating "no evidence of a problem" with
//!   "evidence of no problem"
//!   is the single most productive bug generator in this constellation.
//! * **A fat variant is never failed for being fat — and never passes for being
//!   empty.** A `linkage = "fat"` build is *supposed* to carry the server and the
//!   storage engine, so finding them there is the expected result, not a
//!   violation. But a guard that passes everything proves nothing: if the fat
//!   variant stops embedding, the thin check it is paired against has quietly
//!   become vacuous. So a variant that declares it embeds and measurably embeds
//!   nothing is [`LinkageVerdict::EmbeddedButEmpty`], which does not hold.
//!
//! ## Why this lives here and not in the forge
//!
//! It used to live in `dwarves::build_thing`, where it could not be reused: dwarves
//! is the whole CI/CD forge (~127 dependencies), so dev-depending on it to reach one
//! struct would make `cargo test -p holger-ui` build the forge. holger therefore
//! re-implemented the contract in its own test file — a twin, and twins have been
//! the richest single source of bugs here. `nornir-build-thing` is the right home:
//! every build thing already depends on it, it is a leaf, and it already owns the
//! `ServiceShape` / `linkage` schema this audits.
//!
//! The symbol read is **pure-Rust**: the `object` parser reads the ELF symbol
//! table in-process and `rustc-demangle` turns each name into the readable path
//! `nm -C` would print — no `nm` subprocess (the constellation LAW: symbol
//! listing goes through `object`/`gimli`, never `nm`). This mirrors
//! `plugin_host`, which already parses object files with `object::File::parse`.
//! `nm -C` demangles, and families such as `nornir::server` only match once the
//! v0/legacy mangling is undone, so demangling is not optional here.

use std::path::{Path, PathBuf};

/// Read every symbol-table name from the object file at `artifact` and demangle
/// it (v0 `_R…` / legacy `_ZN…`) to the readable path `nm -C` prints — pure-Rust
/// via the `object` parser + `rustc-demangle`, no `nm` subprocess. Reads
/// `.symtab` (like plain `nm`), not the dynamic table, so counts match
/// `nm -C bin | grep -c`.
///
/// `None` ⇒ the audit is UNMEASURED, never a pass, in **three** cases:
///
/// ```text
/// absent                the file is not there
/// not an object file    an ISO, a tarball, a shell script
/// STRIPPED              it parsed, and the symbol table is empty
/// ```
///
/// # Why a stripped binary is unmeasured rather than clean
///
/// This is the silent-green shape the whole module exists to avoid, and it was
/// here until 2026-08-06. A `strip = true` release binary has **no symbols at
/// all**, so every forbidden family counts zero, so a `linkage = "thin"` claim —
/// and a receipt's `gpu = false` — passed *because nothing could be seen*. That
/// is the exact conflation of "no evidence of a problem" with "evidence of no
/// problem" the doc comment above warns about, arrived at by a different road:
/// the reader succeeded, and had nothing to say. An empty symbol table is not a
/// measurement, so it does not get to be a pass.
///
/// Public because [`receipt_proof`](crate::receipt_proof) measures a receipt's
/// claims against the same symbol table this audit reads. It consumes THIS
/// function rather than carrying its own `object` walk: a second symbol read
/// would be a second answer to "what does this binary link", and the two would
/// disagree the first time one of them learned about a mangling or a section the
/// other did not — starting with this rule.
pub fn demangled_symbols(artifact: &Path) -> Option<Vec<String>> {
    use object::{Object, ObjectSymbol};
    let bytes = std::fs::read(artifact).ok()?;
    let file = object::File::parse(&*bytes).ok()?;
    let symbols: Vec<String> = file
        .symbols()
        .filter_map(|s| s.name().ok())
        .map(|n| format!("{:#}", rustc_demangle::demangle(n)))
        .collect();
    if symbols.is_empty() {
        return None;
    }
    Some(symbols)
}

/// Is `sym` a compiler-generated DESTRUCTOR, rather than code somebody wrote?
///
/// `core::ptr::drop_glue::<T>` and `core::ptr::drop_in_place::<T>` are emitted by
/// rustc for a monomorphised `T`, and the demangled name spells out **every type
/// parameter of `T`**. That matters here because the families in
/// [`THIN_FORBIDDEN_SYMBOLS`] are matched as SUBSTRINGS, so destructor glue for a
/// deeply-generic type trips a family it merely *mentions*:
///
/// ```text
/// core::ptr::drop_glue::<hyper_util::service::glue::TowerToHyperService<…
///     <tonic::transport::server::Server>::serve_internal<…>::{closure#0}…>>
/// ```
///
/// One symbol, and it matches `tonic::transport::server` without a single byte of
/// server code being reachable.
///
/// **Why it is safe to exclude glue from the count.** To *run* a server you need the
/// server's own code, and that is not glue. Retained glue with no accompanying
/// non-glue symbol means the type is constructed nowhere — the instantiation
/// survived codegen while its constructing code was optimised away. Measured on
/// holger's two real artifacts on 2026-08-01, same flags, same facett:
///
/// | family | thin `gui` | fat `gui-embedded` |
/// |---|---|---|
/// | `tonic::transport::server` | 1 total — **1 glue, 0 non-glue** | 4 total — **0 glue, 4 non-glue** |
/// | `FastRoutes` | 0 | 19 total — 5 glue, **14 non-glue** |
/// | `LocalHolger` | 0 | 34 total — 7 glue, **27 non-glue** |
/// | `iceberg` | 0 | 5 684 total — **4 102 non-glue** |
/// | `parquet` | 0 | 2 387 total — **1 955 non-glue** |
///
/// The genuinely-embedding artifact has **no glue-only family at all**, so ignoring
/// glue costs nothing in detection, while the thin artifact's single false hit
/// disappears. That is the whole justification, and it is a measurement rather than
/// an argument from first principles.
///
/// **Glue is not discarded, it is reclassified** — see [`LinkageAudit::glue_only`].
/// A family whose only evidence is glue is reported, just not counted as linkage. An
/// audit that silently dropped the evidence would be trading one blind spot for
/// another.
pub fn is_compiler_destructor_glue(sym: &str) -> bool {
    sym.contains("core::ptr::drop_glue::<") || sym.contains("core::ptr::drop_in_place::<")
}

/// Is `sym` an object-file **FILE symbol** (ELF `STT_FILE`) — the name of a
/// TRANSLATION UNIT the linker recorded — rather than code somebody wrote?
///
/// The symbol table of a binary that statically links C carries one entry per
/// C source file: `p_x25519.c`, `sha512.c`, `zlib_inflate.c`. `object`'s symbol
/// iterator yields them (plain `nm` hides them behind `-a`), and the families
/// in [`count_families`] are matched as SUBSTRINGS, so a translation-unit name
/// trips a family it merely *spells*.
///
/// **MEASURED 2026-08-22**, and it is why this function exists: an installer
/// option declaring `forbid_symbols = ["x25519"]` — the spelling the design
/// document itself uses — went RED on a binary carrying **no WireGuard client
/// at all**. The two hits were `p_x25519.c` and `p_x25519_asn1.c`, aws-lc
/// translation units pulled in by rustls under `reqwest` under `nornir-git`.
/// Every appliance binary in the constellation links a TLS stack, so that
/// guard could never have gone green — the exact mirror of a guard that can
/// never go red, and just as useless.
///
/// The rule is deliberately narrow so it cannot swallow real code: a Rust path
/// always carries `::`, so a name with no `::` that ends in a C/C++/assembly
/// source extension is a translation unit and nothing else.
///
/// Like glue, this is **reclassified, not discarded** — it lands in
/// [`FamilyCounts::glue_only`], so the evidence stays visible and only stops
/// counting as linkage.
pub fn is_object_file_symbol(sym: &str) -> bool {
    if sym.contains("::") {
        return false;
    }
    let lower = sym.to_ascii_lowercase();
    [".c", ".cc", ".cpp", ".cxx", ".s", ".asm"].iter().any(|e| lower.ends_with(e)) || is_rust_cgu_file_symbol(sym)
}

/// Is `sym` the FILE symbol of a **Rust codegen unit** —
/// `<crate>.<16 hex>-cgu.<n>` (the `.rcgu.o` object's translation-unit name)?
///
/// **MEASURED 2026-09-15**, and it is why this arm exists: gunnar-server built
/// with its runtime grow engine (`korp-installer`, which depends on
/// `wg-appliance-core`) went RED on the `wireguard = off` audit with
/// `wg_appliance_core ×1`. The one hit was
/// `wg_appliance_core.27049babafb37dc3-cgu.0` — the FILE symbol of a codegen
/// unit the linker kept for THREE symbols, all of them `alloc`/`std` generics
/// (`Arc<mpmc::context::Inner>::drop_slow`, `RawVec<…>::grow_one`,
/// `RawVecInner::finish_grow`) that happened to be INSTANTIATED in that crate
/// and deduplicated across the link: the same code every crate using a std
/// channel instantiates, with `wg_appliance_core` only in the v0 mangling's
/// instantiating-crate suffix, which the demangler rightly drops. Not one byte
/// of the crate's own code was in the binary — the `::` paths the audit
/// matches on (`wg_appliance_core::…`) were absent, and the family tripped on
/// a translation-unit name it merely spells, exactly the C case above.
///
/// Narrow on purpose: no `::`, a 16-hex-digit hash after the first `.`, and
/// `-cgu.<digits>` (optionally `.rcgu.o`) at the end. A real symbol of the
/// family carries its path, and a path carries `::`.
pub fn is_rust_cgu_file_symbol(sym: &str) -> bool {
    if sym.contains("::") {
        return false;
    }
    let sym = sym.strip_suffix(".rcgu.o").unwrap_or(sym);
    let Some((_, rest)) = sym.split_once('.') else { return false };
    let Some((hash, n)) = rest.split_once("-cgu.") else { return false };
    hash.len() == 16 && hash.bytes().all(|b| b.is_ascii_hexdigit()) && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())
}

/// Count each family's symbols in `symbols`, split into real linkage and glue-only.
///
/// Returns `(found, glue_only)`. A family lands in `found` when at least one
/// NON-glue symbol names it, carrying that non-glue count. A family with matches
/// that are *all* destructor glue lands in `glue_only` instead — visible, but not
/// treated as linkage. A family with no matches at all appears in neither.
///
/// Pure so it can be tested without building a binary; `of_families` is the only
/// caller, so there is one counting rule and not two.
pub fn count_families(symbols: &[String], families: &[&str]) -> FamilyCounts {
    let mut found = Vec::new();
    let mut glue_only = Vec::new();
    for fam in families {
        let mut real = 0usize;
        let mut glue = 0usize;
        for sym in symbols.iter().filter(|l| l.contains(*fam)) {
            // Two classes of evidence that are NOT code the Rust build chose:
            // compiler destructor glue for a type constructed nowhere, and the
            // FILE symbol of a C translation unit. Both are counted separately
            // and reported, never treated as linkage.
            if is_compiler_destructor_glue(sym) || is_object_file_symbol(sym) {
                glue += 1;
            } else {
                real += 1;
            }
        }
        if real > 0 {
            found.push(((*fam).to_string(), real));
        } else if glue > 0 {
            glue_only.push(((*fam).to_string(), glue));
        }
    }
    FamilyCounts { found, glue_only }
}

/// The split returned by [`count_families`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FamilyCounts {
    /// Families present as real, non-glue linkage, as `(family, non-glue count)`.
    pub found: Vec<(String, usize)>,
    /// Families whose ONLY matches were **not code the Rust build chose** —
    /// compiler destructor glue ([`is_compiler_destructor_glue`]) or the FILE
    /// symbol of a C translation unit ([`is_object_file_symbol`]) — as
    /// `(family, count)`. Reported, not counted.
    pub glue_only: Vec<(String, usize)>,
}

// The declaration this module audits. Re-exported so a consumer that only wants the
// guard reaches the audit and the shape it takes through ONE path — there is no
// second `ServiceShape` here, this is `variant`'s.
pub use crate::variant::ServiceShape;

/// Symbol families a **thin** artifact must not contain — the server entry points it
/// claims to *reach* rather than embed, and the storage engine it would need in order
/// to become a second writer against the warehouse lock.
///
/// **Deliberately NOT "any symbol mentioning nornir", and that restraint is the whole
/// design.** A thin client legitimately links the thin gRPC CLIENT crates
/// (`nornir-funnel-embed`, `nornir-warehouse-embed`) and the dep-light edda leaves:
/// 2 346 such symbols in dwarves' thin UI are *correct*, as are the hundreds of
/// `holger_server_lib` symbols (`RemoteHolger`, the generated stubs, the shared view
/// types) in a thin holger UI. A guard that failed those would be wrong, would fail
/// every client there is, and would be switched off within a week. What is named here
/// is what only a SERVER needs.
///
/// An app with its own server entry points (holger's `FastRoutes` / `LocalHolger`)
/// adds them via [`LinkageAudit::of_families`] rather than growing this list —
/// per-app names do not belong in the shared constellation set.
pub const THIN_FORBIDDEN_SYMBOLS: &[&str] = &[
    // Server entry points.
    "nornir_server",
    "nornir::server",
    "dwarves::server",
    "tonic::transport::server",
    // The storage engine. A client that links these can open a warehouse in-process,
    // which is exactly the second-writer hazard the thin rule exists to prevent.
    "iceberg",
    "parquet",
    "datafusion",
];

/// **The outcome of a linkage audit — five distinct states, because four of them are
/// not "passed".**
///
/// The reason this is an enum and not a `bool` is [`NotMeasured`](Self::NotMeasured):
/// an audit that could not read the artifact must be tellable apart from one that
/// read it and found nothing, at every call site, forever.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkageVerdict {
    /// The artifact could not be read — absent, or not a parseable object file.
    /// **Never a pass**: an unchecked guarantee is not a guarantee.
    NotMeasured,
    /// Declared thin, and carries none of the forbidden families. The good case.
    ThinAndClean,
    /// Declared thin but measurably links a server entry point or the storage engine.
    /// The declaration is a lie.
    ThinButLinks,
    /// Declared fat/mixed and measurably embeds. Expected — a fat build is *supposed*
    /// to carry these.
    EmbeddedAndEmbeds,
    /// Declared fat/mixed but embeds nothing the audit can see. Not a lie about
    /// safety, but it makes the paired thin check vacuous, so it does not hold.
    EmbeddedButEmpty,
    /// [`ServiceShape::NotApplicable`] — the artifact declared out loud that the
    /// fat/thin axis does not apply to it (a `lib`, a browser demo with no wire).
    /// Holds, because it is a DECLARATION, not a silence: an omitted linkage reads
    /// as fat and is audited as fat.
    AxisNotApplicable,
}

impl LinkageVerdict {
    /// Whether the declaration survived contact with the binary.
    pub fn holds(self) -> bool {
        matches!(
            self,
            LinkageVerdict::ThinAndClean
                | LinkageVerdict::EmbeddedAndEmbeds
                | LinkageVerdict::AxisNotApplicable
        )
    }
}

/// What a linkage audit found in a built artifact.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkageAudit {
    /// The variant audited.
    pub variant: String,
    /// The binary.
    pub artifact: PathBuf,
    /// Its size.
    pub bytes: u64,
    /// The linkage the declaration claims.
    pub declared: ServiceShape,
    /// Forbidden symbol families actually PRESENT, as `(family, symbol count)`.
    /// Families with zero hits are not listed, so `found.len()` is the number of
    /// families present.
    ///
    /// The count is **non-glue symbols only** — see [`is_compiler_destructor_glue`].
    pub found: Vec<(String, usize)>,
    /// Families whose only matches were compiler destructor glue, as
    /// `(family, glue count)`. **Reported, never counted as linkage**, and it does
    /// not affect [`verdict`](Self::verdict): retained `drop_glue` for a type that is
    /// constructed nowhere is a codegen artifact, not an embedded server. Kept
    /// visible so the evidence is reclassified rather than discarded — if a family
    /// ever shows up here on an artifact you believe embeds, that is worth reading.
    pub glue_only: Vec<(String, usize)>,
    /// Whether the object parser could read the binary at all. `false` ⇒ UNMEASURED, which is
    /// never reported as a pass.
    pub measured: bool,
}

impl LinkageAudit {
    /// Audit `artifact` against the shared [`THIN_FORBIDDEN_SYMBOLS`].
    ///
    /// `None` when the artifact is ABSENT — a build that was never run is not an
    /// audit at all, and the caller decides whether that is "skip, not built" or a
    /// hard failure. Everything the audit *can* observe but finds wrong comes back as
    /// a `Some` with a non-holding [`verdict`](Self::verdict); in particular an
    /// artifact that exists but cannot be read is `Some` + [`LinkageVerdict::NotMeasured`].
    pub fn of(variant: &str, declared: ServiceShape, artifact: &Path) -> Option<LinkageAudit> {
        Self::of_families(variant, declared, artifact, THIN_FORBIDDEN_SYMBOLS)
    }

    /// [`of`](Self::of) with an app's OWN symbol families — the shared list plus, say,
    /// holger's `FastRoutes` / `LocalHolger` server entry points. Per-app names stay
    /// out of the constellation-wide constant; the contract is identical.
    pub fn of_families(
        variant: &str,
        declared: ServiceShape,
        artifact: &Path,
        families: &[&str],
    ) -> Option<LinkageAudit> {
        let bytes = std::fs::metadata(artifact).ok()?.len();
        // Read the symbol table straight out of the ELF with the pure-Rust `object`
        // parser and demangle each name with `rustc-demangle` — the in-process
        // equivalent of `nm -C`, with NO `nm` subprocess (LAW: symbol reading is
        // pure-Rust, mirroring `plugin_host`'s `object::File::parse`). `None` ⇒ the
        // artifact could not be parsed as an object file: UNMEASURED, never a pass.
        let symbols = demangled_symbols(artifact);
        let measured = symbols.is_some();
        let symbols = symbols.unwrap_or_default();
        // One symbol-table entry is one symbol, so this counts SYMBOLS rather than
        // substring occurrences — the same number a human reproduces with
        // `nm -C bin | grep -c` (families such as `nornir::server` only match once
        // the v0/legacy mangling is undone, which is why we demangle first), MINUS
        // compiler destructor glue, which `grep -c` cannot tell apart from code.
        let counts =
            if measured { count_families(&symbols, families) } else { FamilyCounts::default() };
        Some(LinkageAudit {
            variant: variant.to_string(),
            artifact: artifact.to_path_buf(),
            bytes,
            declared,
            found: counts.found,
            glue_only: counts.glue_only,
            measured,
        })
    }

    /// The verdict. See [`LinkageVerdict`] — "could not measure" is one of the
    /// outcomes, and it is not a pass.
    pub fn verdict(&self) -> LinkageVerdict {
        if !self.measured {
            return LinkageVerdict::NotMeasured;
        }
        match self.declared {
            ServiceShape::NotApplicable => LinkageVerdict::AxisNotApplicable,
            ServiceShape::ThinStandalone => {
                if self.found.is_empty() {
                    LinkageVerdict::ThinAndClean
                } else {
                    LinkageVerdict::ThinButLinks
                }
            }
            // Fat and Mixed both claim to compile siblings IN.
            ServiceShape::FatService | ServiceShape::Mixed => {
                if self.found.is_empty() {
                    LinkageVerdict::EmbeddedButEmpty
                } else {
                    LinkageVerdict::EmbeddedAndEmbeds
                }
            }
        }
    }

    /// Whether the declaration holds. An UNMEASURED audit never passes, and a fat
    /// variant that embeds nothing does not either — see [`LinkageVerdict`].
    pub fn holds(&self) -> bool {
        self.verdict().holds()
    }

    /// The sentence a report prints.
    pub fn line(&self) -> String {
        match self.verdict() {
            LinkageVerdict::NotMeasured => format!(
                "{}: linkage NOT MEASURED (no symbol table in {} — absent, not an object file, \
                 or STRIPPED) — an unchecked claim",
                self.variant,
                self.artifact.display()
            ),
            LinkageVerdict::AxisNotApplicable => format!(
                "{}: linkage not-applicable (declared) — {}",
                self.variant,
                human_bytes(self.bytes)
            ),
            LinkageVerdict::ThinAndClean => format!(
                "{}: declared THIN and carries no server/engine symbols — {} ✓",
                self.variant,
                human_bytes(self.bytes)
            ),
            LinkageVerdict::ThinButLinks => format!(
                "{}: declared THIN but LINKS {} — the declaration is a lie ({})",
                self.variant,
                self.families(),
                human_bytes(self.bytes)
            ),
            LinkageVerdict::EmbeddedAndEmbeds => format!(
                "{}: declared fat/mixed and embeds {} — {} ({} forbidden-family symbol \
                 group(s) present, which is expected)",
                self.variant,
                self.families(),
                human_bytes(self.bytes),
                self.found.len()
            ),
            LinkageVerdict::EmbeddedButEmpty => format!(
                "{}: declared fat/mixed but EMBEDS NOTHING — {}; whatever thin check this \
                 is paired with has become vacuous",
                self.variant,
                human_bytes(self.bytes)
            ),
        }
    }

    /// `iceberg×5642, parquet×2393` — the families present, for a message.
    pub fn families(&self) -> String {
        self.found.iter().map(|(s, n)| format!("{s}×{n}")).collect::<Vec<_>>().join(", ")
    }
}

/// `1024` → `"1.0 KB"`. Shared so a size in a build report reads the same everywhere.
pub fn human_bytes(n: u64) -> String {
    const K: f64 = 1024.0;
    let n = n as f64;
    if n < K {
        format!("{n:.0} B")
    } else if n < K * K {
        format!("{:.1} KB", n / K)
    } else if n < K * K * K {
        format!("{:.1} MB", n / (K * K))
    } else {
        format!("{:.2} GB", n / (K * K * K))
    }
}

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

    fn audit(declared: ServiceShape, found: Vec<(String, usize)>, measured: bool) -> LinkageAudit {
        LinkageAudit {
            variant: "x".into(),
            artifact: PathBuf::from("/nope"),
            bytes: 0,
            declared,
            found,
            // Glue-only families never affect the verdict, so every verdict test below
            // is written against the default (none) and stays about `found` alone.
            glue_only: Vec::new(),
            measured,
        }
    }

    /// **The property the whole module exists for.** An audit that could not read the
    /// binary is not a passing audit — it is its own outcome, and it says so out loud.
    #[test]
    fn an_unmeasured_linkage_audit_never_passes() {
        for shape in [
            ServiceShape::ThinStandalone,
            ServiceShape::FatService,
            ServiceShape::Mixed,
            ServiceShape::NotApplicable,
        ] {
            let a = audit(shape, Vec::new(), false);
            assert_eq!(a.verdict(), LinkageVerdict::NotMeasured, "{shape:?}");
            assert!(!a.holds(), "an unchecked claim is not a verified one ({shape:?})");
            assert!(a.line().contains("NOT MEASURED"), "{}", a.line());
        }
    }

    /// A thin declaration is failed by ANY forbidden family, and passes only on none.
    #[test]
    fn a_thin_declaration_is_failed_by_what_it_links() {
        let clean = audit(ServiceShape::ThinStandalone, Vec::new(), true);
        assert_eq!(clean.verdict(), LinkageVerdict::ThinAndClean);
        assert!(clean.holds());

        let lying = audit(ServiceShape::ThinStandalone, vec![("iceberg".into(), 42)], true);
        assert_eq!(lying.verdict(), LinkageVerdict::ThinButLinks);
        assert!(!lying.holds());
        assert!(lying.line().contains("the declaration is a lie"), "{}", lying.line());
        assert!(lying.line().contains("iceberg×42"), "{}", lying.line());
    }

    /// A fat variant is never failed FOR BEING FAT — the check is directional.
    #[test]
    fn a_fat_variant_is_not_failed_for_being_fat() {
        for shape in [ServiceShape::FatService, ServiceShape::Mixed] {
            let fat = audit(shape, vec![("iceberg".into(), 42)], true);
            assert_eq!(fat.verdict(), LinkageVerdict::EmbeddedAndEmbeds, "{shape:?}");
            assert!(fat.holds(), "{shape:?}");
            assert!(fat.line().contains("which is expected"), "{}", fat.line());
        }
    }

    /// …but it IS failed for not being fat. A guard that passes everything proves
    /// nothing: if the embedded variant stops embedding, the thin check it anchors is
    /// vacuous, and that must be loud rather than green.
    #[test]
    fn an_embedded_variant_that_embeds_nothing_makes_the_thin_check_vacuous() {
        let hollow = audit(ServiceShape::FatService, Vec::new(), true);
        assert_eq!(hollow.verdict(), LinkageVerdict::EmbeddedButEmpty);
        assert!(!hollow.holds());
        assert!(hollow.line().contains("vacuous"), "{}", hollow.line());
    }

    /// `not-applicable` is a DECLARATION and holds; an omitted linkage is not this —
    /// it deserializes as fat and is audited as fat.
    #[test]
    fn a_declared_inapplicable_axis_holds_but_silence_is_audited_as_fat() {
        let na = audit(ServiceShape::NotApplicable, Vec::new(), true);
        assert_eq!(na.verdict(), LinkageVerdict::AxisNotApplicable);
        assert!(na.holds());
        // Silence: `ServiceShape::default()` is fat, so an undeclared linkage is held
        // to the fat contract rather than waved through.
        assert_eq!(ServiceShape::default(), ServiceShape::FatService);
        assert!(!audit(ServiceShape::default(), Vec::new(), true).holds());
    }

    /// The forbidden list names server entry points and the storage engine — never a
    /// whole product. `nornir`/`holger` as bare words would fail every thin client
    /// there is (2 346 legitimate nornir symbols in dwarves' thin UI alone).
    #[test]
    fn the_forbidden_list_names_entry_points_not_products() {
        for s in THIN_FORBIDDEN_SYMBOLS {
            assert!(
                *s != "nornir" && *s != "holger" && *s != "dwarves" && *s != "skade",
                "'{s}' is a whole product, not a server entry point — a guard on it \
                 would fail every thin client and be switched off within a week"
            );
        }
        for must in ["nornir_server", "dwarves::server", "tonic::transport::server"] {
            assert!(THIN_FORBIDDEN_SYMBOLS.contains(&must), "{must} is a server entry point");
        }
        for engine in ["iceberg", "parquet", "datafusion"] {
            assert!(THIN_FORBIDDEN_SYMBOLS.contains(&engine), "{engine} is the storage engine");
        }
    }

    /// A missing artifact is `None` — no audit happened at all, which the caller must
    /// distinguish from one that ran.
    #[test]
    fn a_missing_artifact_is_no_audit_at_all() {
        assert!(
            LinkageAudit::of("x", ServiceShape::ThinStandalone, Path::new("/nonexistent-artifact"))
                .is_none()
        );
    }

    /// The real reader over a real file: the `object` parser on this test binary
    /// measures something, and an app's own families ride alongside the shared ones.
    #[test]
    fn object_reads_a_real_binary_and_extra_families_are_honoured() {
        let me = std::env::current_exe().expect("test binary path");
        let Some(a) = LinkageAudit::of_families(
            "self",
            ServiceShape::FatService,
            &me,
            &["nornir_build_thing", "definitely_not_a_symbol_xyzzy"],
        ) else {
            panic!("the running test binary exists");
        };
        if !a.measured {
            // The binary would not parse as an object file — the audit says NOT
            // MEASURED and does not pass.
            assert_eq!(a.verdict(), LinkageVerdict::NotMeasured);
            assert!(!a.holds());
            return;
        }
        assert!(a.bytes > 0);
        assert!(
            a.found.iter().any(|(f, n)| f == "nornir_build_thing" && *n > 0),
            "this crate's own symbols are in its test binary: {:?}",
            a.found
        );
        assert!(
            !a.found.iter().any(|(f, _)| f == "definitely_not_a_symbol_xyzzy"),
            "absent families are not listed"
        );
    }

    #[test]
    fn human_bytes_reads_like_a_size() {
        assert_eq!(human_bytes(512), "512 B");
        assert_eq!(human_bytes(2048), "2.0 KB");
        assert_eq!(human_bytes(5 * 1024 * 1024), "5.0 MB");
    }

    /// The REAL symbol that made holger's thin-client guard go red on 2026-08-01,
    /// copied verbatim out of `nm -C holger-server-ui`. Not a hand-written
    /// approximation: a fixture invented to match the predicate would prove only
    /// that the predicate matches its own fixture.
    const REAL_GLUE: &str = "core::ptr::drop_glue::<hyper_util::service::glue::TowerToHyperService<tower::util::map_request::MapRequest<tower::util::boxed_clone::BoxCloneService<http::request::Request<tonic::body::Body>, http::response::Response<tonic::body::Body>, alloc::boxed::Box<dyn core::error::Error + core::marker::Send + core::marker::Sync>>, <tonic::transport::server::Server>::serve_internal<tonic::service::router::Routes, tonic::transport::server::incoming::TcpIncoming, core::future::ready::Ready<()>, tokio::net::tcp::stream::TcpStream, std::io::error::Error, tonic::body::Body>::{closure#0}::{closure#1}>>>";

    /// A real non-glue symbol from the fat `gui-embedded` artifact's server path.
    const REAL_SERVER_CODE: &str =
        "<tonic::transport::server::Server>::serve_with_shutdown::<tonic::service::router::Routes, core::future::ready::Ready<()>>";

    #[test]
    fn a_rust_codegen_unit_file_symbol_is_a_translation_unit_and_not_linkage() {
        // The measured hit of 2026-09-15 (gunnar-server + grow-runtime, wireguard off).
        assert!(is_rust_cgu_file_symbol("wg_appliance_core.27049babafb37dc3-cgu.0"));
        assert!(is_rust_cgu_file_symbol("wg_appliance_core.27049babafb37dc3-cgu.12.rcgu.o"));
        assert!(is_object_file_symbol("wg_appliance_core.27049babafb37dc3-cgu.0"));
        // Real linkage of the family carries its path, and is still counted.
        assert!(!is_rust_cgu_file_symbol("wg_appliance_core::keys::generate_keypair"));
        assert!(!is_rust_cgu_file_symbol("wg_appliance_core.notahash-cgu.0"));
        assert!(!is_rust_cgu_file_symbol("wg_appliance_core.27049babafb37dc3-cgu."));
        let symbols: Vec<String> = ["wg_appliance_core.27049babafb37dc3-cgu.0", "<alloc::sync::Arc<std::sync::mpmc::context::Inner>>::drop_slow"].iter().map(|s| s.to_string()).collect();
        let counts = count_families(&symbols, &["wg_appliance_core"]);
        assert!(counts.found.is_empty(), "{counts:?}");
        assert_eq!(counts.glue_only, vec![("wg_appliance_core".to_string(), 1)]);
        let real: Vec<String> = vec!["wg_appliance_core::keys::generate_keypair".into()];
        assert_eq!(count_families(&real, &["wg_appliance_core"]).found, vec![("wg_appliance_core".to_string(), 1)]);
    }

    #[test]
    fn destructor_glue_is_recognised_and_ordinary_code_is_not() {
        assert!(is_compiler_destructor_glue(REAL_GLUE), "the real drop_glue symbol");
        assert!(is_compiler_destructor_glue(
            "core::ptr::drop_in_place::<tonic::transport::server::Server>"
        ));
        assert!(!is_compiler_destructor_glue(REAL_SERVER_CODE), "server code is not glue");
        // The word appearing in a NAME is not the same as being glue.
        assert!(!is_compiler_destructor_glue("holger_server_lib::drop_glue_helper"));
    }

    #[test]
    fn a_family_seen_only_as_destructor_glue_is_reported_but_not_counted() {
        let syms = vec![REAL_GLUE.to_string()];
        let c = count_families(&syms, &["tonic::transport::server"]);
        assert!(
            c.found.is_empty(),
            "glue alone is not linkage — this is the false red that failed holger's thin \
             `gui` tier on 2026-08-01, where the artifact carried 0 FastRoutes, 0 LocalHolger \
             and 0 iceberg/parquet symbols. found={:?}",
            c.found
        );
        assert_eq!(
            c.glue_only,
            vec![("tonic::transport::server".to_string(), 1)],
            "and it must still be REPORTED — reclassified, not discarded"
        );
    }

    #[test]
    fn real_server_code_is_still_caught_even_beside_glue() {
        let syms = vec![REAL_GLUE.to_string(), REAL_SERVER_CODE.to_string()];
        let c = count_families(&syms, &["tonic::transport::server"]);
        assert_eq!(
            c.found,
            vec![("tonic::transport::server".to_string(), 1)],
            "one NON-glue symbol; the glue beside it must not inflate the count"
        );
        assert!(c.glue_only.is_empty(), "the family is real linkage, so it is not glue-only");
    }

    /// The floor that stops this from becoming a way to pass by having no symbols.
    #[test]
    fn the_counter_is_not_vacuous_and_absence_is_distinguishable() {
        // Nothing in, nothing out — but the caller learns nothing was found, and
        // `of_families` separately records `measured: false` when it could not read
        // the file at all, so "clean" and "unread" never collapse.
        let c = count_families(&[], &["tonic::transport::server", "iceberg"]);
        assert!(c.found.is_empty() && c.glue_only.is_empty());
        // A family that is present as ordinary code is counted, so a non-empty
        // symbol list genuinely exercises the match.
        let c = count_families(&["iceberg::table::Table::scan".to_string()], &["iceberg"]);
        assert_eq!(c.found, vec![("iceberg".to_string(), 1)]);
    }
}