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

nordisk/gunnar

xtask/src/images.rs

//! `cargo xtask images`: prove the appliance images serve. It builds nothing.
//!
//! The appliance design opens with a claim: the Gunnar **server** is
//! built to run where there is no operating system under it, a single Rust
//! binary as PID 1 in a distroless image. Until something starts it somewhere
//! that is an argument, not a demonstration. The deliverable, in one line:
//! *"gunnar serves from a tunnr-style image"*.
//!
//! This task is what turns it into a fact, and the deliverable is **a working
//! `git clone`, not an image**. An image that builds and does not serve is worth
//! nothing, so nothing here reports success on a build.
//!
//! # ⚒ THE BUILD THING BUILDS. THIS MEASURES. There is no second builder.
//!
//! Until 2026-08-03 this file WAS a builder: it carried a Rust table of variants,
//! four Containerfiles under `containers/`, and a driver that shelled
//! `podman build` per variant. That was a second way to build gunnar's images
//! beside the one the constellation already has, with its own description of what
//! an appliance image is. Two descriptions of one thing is the twinning one-implementation
//! forbids. It is gone. The image is declared as data in
//! `.nornir/build-variants.toml` (`nornir-build-thing`'s `[[build.container]]`
//! schema).
//!
//! The one build door is dwarves' ⚒ Build Thing ▶ Build, on the `gunnar-image`
//! cell (`kind = "container"`, `container_ref = "gunnar-scratch"`).
//! `dwarves::assemble_cell` routes on `ArtifactKind`, builds the chained
//! `gunnar-server` cargo variant, stages its artifact and drives podman over the
//! REST API (bollard, zero-shell) — it materializes the Containerfile from the
//! declaration rather than reading one off disk. This file reads the SAME
//! declaration for the list of images, not a table of its own, and then measures
//! and proves them. If an image is missing it says so; it never produces one.
//!
//! ```text
//!   cargo xtask images              measure + prove every declared image
//!   cargo xtask images --keep       leave the containers and volumes running
//!   cargo xtask images --only NAME  one image (skips the red-first proofs)
//! ```
//!
//! # What is measured, and how
//!
//! | column | how |
//! |---|---|
//! | size | `podman image inspect --format {{.Size}}`, bytes on disk, uncompressed |
//! | files | non-directory entries in `podman export`'s tar, i.e. the real filesystem, not the base image's name |
//! | shell | the same tar, scanned for `sh`/`bash`/`dash`/`ash`/`busybox`/`ksh`/`zsh` |
//! | pkg | the same tar, scanned for `apk`/`apt`/`apt-get`/`dpkg`/`rpm`/`yum` |
//! | starts | the container is up AND the SSH port accepts a TCP connection |
//! | ready | wall time from `podman run -d` returning to that first accepted connection |
//! | rss | `podman stats --no-stream`, taken at rest after readiness |
//! | objs | objects in the clone's own object database, `git cat-file --batch-all-objects` |
//! | clone | stock `git push` then stock `git clone` over SSH, and the **object graph compared oid by oid** |
//!
//! The shell and package-manager scans read the exported filesystem rather than
//! trying to `exec` something: an image with no shell cannot be asked what it
//! contains, and "the exec failed" is shaped exactly like "the image is fine".
//!
//! # What "serves" means here, and why it is not the file bytes
//!
//! The clone probe used to compare `hello.txt`. That is applied output and it is
//! real, but it is one blob: a server that returned the right blob under a wrong
//! tree, or dropped an object the commit points at, would pass it. So the probe
//! now enumerates the object database on BOTH sides —
//! `git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype)
//! %(objectsize)'`, every object in the odb, in oid order — and requires the two
//! listings to be **identical, entry for entry**. The blob bytes are still
//! compared, because an identical graph of the wrong content is not a thing git
//! can produce but an identical graph of nothing is.
//!
//! ## The non-vacuity floor
//!
//! Two identical EMPTY listings compare equal. So the probe also refuses any graph
//! below [`MIN_OBJECTS`] — a commit, its tree and one blob, which is the least a
//! real push can produce. Without the floor a Gunnar serving an empty repository
//! passes: `git clone` exits 0, both sides enumerate nothing, and nothing is what
//! they agree on. `red_first` points the probe at exactly that case and requires
//! it to say no.
//!
//! # The guards are seen red before any green is believed
//!
//! Four reds, none optional, in [`red_first`]:
//!
//! * **no-shell**, against `docker.io/library/debian:trixie` — a stock base with
//!   `bash`, `dash`, `apt` and `dpkg` in it. It is scanned with the same
//!   [`export_contents`] + [`matching`] the candidate is, so a scan that matches
//!   nothing at all cannot pass this. gunnar used to declare a sixth
//!   `[[build.container]]` cell (`gunnar-fat`) that WAS this base with the real
//!   binary copied in; the cell is gone with the base-image comparison, and the
//!   fixture did not need to be a gunnar image — the property under test is a
//!   filesystem scan, and the base alone is red on it.
//! * **the clone probe reports failure at all**, against a container that is not
//!   Gunnar (`alpine:3` running `sleep 120` on the same published port).
//! * **the clone probe looks past the exit code**, against a real Gunnar serving
//!   an EMPTY repository, where `git clone` exits 0 and produces nothing. Only
//!   the floor above can say no to that.
//! * **the oid-by-oid comparison itself**, against a source given three objects
//!   AFTER the push — a clone that clears the floor and whose `hello.txt` is
//!   byte-correct, so nothing but the comparison can reject it. Without this the
//!   comparison's two sides have only ever agreed, which is `assert!(true)` with
//!   more steps.
//!
//! This repository already ships a guard
//! that asserted true things while the constraint it existed to protect was
//! broken, which is why none of the three is optional.
//!
//! # Why there are no crates in here
//!
//! `xtask` has zero dependencies on purpose (`xtask/Cargo.toml`): a workspace
//! member takes part in resolver-2 feature unification, so a dependency added
//! here could turn a feature on in a crate the appliance links, and the guard's
//! job is not to be the thing it guards against. Everything below is `std` plus
//! `podman`, `git`, `ssh-keygen` and `tar` as processes.

use std::fmt::Write as _;
use std::io::Write as _;
use std::net::{Shutdown, SocketAddr, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{Duration, Instant};

// ── the image list comes from the DECLARATION, never from a table in here ───

/// The declaration this file reads. It is the same file `nornir build --image`
/// reads, which is the point: one description of gunnar's images exists.
pub const DECLARATION: &str = ".nornir/build-variants.toml";

/// What this repository CLAIMS about one declared image.
///
/// The declaration says what is built; it has no field for *"this image contains
/// no shell"* and should not, because that is an assertion to be tested rather
/// than a build input. So the claims live here, keyed by the declared cell name,
/// and [`claims_for`] returns `None` for a name it does not know, which
/// [`load_variants`] turns into a hard error. A cell added to the declaration
/// with no claim beside it must NOT quietly become an image nothing asserts
/// anything about.
struct Claim {
    /// Whether this image claims to contain no shell.
    claims_no_shell: bool,
    /// What this base carries that Gunnar does not need, stated up front and
    /// then checked against the exported filesystem.
    freight: &'static str,
}

fn claims_for(cell: &str) -> Option<Claim> {
    match cell {
        "gunnar-scratch" => Some(Claim {
            claims_no_shell: true,
            freight: "nothing: the image is one file",
        }),
        _ => None,
    }
}

/// One declared image, as read out of [`DECLARATION`].
pub struct Variant {
    /// The `[[build.container]]` cell name, the handle a `kind = "container"`
    /// variant node's `container_ref` takes.
    pub name: String,
    /// The runtime base (`FROM`). `scratch` is the empty one.
    pub base: String,
    /// The image tag the declaration applies, which is what podman knows it by.
    pub tag: String,
    pub claims_no_shell: bool,
    pub expected_freight: &'static str,
}

/// Read every `[[build.container]]` cell out of the declaration.
///
/// A deliberately minimal scan rather than a TOML parser: `xtask` has zero
/// dependencies on purpose (see the module header), and the shape being read is
/// three string keys under a known table header. `nornir` reads its own
/// `.cargo/config.toml` `target-dir` the same way and for the same reason.
///
/// The empty result is an ERROR, not an empty run: a scan that silently found no
/// images would report "every image passed" having measured nothing, which is the
/// exact failure mode `export_contents` guards against downstream.
pub fn load_variants(root: &Path) -> Result<Vec<Variant>, String> {
    let path = root.join(DECLARATION);
    let text = std::fs::read_to_string(&path)
        .map_err(|e| format!("cannot read the image declaration {}: {e}", path.display()))?;

    struct Raw {
        name: Option<String>,
        base: Option<String>,
        tag: Option<String>,
    }
    let mut cells: Vec<Raw> = Vec::new();
    // `capturing` is cleared by ANY other table header, so the `name` of a
    // `[[build.variant]]` can never be mistaken for an image, and the
    // `[build.container.env]` / `.labels` / `.layers` sub-tables that follow a
    // cell contribute nothing.
    let mut capturing = false;
    for line in text.lines() {
        let line = line.trim();
        if line.starts_with("[[build.container]]") {
            cells.push(Raw {
                name: None,
                base: None,
                tag: None,
            });
            capturing = true;
            continue;
        }
        if line.starts_with('[') {
            capturing = false;
            continue;
        }
        if !capturing {
            continue;
        }
        let Some(cell) = cells.last_mut() else {
            continue;
        };
        if let Some(v) = quoted_value(line, "name") {
            cell.name.get_or_insert(v);
        } else if let Some(v) = quoted_value(line, "base") {
            cell.base.get_or_insert(v);
        } else if line.starts_with("tags") {
            // `tags = ["gunnar-scratch:1.1.3"]`, the first entry is the primary
            // reference, which is what `podman image inspect` is asked about.
            if let Some(first) = line.split('"').nth(1) {
                cell.tag.get_or_insert(first.to_string());
            }
        }
    }

    let mut out = Vec::new();
    for raw in cells {
        let name = raw
            .name
            .ok_or_else(|| format!("{DECLARATION}: a [[build.container]] cell has no name"))?;
        let base = raw
            .base
            .clone()
            .ok_or_else(|| format!("{DECLARATION}: container '{name}' declares no base"))?;
        let tag = raw
            .tag
            .clone()
            .ok_or_else(|| format!("{DECLARATION}: container '{name}' declares no tags"))?;
        let claim = claims_for(&name).ok_or_else(|| {
            format!(
                "{DECLARATION} declares the image '{name}', and this task asserts nothing about \
                 it. Add it to `claims_for`, whether it claims to have no shell and what its \
                 base carries that gunnar does not need. An image nobody claims anything about \
                 is an image no guard can fail."
            )
        })?;
        out.push(Variant {
            name,
            base,
            tag,
            claims_no_shell: claim.claims_no_shell,
            expected_freight: claim.freight,
        });
    }
    if out.is_empty() {
        return Err(format!(
            "{DECLARATION} declares no [[build.container]] cell, so there is nothing to measure \
            , and a run that measured nothing would have printed an empty table and passed"
        ));
    }
    Ok(out)
}

/// `key = "value"` → `value`, for the minimal scan above.
fn quoted_value(line: &str, key: &str) -> Option<String> {
    let rest = line.strip_prefix(key)?;
    let rest = rest.trim_start();
    let rest = rest.strip_prefix('=')?;
    let mut parts = rest.split('"');
    parts.next()?;
    parts.next().map(|s| s.to_string())
}

/// Names that mean "there is a shell in this image".
const SHELL_NAMES: &[&str] = &["sh", "bash", "dash", "ash", "busybox", "ksh", "zsh", "tcsh"];
/// Names that mean "this image can install more software".
const PKG_NAMES: &[&str] = &["apk", "apt", "apt-get", "dpkg", "rpm", "yum", "dnf", "opkg"];

// ── the measurement of one variant ──────────────────────────────────────────

pub struct Measured {
    pub name: String,
    pub tag: String,
    pub base: String,
    pub size_bytes: u64,
    pub files: usize,
    pub shells: Vec<String>,
    pub pkg_managers: Vec<String>,
    pub starts: bool,
    pub ready: Option<Duration>,
    pub rss: Option<String>,
    pub serves_clone: bool,
    /// How many objects the CLONE's own object database held. Reported as a real
    /// number rather than folded into `serves_clone`, so a run that passed the
    /// floor by exactly one object is visible instead of merely green.
    pub objects: usize,
    /// The oid-by-oid verdict, spelled out for the report.
    pub graph: String,
    pub expected_freight: &'static str,
    pub notes: Vec<String>,
}

impl Measured {
    /// The variant passed everything it claims.
    pub fn ok(&self, claims_no_shell: bool) -> bool {
        self.starts && self.serves_clone && !(claims_no_shell && !self.shells.is_empty())
    }
}

// ── entry point ─────────────────────────────────────────────────────────────

pub fn run(args: &[String]) -> Result<bool, String> {
    let keep = args.iter().any(|a| a == "--keep");
    let only = args
        .iter()
        .position(|a| a == "--only")
        .and_then(|i| args.get(i + 1))
        .cloned();

    let root = crate::guard::workspace_root();
    preflight(&root)?;

    let declared = load_variants(&root)?;
    eprintln!(
        "{DECLARATION} declares {} image(s): {}",
        declared.len(),
        declared
            .iter()
            .map(|v| v.name.as_str())
            .collect::<Vec<_>>()
            .join(", ")
    );

    let wanted: Vec<&Variant> = declared
        .iter()
        .filter(|v| only.as_deref().is_none_or(|n| n == v.name))
        .collect();
    if wanted.is_empty() {
        return Err(format!(
            "--only {} matched no declared image; known: {}",
            only.unwrap_or_default(),
            declared
                .iter()
                .map(|v| v.name.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        ));
    }

    // ── every image ─────────────────────────────────────────────────────────
    let mut measured = Vec::new();
    for v in &wanted {
        eprintln!("\n── {} ──", v.name);
        match measure(&root, v, keep) {
            Ok(m) => measured.push((*v, m)),
            Err(err) => return Err(format!("{}: {err}", v.name)),
        }
    }

    // ── RED FIRST ───────────────────────────────────────────────────────────────
    let reds = if only.is_none() {
        eprintln!("\n── the guards, seen red ──");
        Some(red_first(&root, &measured)?)
    } else {
        eprintln!("\nLAW 2 red-first proofs SKIPPED: --only measures a subset, and proof 3");
        eprintln!("needs a candidate that served, to point the floor at.");
        None
    };

    print!("{}", render(&measured, reds.as_deref()));

    let all_ok = measured.iter().all(|(v, m)| m.ok(v.claims_no_shell));
    let reds_ok = reds.as_ref().is_none_or(|r| r.iter().all(|p| p.fired));
    Ok(all_ok && reds_ok)
}

fn preflight(root: &Path) -> Result<(), String> {
    for (tool, args) in [
        ("podman", &["--version"][..]),
        ("git", &["--version"]),
        ("ssh-keygen", &["-?"]),
        ("tar", &["--version"]),
    ] {
        let out = Command::new(tool).args(args).output();
        let ok = match &out {
            // Output, not exit status: `ssh-keygen -?` exits non-zero and
            // prints its banner, and a tool that is absent produces an Err
            // here rather than an empty success.
            Ok(o) => !o.stdout.is_empty() || !o.stderr.is_empty(),
            Err(_) => false,
        };
        if !ok {
            return Err(format!(
                "`{tool}` is not usable, and every measurement below depends on it"
            ));
        }
    }
    if !root.join(DECLARATION).is_file() {
        return Err(format!(
            "{DECLARATION} is missing from the checkout, and it is where the images are declared"
        ));
    }
    Ok(())
}

// ── measure one variant ─────────────────────────────────────────────────────

fn measure(root: &Path, v: &Variant, keep: bool) -> Result<Measured, String> {
    // THE IMAGE IS AN INPUT. This task builds nothing; dwarves' ⚒ Build Thing
    // does, from the same declaration this list was read out of. A missing image
    // is reported with the door that produces it rather than quietly produced
    // here, because producing it here is what made this file a second builder.
    let tag = v.tag.clone();
    if image_size(&tag).is_err() {
        return Err(format!(
            "the declared image '{}' is not present as `{tag}`. Build it through the \
             constellation's one build door — dwarves ⚒ Build Thing ▶ Build on the \
             `kind = \"container\"` variant whose `container_ref` is '{}'.",
            v.name, v.name
        ));
    }

    let size_bytes = image_size(&tag)?;
    let contents = export_contents(root, &tag)?;
    let shells = matching(&contents, SHELL_NAMES);
    let pkg_managers = matching(&contents, PKG_NAMES);

    let mut notes = Vec::new();
    let run = Serving::start(root, &tag, &v.name)?;
    let starts = run.as_ref().map(|r| r.ready.is_some()).unwrap_or(false);
    let ready = run.as_ref().and_then(|r| r.ready);
    let rss = run.as_ref().and_then(|r| r.rss.clone());
    let mut serves_clone = false;
    let mut objects = 0usize;
    let mut graph = String::from("-");
    match &run {
        Some(r) if r.ready.is_some() => {
            match r.clone_probe("demo/hello", CONTENT, Probe::PushAndClone) {
                Ok(proof) => {
                    serves_clone = proof.served;
                    objects = proof.cloned.len();
                    graph = proof.detail.clone();
                    if !proof.served {
                        notes.push(proof.detail.clone());
                    }
                    // The graph itself, printed. A count is a summary; these are the
                    // oids that were compared, and they are what makes "identical"
                    // checkable by somebody reading the run rather than trusting it.
                    eprintln!("  object graph, oid by oid:");
                    for l in &proof.cloned {
                        let same = proof.pushed.as_ref().is_none_or(|p| p.contains(l));
                        eprintln!("    {} {l}", if same { "==" } else { "!!" });
                    }
                }
                Err(e) => {
                    notes.push(format!("clone failed: {e}"));
                    graph = format!("git failed: {}", first_line(&e));
                }
            }
        }
        Some(r) => notes.push(format!("never became ready: {}", r.why)),
        None => notes.push("the container did not start at all".to_string()),
    }
    if let Some(r) = run {
        if keep {
            eprintln!(
                "  --keep: container {} is still running, volume {}",
                r.container, r.volume
            );
        } else {
            r.tear_down();
        }
    }

    Ok(Measured {
        name: v.name.clone(),
        tag,
        base: v.base.clone(),
        size_bytes,
        files: contents.len(),
        shells,
        pkg_managers,
        starts,
        ready,
        rss,
        serves_clone,
        objects,
        graph,
        expected_freight: v.expected_freight,
        notes,
    })
}

fn image_size(tag: &str) -> Result<u64, String> {
    let out = podman(&["image", "inspect", "--format", "{{.Size}}", tag])?;
    let text = String::from_utf8_lossy(&out.stdout).trim().to_string();
    text.parse()
        .map_err(|_| format!("podman reported a size for {tag} that is not a number: {text:?}"))
}

/// Every path inside the image, read from `podman export`'s tar.
///
/// Not `podman run <image> ls`: an image with no shell and no coreutils cannot
/// be asked what it contains, and an `exec` that fails looks exactly like an
/// image that is clean. Exporting is the only reading that works identically on
/// `scratch` and on `debian:trixie`, which is what makes the column comparable.
fn export_contents(root: &Path, tag: &str) -> Result<Vec<String>, String> {
    let scratch = root.join("target/xtask-images");
    std::fs::create_dir_all(&scratch).map_err(|e| format!("{}: {e}", scratch.display()))?;
    let tarball = scratch.join(format!("{}.tar", tag.replace(['/', ':'], "_")));

    let out = podman(&["create", tag])?;
    let ctr = String::from_utf8_lossy(&out.stdout).trim().to_string();
    if ctr.is_empty() {
        return Err(format!("podman create {tag} printed no container id"));
    }
    let export = podman(&["export", "-o", tarball.to_str().unwrap(), &ctr]);
    let _ = podman(&["rm", "-f", &ctr]);
    export?;

    let listing = Command::new("tar")
        .arg("-tf")
        .arg(&tarball)
        .output()
        .map_err(|e| format!("could not run tar: {e}"))?;
    let _ = std::fs::remove_file(&tarball);
    if !listing.status.success() {
        return Err(format!(
            "tar could not read the exported {tag}: {}",
            String::from_utf8_lossy(&listing.stderr).trim()
        ));
    }
    // DIRECTORIES ARE DROPPED, and the trailing slash `tar` prints is the only
    // thing that distinguishes them, so it is read before it is trimmed, not
    // after. Trimming first made the scan report `pkg: YES` for
    // `gcr.io/distroless/static-debian12`, on the strength of the directories
    // `/etc/dpkg` and `/var/lib/dpkg`. That image ships dpkg's *metadata* and
    // no dpkg: there is no binary in it that can install anything, which is the
    // property the column claims to measure. Symlinks keep no trailing slash
    // and so survive, which matters, alpine's `/bin/sh` is a symlink to
    // busybox and must still be found.
    let paths: Vec<String> = String::from_utf8_lossy(&listing.stdout)
        .lines()
        .filter(|l| !l.ends_with('/'))
        .map(|l| l.to_string())
        .filter(|l| !l.is_empty())
        .collect();
    // The empty-graph trap, again: an export that produced nothing would make
    // every scan below report "no shell found" for the same reason a broken
    // `cargo tree` invocation once made the dependency guard report "clean".
    if paths.is_empty() {
        return Err(format!(
            "the exported filesystem of {tag} is EMPTY, every content scan \
             below would have reported a clean image without reading anything"
        ));
    }
    Ok(paths)
}

/// Paths whose file name is one of `names`, deduplicated.
fn matching(paths: &[String], names: &[&str]) -> Vec<String> {
    let mut hits: Vec<String> = paths
        .iter()
        .filter(|p| {
            p.rsplit('/')
                .next()
                .is_some_and(|base| names.contains(&base))
        })
        .map(|p| format!("/{}", p.trim_start_matches("./").trim_start_matches('/')))
        .collect();
    hits.sort();
    hits.dedup();
    hits
}

// ── a running container, and the clone that is the actual deliverable ───────

const CONTENT: &str = "gunnar served this from a container\n";

/// **The non-vacuity floor.** The fewest objects a real push can put in a
/// repository: one blob, the tree that names it, the commit that points at the
/// tree. A clone holding fewer than this did not receive a history, whatever
/// `git clone` exited with, and no oid-by-oid comparison below can say so —
/// two empty listings are equal. `red_first` proof 3 is this line seen red.
pub const MIN_OBJECTS: usize = 3;

/// One repository's OBJECT DATABASE, enumerated: one `oid type size` per line,
/// in oid order, for every object the odb holds.
///
/// `--batch-all-objects` and not `rev-list --objects`: the question is what the
/// server actually DELIVERED, not what is reachable from the refs it advertised.
/// An object dropped in transit is the failure being looked for, and a reachable-
/// set walk of a clone that is missing one cannot enumerate past the hole.
///
/// EMPTY IS AN ERROR at the call site, never here: `git` on a repository with no
/// objects exits 0 and prints nothing, which is shaped exactly like a clean read.
fn object_graph(dir: &Path, ssh: &str) -> Result<Vec<String>, String> {
    let out = git(
        dir,
        ssh,
        &[
            "cat-file",
            "--batch-all-objects",
            "--batch-check=%(objectname) %(objecttype) %(objectsize)",
        ],
    )?;
    Ok(out
        .lines()
        .map(str::trim)
        .filter(|l| !l.is_empty())
        .map(str::to_string)
        .collect())
}

/// **What the probe does before it clones**, as a named case rather than a bool.
///
/// The comment on [`Serving::clone_probe`] records why this is not inferred from
/// the arguments: a guard whose behaviour is chosen by comparing one of its
/// arguments to a constant is a guard nobody can point anywhere. It is an enum
/// and not a `push: bool` for the same reason one step further out — the third
/// case is a push, so a boolean could not name it without lying about one of the
/// two that push.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Probe {
    /// Push a commit, clone it back, compare the two graphs. The deliverable.
    PushAndClone,
    /// Clone only. No source side, so the graph comparison does not run and the
    /// floor is the whole verdict. What the red-first probes use.
    CloneOnly,
    /// Push a commit, then commit a SECOND file that is never pushed, so the
    /// source graph is a strict superset of anything the server can return.
    ///
    /// This exists to see the graph comparison RED. Every other call has the two
    /// sides agreeing, and a comparison that has only ever been run against
    /// inputs it accepts is indistinguishable from `assert!(true)` — the exact
    /// defect this file's own header warns about. The failure it stands in for is
    /// real: a server that acknowledges a push and returns fewer objects than it
    /// was given. The floor cannot catch that (the clone holds 3, well over
    /// MIN_OBJECTS) and the byte comparison cannot either (hello.txt is there and
    /// correct). Only the oid-by-oid comparison can.
    PushThenOrphan,
}

/// What one push-and-clone round trip proved, as data rather than a bool.
///
/// `served` is the verdict; the rest is why, and it is carried out so the report
/// can print a real object count instead of a green tick. A `false` with 3
/// objects and a `false` with 0 are different failures.
pub struct CloneProof {
    /// The whole claim: the graphs matched, the floor was cleared, and the blob
    /// bytes are what was written.
    pub served: bool,
    /// The source side's graph, when this probe pushed one. `None` for the red-first
    /// proofs, which push nothing and have no source to compare against.
    pub pushed: Option<Vec<String>>,
    /// The clone's graph, always read.
    pub cloned: Vec<String>,
    /// One line naming which of the three checks decided the verdict.
    pub detail: String,
}

pub struct Serving {
    pub container: String,
    pub volume: String,
    pub port: u16,
    pub key: PathBuf,
    pub ready: Option<Duration>,
    pub rss: Option<String>,
    pub why: String,
    work: PathBuf,
}

impl Serving {
    /// Bootstrap a data volume through the image's OWN verbs, then serve.
    ///
    /// The bootstrap runs as separate `podman run` invocations because the
    /// Arrow ref/secret log takes an `O_EXCL` lock at open
    /// while the server holds the store lock, a CLI verb cannot run
    /// beside a live `gunnar serve` on the same data root. Sequential
    /// containers give the lock what it needs for free.
    fn start(root: &Path, tag: &str, name: &str) -> Result<Option<Serving>, String> {
        let work = root.join("target/xtask-images").join(name);
        let _ = std::fs::remove_dir_all(&work);
        std::fs::create_dir_all(&work).map_err(|e| format!("{}: {e}", work.display()))?;

        // A client key. `ssh-keygen` is a TEST tool here, not a Gunnar
        // subprocess, the zero-shell rule is about what the server does, and
        // xtask already asserts this binary's presence in `preflight`.
        let key = work.join("id_ed25519");
        let kg = Command::new("ssh-keygen")
            .args([
                "-q",
                "-t",
                "ed25519",
                "-N",
                "",
                "-C",
                "gunnar-xtask-images",
                "-f",
            ])
            .arg(&key)
            .output()
            .map_err(|e| format!("ssh-keygen: {e}"))?;
        if !key.exists() {
            return Err(format!(
                "ssh-keygen produced no key: {}",
                String::from_utf8_lossy(&kg.stderr).trim()
            ));
        }

        let volume = format!("gunnar-xtask-{name}");
        let _ = podman(&["volume", "rm", "-f", &volume]);
        podman(&["volume", "create", &volume])?;

        let mount = format!("{volume}:/data");
        let keymount = format!("{}:/bootstrap:ro,z", work.display());
        let bootstrap: &[&[&str]] = &[
            &["namespace", "create", "demo"],
            // sha1 so a stock `git clone` needs no object-format negotiation.
            // D11 makes sha256 the default and the sha256 path is proven by
            // `crates/gunnar-cli/tests/ssh_end_to_end.rs`; this task is about
            // the IMAGE, and pinning one hash keeps the variants comparable.
            &["repo", "create", "demo/hello", "--hash", "sha1", "--public"],
            // The empty repository the red-first proof clones from: a clone
            // that exits zero and produces nothing.
            &["repo", "create", "demo/empty", "--hash", "sha1", "--public"],
            &["principal", "add", "tester"],
            &["key", "add", "tester", "@/bootstrap/id_ed25519.pub"],
            &["grant", "tester", "write", "--repo", "demo/hello"],
        ];
        for verbs in bootstrap {
            let mut args = vec!["run", "--rm", "-v", &mount, "-v", &keymount, tag];
            args.extend_from_slice(verbs);
            let out = podman(&args).map_err(|e| format!("bootstrap `{}`: {e}", verbs.join(" ")))?;
            let _ = out;
        }

        // Publish on an ephemeral loopback port and ask podman which one it
        // chose, rather than guessing a free port and racing another job.
        let out = podman(&[
            "run",
            "-d",
            "-v",
            &mount,
            "-p",
            "127.0.0.1::2222",
            "--name",
            &format!("gunnar-xtask-{name}"),
            tag,
            "serve",
            "--no-grpc",
        ])?;
        let container = String::from_utf8_lossy(&out.stdout).trim().to_string();
        if container.is_empty() {
            return Ok(None);
        }
        let started = Instant::now();

        let port = match podman(&["port", &container, "2222/tcp"]) {
            Ok(p) => String::from_utf8_lossy(&p.stdout)
                .lines()
                .next()
                .and_then(|l| l.rsplit(':').next())
                .and_then(|p| p.trim().parse::<u16>().ok()),
            Err(_) => None,
        };
        let Some(port) = port else {
            let why = logs(&container);
            let _ = podman(&["rm", "-f", &container]);
            let _ = podman(&["volume", "rm", "-f", &volume]);
            return Err(format!("podman published no port for {tag}: {why}"));
        };

        let addr: SocketAddr = ([127, 0, 0, 1], port).into();
        let mut ready = None;
        let deadline = Instant::now() + Duration::from_secs(30);
        while Instant::now() < deadline {
            if let Ok(s) = TcpStream::connect_timeout(&addr, Duration::from_millis(250)) {
                let _ = s.shutdown(Shutdown::Both);
                ready = Some(started.elapsed());
                break;
            }
            if !running(&container) {
                break;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        let why = if ready.is_none() {
            logs(&container)
        } else {
            String::new()
        };
        // At rest: readiness has happened and nothing has connected yet.
        let rss = ready.and(rss_of(&container));

        Ok(Some(Serving {
            container,
            volume,
            port,
            key,
            ready,
            rss,
            why,
            work,
        }))
    }

    /// **The deliverable.** Stock `git` pushes over SSH, stock `git` clones back,
    /// and the two OBJECT GRAPHS are compared oid by oid.
    ///
    /// Three checks decide the verdict, and each exists because the one before it
    /// can be passed by a server that served nothing:
    ///
    /// 1. **the floor.** `cloned.len() >= `[`MIN_OBJECTS`]. A clone of an empty
    ///    repository exits zero and leaves an empty directory, so an exit-status
    ///    check reports success for a server that serves nothing — and so does an
    ///    oid-by-oid comparison of two empty listings.
    /// 2. **the graph.** Every object in the source odb and every object in the
    ///    clone's odb, `oid type size`, in oid order, compared entry for entry.
    ///    Only run when this probe pushed: the red-first proofs have no source side.
    /// 3. **the bytes.** `hello.txt` read back off the clone's worktree. An
    ///    identical graph of the wrong content is not something git can produce,
    ///    but the read costs nothing and it is the one check that touches the
    ///    checkout rather than the odb.
    ///
    /// [`Probe`] is an explicit argument and not inferred from `content`. It was
    /// inferred once, the push ran whenever `content == CONTENT`, and that is
    /// precisely what made the third red-first proof unable to fire: pointing the
    /// probe at an EMPTY repository with the normal expected bytes also asked
    /// it to push into that repository, the push was refused for want of a
    /// grant, and the run reported *"git itself failed, so the byte comparison
    /// was never reached"* instead of the red it exists to produce. A guard
    /// whose behaviour is chosen by comparing one of its arguments to a
    /// constant is a guard nobody can point anywhere.
    pub fn clone_probe(
        &self,
        repo: &str,
        content: &str,
        probe: Probe,
    ) -> Result<CloneProof, String> {
        let ssh = format!(
            "ssh -i {} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no \
             -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR",
            self.key.display()
        );
        let url = format!("ssh://git@127.0.0.1:{}/{repo}.git", self.port);

        let mut pushed = None;
        if probe != Probe::CloneOnly {
            let src = self.work.join("src");
            let _ = std::fs::remove_dir_all(&src);
            std::fs::create_dir_all(&src).map_err(|e| format!("{}: {e}", src.display()))?;
            git(&src, &ssh, &["init", "--quiet", "--object-format=sha1"])?;
            git(
                &src,
                &ssh,
                &["config", "user.email", "xtask@gunnar.invalid"],
            )?;
            git(&src, &ssh, &["config", "user.name", "gunnar xtask"])?;
            let mut f = std::fs::File::create(src.join("hello.txt"))
                .map_err(|e| format!("hello.txt: {e}"))?;
            f.write_all(content.as_bytes())
                .map_err(|e| format!("hello.txt: {e}"))?;
            drop(f);
            git(&src, &ssh, &["add", "hello.txt"])?;
            git(
                &src,
                &ssh,
                &["commit", "--quiet", "-m", "served from a container"],
            )?;
            git(
                &src,
                &ssh,
                &["push", "--quiet", &url, "HEAD:refs/heads/main"],
            )?;
            if probe == Probe::PushThenOrphan {
                // AFTER the push, so the server never sees these three objects
                // and cannot return them. A blob, the tree that names it and the
                // commit: the source graph becomes a strict superset of the
                // clone's, which is what the comparison must notice.
                std::fs::write(src.join("orphan.txt"), b"never pushed\n")
                    .map_err(|e| format!("orphan.txt: {e}"))?;
                git(&src, &ssh, &["add", "orphan.txt"])?;
                git(&src, &ssh, &["commit", "--quiet", "-m", "orphan"])?;
            }
            // Read the SOURCE graph after the push, not before: what is being
            // compared is what the server was given against what it gave back.
            let g = object_graph(&src, &ssh)?;
            if g.len() < MIN_OBJECTS {
                // Not a verdict — a broken probe. The source is built three
                // lines above out of one file and one commit; fewer than three
                // objects here means the `git` calls above did nothing, and the
                // comparison would then be between two things that are both
                // empty for the same reason.
                return Err(format!(
                    "the probe's OWN source repository holds {} object(s), below the floor of \
                     {MIN_OBJECTS}. Nothing was pushed, so nothing could be compared.",
                    g.len()
                ));
            }
            pushed = Some(g);
        }

        let dest = self.work.join(format!("clone-{}", repo.replace('/', "-")));
        let _ = std::fs::remove_dir_all(&dest);
        git(
            &self.work,
            &ssh,
            &["clone", "--quiet", &url, dest.to_str().unwrap()],
        )?;

        let cloned = object_graph(&dest, &ssh)?;

        // 1 — the floor. Before any comparison, because two empty listings agree.
        if cloned.len() < MIN_OBJECTS {
            return Ok(CloneProof {
                served: false,
                pushed,
                detail: format!(
                    "the clone's object database holds {} object(s), below the floor of \
                     {MIN_OBJECTS}; `git clone` exited 0 and served no history",
                    cloned.len()
                ),
                cloned,
            });
        }

        // 2 — the graph, oid by oid.
        if let Some(src_graph) = &pushed {
            if src_graph != &cloned {
                let mut missing: Vec<&str> = src_graph
                    .iter()
                    .filter(|l| !cloned.contains(l))
                    .map(String::as_str)
                    .collect();
                let extra: Vec<&str> = cloned
                    .iter()
                    .filter(|l| !src_graph.contains(l))
                    .map(String::as_str)
                    .collect();
                missing.truncate(4);
                return Ok(CloneProof {
                    served: false,
                    detail: format!(
                        "the object graphs differ: {} pushed, {} cloned, {} missing from the \
                         clone [{}], {} it did not receive from us",
                        src_graph.len(),
                        cloned.len(),
                        src_graph.iter().filter(|l| !cloned.contains(l)).count(),
                        missing.join(" | "),
                        extra.len()
                    ),
                    pushed,
                    cloned,
                });
            }
        }

        // 3 — the bytes on the checked-out worktree.
        let bytes_ok = std::fs::read_to_string(dest.join("hello.txt"))
            .map(|got| got == content)
            .unwrap_or(false);
        let n = cloned.len();
        Ok(CloneProof {
            served: bytes_ok,
            detail: if bytes_ok {
                match &pushed {
                    Some(_) => format!("{n} objects, identical oid for oid, and hello.txt matches"),
                    None => format!("{n} objects cleared the floor and hello.txt matches"),
                }
            } else {
                "the object graph matched and hello.txt did not".to_string()
            },
            pushed,
            cloned,
        })
    }

    fn tear_down(self) {
        let _ = podman(&["rm", "-f", &self.container]);
        let _ = podman(&["volume", "rm", "-f", &self.volume]);
        let _ = std::fs::remove_dir_all(&self.work);
    }
}

fn git(dir: &Path, ssh: &str, args: &[&str]) -> Result<String, String> {
    let out = Command::new("git")
        .current_dir(dir)
        .env("GIT_SSH_COMMAND", ssh)
        .env("GIT_TERMINAL_PROMPT", "0")
        .args(args)
        .output()
        .map_err(|e| format!("git {}: {e}", args.join(" ")))?;
    if !out.status.success() {
        return Err(format!(
            "git {} failed: {}",
            args.join(" "),
            String::from_utf8_lossy(&out.stderr).trim()
        ));
    }
    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}

fn running(container: &str) -> bool {
    podman(&["inspect", "--format", "{{.State.Running}}", container])
        .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "true")
        .unwrap_or(false)
}

fn logs(container: &str) -> String {
    let out = Command::new("podman").args(["logs", container]).output();
    match out {
        Ok(o) => {
            let mut s = String::from_utf8_lossy(&o.stderr).trim().to_string();
            if s.is_empty() {
                s = String::from_utf8_lossy(&o.stdout).trim().to_string();
            }
            s.lines().rev().take(4).collect::<Vec<_>>().join(" / ")
        }
        Err(e) => format!("(no logs: {e})"),
    }
}

fn rss_of(container: &str) -> Option<String> {
    let out = podman(&[
        "stats",
        "--no-stream",
        "--format",
        "{{.MemUsage}}",
        container,
    ])
    .ok()?;
    let text = String::from_utf8_lossy(&out.stdout);
    let first = text.lines().find(|l| !l.trim().is_empty())?;
    Some(first.split('/').next()?.trim().to_string())
}

fn podman(args: &[&str]) -> Result<Output, String> {
    let out = Command::new("podman")
        .args(args)
        .output()
        .map_err(|e| format!("podman {}: {e}", args.join(" ")))?;
    if !out.status.success() {
        return Err(format!(
            "podman {} failed: {}",
            args.join(" "),
            String::from_utf8_lossy(&out.stderr).trim()
        ));
    }
    Ok(out)
}

// ── the guards, seen red ─────────────────────────────────────────────

pub struct RedProof {
    pub what: &'static str,
    /// A `String` rather than a `&'static str`: two of the three proofs name the
    /// variant they ran against, which is only known at run time.
    pub against: String,
    pub fired: bool,
    pub detail: String,
}

/// Four reds, and none of them optional.
///
/// A guard that has only ever been run against inputs it accepts is
/// indistinguishable from a guard that accepts everything.
fn red_first(root: &Path, measured: &[(&Variant, Measured)]) -> Result<Vec<RedProof>, String> {
    let mut proofs = Vec::new();

    // ── 1, the no-shell scan, against an image that is nothing but shell ────────
    //
    // A STOCK BASE, not a gunnar cell. gunnar used to declare a sixth
    // `[[build.container]]` — `gunnar-fat`, a full `debian:trixie` with the real
    // binary copied in — purely so this proof had something to point at, and the
    // cell went with the base-image comparison it belonged to. Nothing was lost:
    // the property under test is a filesystem scan for shells and package
    // managers, `debian:trixie` is red on both, and copying a 15 MB binary into it
    // never contributed to that. It is scanned by the SAME `export_contents` +
    // `matching` the candidate is, which is what makes it evidence about the real
    // scan and not about a second one written for the occasion.
    const SHELLFUL: &str = "docker.io/library/debian:trixie";
    if image_size(SHELLFUL).is_err() {
        // Pull rather than skip. A red-first proof that quietly does not run is
        // the failure this whole section exists to prevent.
        eprintln!("  pulling {SHELLFUL} for the no-shell red proof …");
        podman(&["pull", "-q", SHELLFUL])
            .map_err(|e| format!("cannot obtain the shellful fixture {SHELLFUL}: {e}"))?;
    }
    let fat = export_contents(root, SHELLFUL)?;
    let fat_shells = matching(&fat, SHELL_NAMES);
    let fat_pkg = matching(&fat, PKG_NAMES);
    let fired = !fat_shells.is_empty() && !fat_pkg.is_empty();
    proofs.push(RedProof {
        what: "no-shell / no-package-manager scan rejects a base that has both",
        against: format!("{SHELLFUL}, {} entries in `podman export`", fat.len()),
        fired,
        detail: if fired {
            format!(
                "found shells [{}] and package managers [{}]",
                fat_shells.join(" "),
                fat_pkg.join(" ")
            )
        } else {
            format!(
                "found {} shell(s) and {} package manager(s) in an image that ships bash, dash, \
                 apt and dpkg — the scan is blind",
                fat_shells.len(),
                fat_pkg.len()
            )
        },
    });

    // ── 2, the clone probe, against something that is not a git server at all ──
    //     This is the probe proving it CAN report failure.
    let dead = "gunnar-xtask-red-nothing";
    let _ = podman(&["rm", "-f", dead]);
    let spun = podman(&[
        "run",
        "-d",
        "--name",
        dead,
        "-p",
        "127.0.0.1::2222",
        "docker.io/library/alpine:3",
        "sleep",
        "120",
    ]);
    match spun {
        Ok(out) => {
            let ctr = String::from_utf8_lossy(&out.stdout).trim().to_string();
            let port = podman(&["port", &ctr, "2222/tcp"]).ok().and_then(|p| {
                String::from_utf8_lossy(&p.stdout)
                    .lines()
                    .next()
                    .and_then(|l| l.rsplit(':').next())
                    .and_then(|s| s.trim().parse::<u16>().ok())
            });
            let mut detail = "could not read the published port".to_string();
            let mut fired = false;
            if let Some(port) = port {
                let work = root.join("target/xtask-images/red-nothing");
                let _ = std::fs::remove_dir_all(&work);
                let _ = std::fs::create_dir_all(&work);
                // A REAL key, generated here. This proof borrowed the `scratch`
                // variant's key once, and `scratch` had already been torn down
                // by the time it ran, so ssh refused on *"Identity file … not
                // accessible"* and the proof went red without the probe ever
                // reaching the container. That is a red for the wrong reason,
                // which is the same defect as a green for the wrong reason: it
                // says nothing about whether the clone probe can tell gunnar
                // from `sleep 120`.
                let key = work.join("id_ed25519");
                let kg = Command::new("ssh-keygen")
                    .args([
                        "-q",
                        "-t",
                        "ed25519",
                        "-N",
                        "",
                        "-C",
                        "gunnar-xtask-red",
                        "-f",
                    ])
                    .arg(&key)
                    .output()
                    .map_err(|e| format!("ssh-keygen: {e}"))?;
                if !key.exists() {
                    return Err(format!(
                        "ssh-keygen produced no key for the red-nothing proof, so it could \
                         only have failed on the missing file: {}",
                        String::from_utf8_lossy(&kg.stderr).trim()
                    ));
                }
                let probe = Serving {
                    container: ctr.clone(),
                    volume: String::new(),
                    port,
                    key,
                    ready: None,
                    rss: None,
                    why: String::new(),
                    work: work.clone(),
                };
                match probe.clone_probe("demo/hello", CONTENT, Probe::CloneOnly) {
                    Ok(p) if p.served => {
                        detail = format!(
                            "the clone probe SUCCEEDED against `sleep 120` with {} object(s)",
                            p.cloned.len()
                        )
                    }
                    Ok(p) => {
                        fired = true;
                        detail = format!("the probe said no: {}", p.detail);
                    }
                    Err(e) => {
                        fired = true;
                        detail = format!("git refused: {}", first_line(&e));
                    }
                }
                let _ = std::fs::remove_dir_all(&work);
            }
            let _ = podman(&["rm", "-f", &ctr]);
            proofs.push(RedProof {
                what: "clone probe rejects a container that is not gunnar",
                against: "alpine:3 running `sleep 120` on the same published port".to_string(),
                fired,
                detail,
            });
        }
        Err(e) => {
            proofs.push(RedProof {
                what: "clone probe rejects a container that is not gunnar",
                against: "alpine:3 running `sleep 120`".to_string(),
                fired: false,
                detail: format!("the fixture could not be started: {e}"),
            });
        }
    }

    // ── 3, the FLOOR, against a REAL gunnar serving an EMPTY repository ────────
    //     `git clone` exits ZERO here and the two object graphs are equal, because
    //     both are empty. Only `MIN_OBJECTS` can say no, which is the difference
    //     between this guard and both a status check and a naive diff.
    let candidate = measured
        .iter()
        .find(|(_, m)| m.serves_clone)
        .map(|(v, _)| *v);
    match candidate {
        Some(v) => {
            // THE DECLARED TAG, read off the variant. This line used to build a
            // name — `format!("localhost/gunnar-{}:dev", v.name)` — which for a
            // cell already called `gunnar-scratch` produced
            // `localhost/gunnar-gunnar-scratch:dev`. No such image exists, so the
            // fixture server could never start and the proof could only ever
            // report "never became ready": a guard that cannot fire, in the
            // function whose whole job is proving guards fire.
            match Serving::start(root, &v.tag, "red-empty")? {
                Some(server) if server.ready.is_some() => {
                    // `push: false` is the whole proof. The repository must
                    // STAY empty: what is under test is that a `git clone`
                    // which exits 0 and produces nothing is still rejected,
                    // and the expected bytes are the same `CONTENT` every
                    // other call expects, so the comparison is the identical
                    // one, only the server's answer differs.
                    let r = server.clone_probe("demo/empty", CONTENT, Probe::CloneOnly);
                    let (fired, detail) = match r {
                        Ok(p) if p.served => (
                            false,
                            format!(
                                "the probe reported the empty repository as SERVED ({} objects)",
                                p.cloned.len()
                            ),
                        ),
                        Ok(p) => (
                            // The red must be the FLOOR's red, not any red. A
                            // probe that said no because git errored would prove
                            // it can fail, not that it looks past exit code 0.
                            p.cloned.is_empty(),
                            format!(
                                "git clone exited 0 and the probe still said no: {}",
                                p.detail
                            ),
                        ),
                        Err(e) => (
                            false,
                            format!(
                                "git itself failed, so the floor was never reached: {}",
                                first_line(&e)
                            ),
                        ),
                    };
                    server.tear_down();
                    proofs.push(RedProof {
                        what: "clone probe looks past the exit code (the non-vacuity floor)",
                        against: format!("{} serving demo/empty, `git clone` exits 0", v.name),
                        fired,
                        detail,
                    });
                }
                other => {
                    if let Some(s) = other {
                        s.tear_down();
                    }
                    proofs.push(RedProof {
                        what: "clone probe looks past the exit code (the non-vacuity floor)",
                        against: "an empty repository on a real gunnar".to_string(),
                        fired: false,
                        detail: "the fixture server never became ready".to_string(),
                    });
                }
            }
        }
        None => proofs.push(RedProof {
            what: "clone probe looks past the exit code (the non-vacuity floor)",
            against: "an empty repository on a real gunnar".to_string(),
            fired: false,
            detail: "no variant served a clone, so there was nothing to point it at".to_string(),
        }),
    }

    // ── 4, the GRAPH COMPARISON itself, seen red ──────────────────────────────
    //     The three proofs above exercise the scan, the probe's ability to fail,
    //     and the floor. NOT the oid-by-oid comparison: in every green run its two
    //     sides agree, and a comparison that has only ever seen inputs it accepts
    //     is indistinguishable from `assert!(true)`. So the source is given three
    //     objects the server was never sent, AFTER the push. The clone is a
    //     perfectly good 3-object clone — it clears the floor and hello.txt is
    //     byte-correct — so neither of the other two checks can say no. Only the
    //     comparison can.
    match candidate {
        Some(v) => match Serving::start(root, &v.tag, "red-orphan")? {
            Some(server) if server.ready.is_some() => {
                let r = server.clone_probe("demo/hello", CONTENT, Probe::PushThenOrphan);
                let (fired, detail) = match r {
                    Ok(p) if p.served => (
                        false,
                        format!(
                            "the probe reported {} pushed / {} cloned as SERVED",
                            p.pushed.as_ref().map(Vec::len).unwrap_or(0),
                            p.cloned.len()
                        ),
                    ),
                    Ok(p) => {
                        // The red must be the COMPARISON's red. A clone that came
                        // back under the floor, or wrong bytes, would also be a
                        // `served: false` and would prove nothing about the graph.
                        let over_floor = p.cloned.len() >= MIN_OBJECTS;
                        let differ = p
                            .pushed
                            .as_ref()
                            .is_some_and(|src| src.len() > p.cloned.len());
                        (over_floor && differ, p.detail)
                    }
                    Err(e) => (
                        false,
                        format!(
                            "git itself failed, so the comparison was never reached: {}",
                            first_line(&e)
                        ),
                    ),
                };
                server.tear_down();
                proofs.push(RedProof {
                    what: "the oid-by-oid comparison rejects a graph that is short an object",
                    against: format!(
                        "{}: three objects committed to the source AFTER the push, so the \
                         server cannot return them",
                        v.name
                    ),
                    fired,
                    detail,
                });
            }
            other => {
                if let Some(s) = other {
                    s.tear_down();
                }
                proofs.push(RedProof {
                    what: "the oid-by-oid comparison rejects a graph that is short an object",
                    against: "a real gunnar with an orphaned source commit".to_string(),
                    fired: false,
                    detail: "the fixture server never became ready".to_string(),
                });
            }
        },
        None => proofs.push(RedProof {
            what: "the oid-by-oid comparison rejects a graph that is short an object",
            against: "a real gunnar with an orphaned source commit".to_string(),
            fired: false,
            detail: "no variant served a clone, so there was nothing to point it at".to_string(),
        }),
    }

    Ok(proofs)
}

fn first_line(s: &str) -> String {
    s.lines().next().unwrap_or("").trim().to_string()
}

// ── the report ──────────────────────────────────────────────────────────────

/// Bytes, grouped, and NOT rounded to MB. `15.1 MB` is the same string for
/// 15,066,088 and for 15,083,583 bytes, and those are two different images: the
/// size column is a measurement of the artifact, so it prints the measurement.
fn bytes(n: u64) -> String {
    let d = n.to_string();
    let mut out = String::new();
    for (i, c) in d.chars().enumerate() {
        if i > 0 && (d.len() - i) % 3 == 0 {
            out.push(' ');
        }
        out.push(c);
    }
    out
}

pub fn render(measured: &[(&Variant, Measured)], reds: Option<&[RedProof]>) -> String {
    let mut s = String::new();
    let _ = writeln!(s, "\n══ the appliance image ══\n");
    // No `libc` column any more, and its absence is the finding it recorded.
    // The images used to differ in libc as well as in base, so the size column
    // compared two different binaries on five floors. Collapsing every cell onto
    // the one static `gunnar-server` artifact is what made the column mean
    // anything — and then made the five extra floors not worth declaring.
    let _ = writeln!(
        s,
        "{:<20} {:>11} {:>7} {:>6} {:>6} {:>8} {:>9} {:>6} {:>7}",
        "image", "size", "files", "shell", "pkg", "starts", "ready", "objs", "clone"
    );
    let _ = writeln!(s, "{}", "-".repeat(92));
    for (v, m) in measured {
        let _ = writeln!(
            s,
            "{:<20} {:>11} {:>7} {:>6} {:>6} {:>8} {:>9} {:>6} {:>7}",
            m.name,
            bytes(m.size_bytes),
            m.files,
            if m.shells.is_empty() { "none" } else { "YES" },
            if m.pkg_managers.is_empty() {
                "none"
            } else {
                "YES"
            },
            if m.starts { "yes" } else { "NO" },
            m.ready
                .map(|d| format!("{} ms", d.as_millis()))
                .unwrap_or_else(|| "-".into()),
            m.objects,
            if m.serves_clone { "yes" } else { "NO" },
        );
        let _ = v;
    }
    let _ = writeln!(s);
    for (v, m) in measured {
        let _ = writeln!(s, "  {} — {}", m.name, m.base);
        let _ = writeln!(s, "      rss at rest : {}", m.rss.as_deref().unwrap_or("-"));
        let _ = writeln!(s, "      carries     : {}", m.expected_freight);
        if !m.shells.is_empty() {
            let _ = writeln!(s, "      shells      : {}", m.shells.join(" "));
        }
        if !m.pkg_managers.is_empty() {
            let _ = writeln!(s, "      pkg manager : {}", m.pkg_managers.join(" "));
        }
        if v.claims_no_shell && !m.shells.is_empty() {
            let _ = writeln!(
                s,
                "      ⛔ CLAIMS no shell and HAS one, the claim is false"
            );
        }
        let _ = writeln!(s, "      graph       : {}", m.graph);
        for n in &m.notes {
            let _ = writeln!(s, "      note        : {n}");
        }
        let _ = writeln!(s);
    }

    if let Some(reds) = reds {
        let _ = writeln!(s, "══ the guards, seen red ══\n");
        for p in reds {
            let _ = writeln!(
                s,
                "  [{}] {}\n        against: {}\n        {}",
                if p.fired {
                    "RED, as required"
                } else {
                    "DID NOT FIRE"
                },
                p.what,
                p.against,
                p.detail
            );
        }
        let _ = writeln!(s);
    }
    s
}

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

    /// A scratch checkout holding one declaration, removed on drop.
    struct Scratch(PathBuf);

    impl Scratch {
        fn with_declaration(name: &str, text: &str) -> Scratch {
            let root =
                std::env::temp_dir().join(format!("gunnar-images-{name}-{}", std::process::id()));
            let _ = std::fs::remove_dir_all(&root);
            std::fs::create_dir_all(root.join(".nornir")).unwrap();
            std::fs::write(root.join(DECLARATION), text).unwrap();
            Scratch(root)
        }
    }

    impl Drop for Scratch {
        fn drop(&mut self) {
            let _ = std::fs::remove_dir_all(&self.0);
        }
    }

    fn measured(name: &str, shells: &[&str], starts: bool, serves: bool) -> Measured {
        Measured {
            name: name.to_string(),
            tag: format!("{name}:1"),
            base: "scratch".to_string(),
            size_bytes: 15_066_088,
            files: 1,
            shells: shells.iter().map(|s| s.to_string()).collect(),
            pkg_managers: Vec::new(),
            starts,
            ready: starts.then(|| Duration::from_millis(12)),
            rss: None,
            serves_clone: serves,
            objects: if serves { 3 } else { 0 },
            graph: "3 objects, oid for oid".to_string(),
            expected_freight: "nothing: the image is one file",
            notes: vec!["a note".to_string()],
        }
    }

    fn variant(name: &str, claims_no_shell: bool) -> Variant {
        Variant {
            name: name.to_string(),
            base: "scratch".to_string(),
            tag: format!("{name}:1"),
            claims_no_shell,
            expected_freight: "nothing: the image is one file",
        }
    }

    /// The repository's own declaration is what the task measures: one image,
    /// claimed shell-free, and a `[[build.variant]]`'s `name` is not taken for one.
    #[test]
    fn the_declared_image_is_read_out_of_the_declaration() {
        let v = load_variants(&crate::guard::workspace_root()).expect("the declaration loads");
        let names: Vec<&str> = v.iter().map(|v| v.name.as_str()).collect();
        assert_eq!(names, ["gunnar-scratch"]);
        assert_eq!(v[0].base, "scratch");
        assert!(v[0].tag.starts_with("gunnar-scratch:"), "{}", v[0].tag);
        assert!(v[0].claims_no_shell);
    }

    /// The scan takes the first value of each key inside a cell and nothing from
    /// the sub-tables that follow it.
    #[test]
    fn a_sub_table_cannot_rename_an_image() {
        let s = Scratch::with_declaration(
            "subtable",
            "[[build.variant]]\nname = \"not-an-image\"\n\n\
             [[build.container]]\nname = \"gunnar-scratch\"\nbase = \"scratch\"\n\
             tags = [\"gunnar-scratch:9.9.9\", \"gunnar-scratch:latest\"]\n\
             [build.container.env]\nname = \"impostor\"\nbase = \"debian\"\n",
        );
        let v = load_variants(&s.0).unwrap();
        assert_eq!(v.len(), 1);
        assert_eq!(
            (v[0].name.as_str(), v[0].base.as_str(), v[0].tag.as_str()),
            ("gunnar-scratch", "scratch", "gunnar-scratch:9.9.9")
        );
    }

    /// Every incomplete cell is refused by what it lacks, and so is a declaration
    /// that declares nothing: a run over no image must never print a passing table.
    #[test]
    fn an_incomplete_or_empty_declaration_is_refused_by_name() {
        let cases = [
            ("noname", "[[build.container]]\nbase = \"scratch\"\n", "has no name"),
            ("nobase", "[[build.container]]\nname = \"gunnar-scratch\"\ntags = [\"t:1\"]\n", "declares no base"),
            ("notags", "[[build.container]]\nname = \"gunnar-scratch\"\nbase = \"scratch\"\n", "declares no tags"),
            ("unclaimed", "[[build.container]]\nname = \"gunnar-debian\"\nbase = \"debian\"\ntags = [\"d:1\"]\n", "asserts nothing about"),
            ("empty", "[[build.variant]]\nname = \"gunnar-server\"\n", "declares no [[build.container]]"),
        ];
        for (name, text, want) in cases {
            let s = Scratch::with_declaration(name, text);
            match load_variants(&s.0) {
                Ok(v) => panic!("{name}: accepted {} image(s)", v.len()),
                Err(e) => assert!(e.contains(want), "{name}: {e}"),
            }
        }
        let missing =
            std::env::temp_dir().join(format!("gunnar-images-none-{}", std::process::id()));
        let e = load_variants(&missing)
            .err()
            .expect("no declaration at all");
        assert!(e.contains("cannot read the image declaration"), "{e}");
    }

    #[test]
    fn a_quoted_value_belongs_to_its_own_key_only() {
        assert_eq!(
            quoted_value("name = \"gunnar\"", "name").as_deref(),
            Some("gunnar")
        );
        assert_eq!(
            quoted_value("name=\"tight\"", "name").as_deref(),
            Some("tight")
        );
        assert_eq!(quoted_value("names = \"other\"", "name"), None);
        assert_eq!(quoted_value("base = \"scratch\"", "name"), None);
        assert_eq!(quoted_value("name = unquoted", "name"), None);
    }

    /// A shell is found by its file name wherever it sits, once, and a name that
    /// only contains one is not a shell.
    #[test]
    fn a_shell_is_matched_by_its_file_name() {
        let paths: Vec<String> = [
            "./bin/sh",
            "bin/sh",
            "usr/bin/bash",
            "usr/share/doc/bash-completion",
            "etc/zshrc",
            "usr/bin/apt",
        ]
        .iter()
        .map(|s| s.to_string())
        .collect();
        assert_eq!(matching(&paths, SHELL_NAMES), ["/bin/sh", "/usr/bin/bash"]);
        assert_eq!(matching(&paths, PKG_NAMES), ["/usr/bin/apt"]);
        assert!(matching(&[], SHELL_NAMES).is_empty());
    }

    #[test]
    fn sizes_are_printed_exactly_in_groups_of_three() {
        assert_eq!(bytes(0), "0");
        assert_eq!(bytes(999), "999");
        assert_eq!(bytes(1000), "1 000");
        assert_eq!(bytes(15_066_088), "15 066 088");
        assert_eq!(bytes(15_083_583), "15 083 583");
        assert_eq!(first_line("  one  \ntwo"), "one");
        assert_eq!(first_line(""), "");
    }

    /// The verdict: an image passes only if it starts, serves the clone, and a
    /// shell-free claim holds.
    #[test]
    fn a_false_no_shell_claim_fails_the_image() {
        assert!(measured("a", &[], true, true).ok(true));
        assert!(
            !measured("a", &["/bin/sh"], true, true).ok(true),
            "the claim is false"
        );
        assert!(
            measured("a", &["/bin/sh"], true, true).ok(false),
            "nothing was claimed"
        );
        assert!(
            !measured("a", &[], false, true).ok(true),
            "it never started"
        );
        assert!(
            !measured("a", &[], true, false).ok(true),
            "it served no clone"
        );
    }

    /// The report names what failed, row by row, and prints the red-first proofs
    /// with whether each one fired.
    #[test]
    fn the_report_names_a_false_claim_and_a_guard_that_did_not_fire() {
        let good = variant("gunnar-scratch", true);
        let bad = variant("gunnar-liar", true);
        let rows = vec![
            (&good, measured("gunnar-scratch", &[], true, true)),
            (&bad, measured("gunnar-liar", &["/bin/sh"], false, false)),
        ];
        let reds = vec![
            RedProof {
                what: "a shell in a no-shell image",
                against: "gunnar-liar".to_string(),
                fired: true,
                detail: "found /bin/sh".to_string(),
            },
            RedProof {
                what: "the object floor",
                against: "gunnar-scratch".to_string(),
                fired: false,
                detail: "3 objects".to_string(),
            },
        ];
        let page = render(&rows, Some(&reds));
        assert!(page.contains("15 066 088"), "{page}");
        assert!(page.contains("12 ms"), "{page}");
        assert!(page.contains("CLAIMS no shell and HAS one"), "{page}");
        assert!(page.contains("shells      : /bin/sh"), "{page}");
        assert!(page.contains("note        : a note"), "{page}");
        assert!(
            page.contains("[RED, as required] a shell in a no-shell image"),
            "{page}"
        );
        assert!(page.contains("[DID NOT FIRE] the object floor"), "{page}");
        let liar = page
            .lines()
            .find(|l| l.starts_with("gunnar-liar"))
            .expect("its row");
        assert!(liar.contains("YES") && liar.contains("NO"), "{liar}");
        let quiet = render(&rows[..1], None);
        assert!(!quiet.contains("the guards, seen red"), "{quiet}");
        assert!(!quiet.contains("CLAIMS no shell"), "{quiet}");
    }
}