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

nordisk/gunnar

xtask/src/cycle_svg.rs

//! `cargo xtask svg` — the write/read cycle through Arrow IPC and znippy, drawn
//! from a **data model of verified edges** rather than by hand.
//!
//! # Why a generator and not a drawing
//!
//! A hand-drawn diagram of a wiring question rots the moment the wiring changes,
//! and it rots *silently* — the picture keeps rendering. The whole value of this
//! diagram is the distinction between a mechanism that **exists** and a
//! mechanism that is **wired**, so the one thing it must never do is state an
//! intended edge as a real one.
//!
//! So every box and every arrow below is a `const` with an [`Evidence`]
//! attached: the file and line where the edge was read, and a [`Wiring`] verdict
//! that decides how it is drawn. An edge with no consumer is drawn dashed and
//! labelled, and there is no way to draw it solid without changing the datum.
//! `xtask/tests/cycle_svg.rs` asserts the rendering actually differs between the
//! two — a legend that says "dashed means unwired" while both render solid is
//! exactly the silent green this repository keeps finding.
//!
//! # Style
//!
//! The `skade` circuit-board house style (`../skade/.nornir/assets/*.svg`):
//! 1180 wide, `#0b1c2e`→`#06111d` board gradient, `chip`/`rail`/`slot`
//! gradients, DejaVu Sans, cyan `#6fd3f5` signal path, coral `#f0a9a0` for the
//! bad half of a contrast. Those files are hand-written; this one is not, which
//! is the only intentional departure.
//!
//! # Zero dependencies
//!
//! `xtask` has none by construction (see `xtask/Cargo.toml`), so the SVG is
//! emitted as text from `std` alone.

use std::fmt::Write as _;

// ════════════════════════════════════════════════════════════════════════════
// The data model — every claim the diagram makes, with where it was read
// ════════════════════════════════════════════════════════════════════════════

/// Where a claim was verified. `file` is repo-relative; `line` is the line that
/// carries the code or the comment the claim rests on.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Evidence {
    pub file: &'static str,
    pub line: u32,
    /// A token the cited line must contain, so the citation can be **opened**
    /// rather than merely rendered.
    ///
    /// Empty means "not checkable here", and the only citations allowed to
    /// leave it empty are the ones into [`FOREIGN`] — another repository, which
    /// this one cannot open. Everything else is held to it by
    /// [`assert_cites`], because `line > 0` cannot fail on a citation that has
    /// drifted: the number stays a number while it points at a closing brace,
    /// and **five of these had done exactly that**, found by reading on
    /// 2026-08-05 and listed in [`assert_cites`]'s own guard.
    pub anchor: &'static str,
}

impl Evidence {
    pub const fn at(file: &'static str, line: u32) -> Self {
        Self {
            file,
            line,
            anchor: "",
        }
    }

    /// The token the cited line has to contain. See [`Evidence::anchor`].
    pub const fn naming(mut self, anchor: &'static str) -> Self {
        self.anchor = anchor;
        self
    }
    pub fn label(&self) -> String {
        format!("{}:{}", short_path(self.file), self.line)
    }

    /// A label that keeps the crate. [`label`](Self::label) trims to the last
    /// two components, which turns `crates/gunnar-wire/src/bitmap.rs` and
    /// `crates/gunnar-store/src/bitmap.rs` into the same string — and a manual
    /// whose whole point is that the code can be opened from the picture must
    /// not print an address that resolves to two files. This one drops the
    /// `crates/gunnar-` prefix and the `/src` segment instead, so the crate
    /// survives at roughly the same width.
    pub fn crate_label(&self) -> String {
        let p = self
            .file
            .strip_prefix("crates/gunnar-")
            .unwrap_or(self.file);
        let p = p.replacen("/src/", "/", 1);
        format!("{p}:{}", self.line)
    }
}

/// Trim a repo-relative path to its last two components so a `file:line` fits
/// in a 200px chip without becoming unrecognisable.
fn short_path(p: &str) -> &str {
    match p.rfind('/') {
        Some(i) => match p[..i].rfind('/') {
            Some(j) => &p[j + 1..],
            None => p,
        },
        None => p,
    }
}

/// The verdict that decides how an edge is drawn.
///
/// The three are genuinely different facts and the diagram must not collapse
/// them, because the fix for each is different: `Unwired` needs a call site,
/// `NotEmitted` needs a producer, and only `Wired` needs nothing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Wiring {
    /// Producer and consumer both exist and are reachable from a shipped path.
    Wired,
    /// The bytes are written, and nothing outside tests and benches reads them.
    Unwired,
    /// The section is never produced on this path at all — a stronger fact than
    /// "unread", and a different bug.
    NotEmitted,
}

impl Wiring {
    /// Stroke for a wire in this state.
    pub const fn stroke(self) -> &'static str {
        match self {
            Wiring::Wired => "#6fd3f5",
            Wiring::Unwired => "#e0a86a",
            Wiring::NotEmitted => "#f0a9a0",
        }
    }
    /// Dash pattern. `None` renders solid — only `Wired` gets that.
    pub const fn dash(self) -> Option<&'static str> {
        match self {
            Wiring::Wired => None,
            Wiring::Unwired => Some("7 5"),
            Wiring::NotEmitted => Some("3 4"),
        }
    }
}

/// One of the five reserved sections, as the archive actually carries it.
pub struct Section {
    pub module: &'static str,
    /// `Arrow IPC stream` or `raw bytes` — reserved either way.
    pub payload: &'static str,
    /// The real columns / the real header fields.
    pub fields: &'static [(&'static str, &'static str)],
    /// Where the bytes are built.
    pub built_at: Evidence,
    /// Where gunnar produces it on a shipped path, if it does.
    pub produced_by: Option<Evidence>,
    /// Where gunnar reads it back on a shipped path, if it does.
    pub consumed_by: Option<Evidence>,
    pub wiring: Wiring,
    /// One line on what the structure IS, or what stands in for it. Not a
    /// verdict: the stroke carries that, and spelling it out beside the box
    /// says the same thing twice in two registers.
    pub note: &'static str,
}

/// The five, in the order the archive lays them out.
///
/// Read 2026-08-04 against `znippy` @ `znippy-plugin-git` and `gunnar` @
/// `master`. Every `Evidence` below was opened; none is inferred from a doc.
pub const SECTIONS: &[Section] = &[
    Section {
        module: "__gunnar_refs__",
        payload: "Arrow IPC · push log",
        fields: &[
            ("name", "Utf8"),
            ("target", "Utf8? null=del"),
            ("peeled", "Utf8?"),
            ("symref_target", "Utf8?"),
            ("push_seq", "UInt64 order"),
            ("updated_ms", "UInt64 wall"),
        ],
        built_at: Evidence::at("plugins/native/znippy-plugin-git/src/refs.rs", 93),
        // No producer citation, and that is the measurement rather than a gap in
        // it: `refs_reserved_builder` went out with the crate that held it on
        // 2026-08-10 and NOTHING in the tree builds this section today. A
        // citation pointing at where it used to live was worse than none — it
        // named a file that will not open, so the guard that checks every
        // citation could only ever be red.
        produced_by: None,
        // No `.naming(…)`: this citation crosses into znippy, and the anchor
        // check can only read files in THIS repository. An anchor it cannot
        // verify is a claim nothing tests, which is the shape of guard this
        // file exists to refuse.
        consumed_by: Some(Evidence::at(
            "plugins/native/znippy-plugin-git/src/refs.rs",
            237,
        )),
        wiring: Wiring::Unwired,
        note: "read back by znippy's git plugin; nothing in any server build \
               writes this section",
    },
    Section {
        module: "__gunnar_secrets__",
        payload: "Arrow IPC · same log",
        fields: &[
            ("name", "Utf8"),
            ("ciphertext", "Binary? sealed"),
            ("recipient", "Utf8? label"),
            ("push_seq", "UInt64"),
            ("updated_ms", "UInt64"),
        ],
        built_at: Evidence::at("plugins/native/znippy-plugin-git/src/secrets.rs", 89),
        produced_by: None,
        consumed_by: None,
        wiring: Wiring::NotEmitted,
        note: "FoldRequest carries no secrets, so the archive section has no \
               input; the store is sealed separately at stores.rs:279",
    },
    Section {
        module: "__gunnar_oid__",
        payload: "raw · stree keyspace",
        fields: &[
            ("magic/ver", "ZNPYGOID v2"),
            ("keys[n]", "i64 oid[..8]"),
            ("rows[n]", "u64 lookup"),
            ("ords[n]", "u32 ordinal"),
            ("oids[n]", "verify step"),
            ("", "8B prefix is a FILTER"),
        ],
        built_at: Evidence::at("plugins/native/znippy-plugin-git/src/oid_index.rs", 79),
        produced_by: None,
        consumed_by: None,
        wiring: Wiring::NotEmitted,
        note: "pack_tier returns an empty section list; the pack's own .idx is \
               the oid index, O(log n) over a 256-bucket fanout",
    },
    Section {
        module: "__gunnar_graph__",
        payload: "Arrow IPC · commit graph",
        fields: &[
            ("oid", "Utf8"),
            ("parents", "List<Utf8>"),
            ("tree", "Utf8?"),
            ("committer_time", "Int64? s"),
            ("generation", "UInt32 gen"),
            ("", "gen = 1 + max(parents)"),
        ],
        built_at: Evidence::at("plugins/native/znippy-plugin-git/src/graph.rs", 47),
        produced_by: None,
        consumed_by: None,
        wiring: Wiring::NotEmitted,
        note: "gunnar's ancestry walk is O(commits) object reads instead",
    },
    Section {
        module: "__gunnar_reach__",
        payload: "Arrow IPC · roaring bitmaps",
        fields: &[
            ("commit_oid", "Utf8"),
            ("cardinality", "UInt64"),
            ("bitmap", "Binary  roaring"),
            ("", "over object ORDINALS"),
            ("", "want−have = ANDNOT"),
            ("", "policy: 512 tips + stride"),
        ],
        built_at: Evidence::at("plugins/native/znippy-plugin-git/src/reach.rs", 65),
        produced_by: None,
        consumed_by: None,
        wiring: Wiring::NotEmitted,
        note: "gunnar answers want−have from a git .bitmap over object \
               ordinals, O(words); this section is a second, unused encoding",
    },
];

/// One box on an operation lane.
pub struct Stage {
    pub label: &'static str,
    pub sub: &'static str,
    /// The wire *leaving* this stage. The last stage's is ignored.
    pub out: Wiring,
    pub ev: Option<Evidence>,
}

impl Stage {
    pub const fn new(label: &'static str, sub: &'static str, ev: Evidence) -> Self {
        Self {
            label,
            sub,
            out: Wiring::Wired,
            ev: Some(ev),
        }
    }
    pub const fn edge(mut self, w: Wiring) -> Self {
        self.out = w;
        self
    }
    pub const fn bare(label: &'static str, sub: &'static str) -> Self {
        Self {
            label,
            sub,
            out: Wiring::Wired,
            ev: None,
        }
    }
}

/// One git operation, drawn left to right: bytes in on the left, out on the right.
pub struct Lane {
    pub op: &'static str,
    pub what: &'static str,
    /// What enters, and from where.
    pub bytes_in: &'static str,
    /// What leaves, and to where.
    pub bytes_out: &'static str,
    pub stages: &'static [Stage],
    /// The one thing about this path a box diagram would get wrong.
    pub note: &'static str,
    pub note_wiring: Wiring,
}

/// The four operations, each traced end to end.
///
/// Read 2026-08-04 against `gunnar` @ `master`. Every `Evidence` is a line that
/// was opened; the `Wiring` on each edge is a grep for call sites, not a reading
/// of the doc comment above it — several of the doc comments here describe an
/// edge that the resolved graph does not contain.
pub const LANES: &[Lane] = &[
    // ── push ────────────────────────────────────────────────────────────────
    Lane {
        op: "git push",
        what: "receive-pack",
        bytes_in: "pkt-line cmds + PACK, ssh/http",
        bytes_out: "pack-<id>.pack/.idx + ref CAS",
        stages: &[
            Stage::new("pkt-line", "cmd list, sideband", Evidence::at("crates/gunnar-server/src/transport.rs", 1469).naming("fn serve_receive_pack")),
            Stage::new("serve_session", "<old> <new> <ref>", Evidence::at("crates/gunnar-wire/src/receive_pack.rs", 905).naming("fn serve_session")),
            Stage::new("index_thin_pack", "O(bytes), SHA-1-bound", Evidence::at("edda/crates/gunnar-pack/src/read.rs", 138)),
            Stage::new("gate", "connectivity, O(new objects)", Evidence::at("crates/gunnar-wire/src/receive_pack.rs", 1609).naming("verify_connectivity")),
            Stage::new("ref CAS", "MustExistAndMatch(old)", Evidence::at("edda/crates/storage-git-gix/src/lib.rs", 859)),
            Stage::new("push refs", "Arrow batch, one push_seq", Evidence::at("plugins/native/znippy-plugin-git/src/refs.rs", 130))
                .edge(Wiring::NotEmitted),
            Stage::new("__gunnar_refs__", "one RecordBatch per push", Evidence::at("plugins/native/znippy-plugin-git/src/refs.rs", 93)),
        ],
        note: "The live ref transaction lands in gix-ref and arrow_log. znippy's RefLog is constructed inside a fold (the old znippy engine crate/src/lib.rs:186) from a ref snapshot pushed as one batch, so a sealed __gunnar_refs__ carries one frame holding the current namespace rather than one frame per push.",
        note_wiring: Wiring::NotEmitted,
    },
    // ── fetch / clone ───────────────────────────────────────────────────────
    Lane {
        op: "git fetch / clone",
        what: "upload-pack",
        bytes_in: "ls-refs, want/have",
        bytes_out: "PACK on the wire",
        stages: &[
            Stage::new("advertise", "v2 caps, ls-refs", Evidence::at("crates/gunnar-wire/src/upload_pack.rs", 155).naming("fn advertise")),
            Stage::new("negotiate", "haves/wants, HaveMemo", Evidence::at("crates/gunnar-wire/src/negotiate.rs", 321).naming("fn negotiate")),
            Stage::new("select_objects", "want−have, bitmap or walk", Evidence::at("crates/gunnar-wire/src/graph.rs", 2315).naming("fn select_objects")),
            Stage::new("objects.packed()", "the pack-copy seam on ObjectStore", Evidence::at("plugins/native/znippy-plugin-git/src/git_ops.rs", 1141)),
            Stage::new("write_pack_copy", "byte-range copy, O(bytes)", Evidence::at("edda/crates/gunnar-pack/src/copy.rs", 1145)),
            Stage::new("send_pack", "sideband out", Evidence::at("crates/gunnar-wire/src/upload_pack.rs", 1810).naming("fn send_pack")),
        ],
        note: "The copy branch is taken whenever the store has packs (upload_pack.rs:1396); its applied output is objects_copied_from_pack against objects_recompressed. Selection is answered from a git .bitmap when one is present and falls back to a closure walk when it is not — the archive's own reach encoding is a second representation of the same set and is not the one gunnar reads.",
        note_wiring: Wiring::Wired,
    },
    // ── gc / repack ─────────────────────────────────────────────────────────
    Lane {
        op: "gc / repack",
        what: "fold, then re-seal",
        bytes_in: "N packs + loose objects",
        bytes_out: "1 consolidated pack",
        stages: &[
            Stage::new("next_generation", "names repository.gN+1.znippy", Evidence::at("plugins/native/znippy-plugin-git/src/gc.rs", 326)),
            Stage::new("hard_link", "second name, same inode", Evidence::at("plugins/native/znippy-plugin-git/src/gc.rs", 214)),
            Stage::new("compact", "rewrite the work copy", Evidence::at("plugins/native/znippy-plugin-git/src/gc.rs", 214)),
            Stage::new("read back", "every entry, blake3 checked", Evidence::at("plugins/native/znippy-plugin-git/src/gc.rs", 358)),
            Stage::new("sync_dir", "rename DURABLE, not just visible", Evidence::at("plugins/native/znippy-plugin-git/src/gc.rs", 412)),
            Stage::new("maintain", "the server's only gc verb", Evidence::at("crates/gunnar-server/src/storage/znippy_engine.rs", 470).naming("pub fn maintain")),
        ],
        // ── REWRITTEN 2026-08-13. This lane described the PACKFILE repacker ──
        //
        // Every stage in it cited `repack.rs` — `snapshot_packs`,
        // `write_pack_file`, `verify_pack_holds`, `tier.fold` — and that file and
        // those functions went out with the packfile engine on 2026-08-10. The
        // citations could not be opened, so the guard could only be red, and the
        // picture was drawing a durability argument about code nobody runs.
        //
        // What is here now is the path that exists: znippy's generation swap.
        // It is in another repository, so the stages carry no anchor.
        note: "One consolidated ARCHIVE, not one consolidated pack: the packfile repacker was amputated 2026-08-10 and `maintain` is the server's only gc verb — it takes no window, depth, delta policy or ordering, which is why `gunnar gc` no longer has those flags. The safety is the same shape as before: compaction runs against a hard link, the result is read back and blake3-checked before anything is committed, and the directory is fsynced so the rename is durable rather than merely visible. ⚠ MEASURED 2026-08-13: `next_generation` derives its target from the archive's CURRENT name, and the live archive is always opened as `repository.znippy`, so the target is always `repository.g1.znippy` and the second gc on any repository hard-fails on `already exists`. gc is cron work. Whether the first gc should swap the generation over the live name, or the open path should follow generations, is NOT determined.",
        note_wiring: Wiring::NotEmitted,
    },
    // ── secrets ─────────────────────────────────────────────────────────────
    Lane {
        op: "secrets",
        what: "host key, deploy keys, webhook tokens",
        bytes_in: "ed25519 minted in-process",
        bytes_out: "sealed arrow_log rows",
        stages: &[
            Stage::new("ensure_host_key", "minted, never shelled out", Evidence::at("crates/gunnar-core/src/host_key.rs", 125).naming("fn ensure_host_key")),
            Stage::new("SecretStore::put", "trait + blanket impls", Evidence::at("crates/gunnar-store/src/secret.rs", 141).naming("trait SecretStore")),
            Stage::new("Stores::secrets()", "the sealed-secret boot-path handle", Evidence::at("crates/gunnar-server/src/stores.rs", 1709).naming("fn secrets(")),
            Stage::new("SealedSecretStore", "AEAD under a KEK", Evidence::at("crates/gunnar-core/src/seal.rs", 377).naming("SealedSecretStore"))
                .edge(Wiring::NotEmitted),
            Stage::new("__gunnar_secrets__", "never reaches an archive", Evidence::at("plugins/native/znippy-plugin-git/src/secrets.rs", 89)),
        ],
        note: "Stores::secrets() (stores.rs:279) constructs SealedSecretStore over the raw ArrowLogSecretStore, the KEK comes from Kek::for_control_dir at stores.rs:223, and serve.rs:387 takes that handle. Sealing happens at the store; __gunnar_secrets__ is a separate archive-side encoding that FoldRequest carries no input for (repack.rs:188).",
        note_wiring: Wiring::Wired,
    },
];

// ════════════════════════════════════════════════════════════════════════════
// SVG primitives — no dependencies, so this is the whole renderer
// ════════════════════════════════════════════════════════════════════════════

pub const W: f64 = 1180.0;
pub const M: f64 = 40.0;

/// XML text escape. `&` first or it double-escapes the ones it just wrote.
pub fn esc(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

/// Trim a float to at most 2 decimals with no trailing zeroes, so the emitted
/// markup is diffable rather than full of `160.00000000000003`.
pub fn n(v: f64) -> String {
    let s = format!("{v:.2}");
    let s = s.trim_end_matches('0').trim_end_matches('.');
    if s.is_empty() || s == "-0" {
        "0".into()
    } else {
        s.into()
    }
}

#[derive(Default)]
pub struct Canvas {
    body: String,
}

#[derive(Clone, Copy)]
pub enum Anchor {
    Start,
    Middle,
    End,
}

impl Anchor {
    const fn as_str(self) -> &'static str {
        match self {
            Anchor::Start => "start",
            Anchor::Middle => "middle",
            Anchor::End => "end",
        }
    }
}

/// A text run. Builder rather than an eight-argument function, because the
/// eight-argument version is where a fill and a size get swapped silently.
pub struct Txt<'a> {
    x: f64,
    y: f64,
    s: &'a str,
    fill: &'a str,
    size: f64,
    weight: u32,
    anchor: Anchor,
    mono: bool,
    opacity: f64,
}

impl<'a> Txt<'a> {
    pub fn new(x: f64, y: f64, s: &'a str) -> Self {
        Self {
            x,
            y,
            s,
            fill: "#cfe6f5",
            size: 11.0,
            weight: 400,
            anchor: Anchor::Start,
            mono: false,
            opacity: 1.0,
        }
    }
    pub fn fill(mut self, f: &'a str) -> Self {
        self.fill = f;
        self
    }
    pub fn size(mut self, s: f64) -> Self {
        self.size = s;
        self
    }
    pub fn bold(mut self) -> Self {
        self.weight = 700;
        self
    }
    pub fn anchor(mut self, a: Anchor) -> Self {
        self.anchor = a;
        self
    }
    pub fn mono(mut self) -> Self {
        self.mono = true;
        self
    }
    pub fn opacity(mut self, o: f64) -> Self {
        self.opacity = o;
        self
    }
}

// An SVG rect genuinely is `x, y, w, h, rx, fill, stroke, stroke-width`. Bagging
// them into a struct would add a type whose only job is to be destructured one
// line later, and the call sites read worse for it. `Txt` is a builder precisely
// because its arguments are the ones that *can* be swapped silently; these
// cannot — a colour will not typecheck as a coordinate.
#[allow(clippy::too_many_arguments)]
impl Canvas {
    /// The accumulated markup. Exposed so a second generator in this crate can
    /// compose a document out of the same primitives instead of reimplementing
    /// them (one implementation, not two).
    pub fn body(&self) -> &str {
        &self.body
    }
    pub fn body_len(&self) -> usize {
        self.body.len()
    }

    pub fn comment(&mut self, s: &str) {
        let _ = write!(self.body, "\n  <!-- {} -->\n", "=".repeat(20));
        let _ = writeln!(self.body, "  <!-- {} -->", esc(s));
    }

    pub fn rect(
        &mut self,
        x: f64,
        y: f64,
        w: f64,
        h: f64,
        rx: f64,
        fill: &str,
        stroke: &str,
        sw: f64,
    ) {
        let _ = writeln!(
            self.body,
            "  <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" rx=\"{}\" fill=\"{fill}\" stroke=\"{stroke}\" stroke-width=\"{}\"/>",
            n(x), n(y), n(w), n(h), n(rx), n(sw)
        );
    }

    /// A rect whose border carries the wiring verdict. This is the single place
    /// a `Wiring` becomes pixels for a box, so no caller can draw an unwired
    /// thing solid.
    pub fn rect_wired(
        &mut self,
        x: f64,
        y: f64,
        w: f64,
        h: f64,
        rx: f64,
        fill: &str,
        wire: Wiring,
        sw: f64,
    ) {
        match wire.dash() {
            None => self.rect(x, y, w, h, rx, fill, wire.stroke(), sw),
            Some(d) => {
                let _ = writeln!(
                    self.body,
                    "  <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" rx=\"{}\" fill=\"{fill}\" stroke=\"{}\" stroke-width=\"{}\" stroke-dasharray=\"{d}\"/>",
                    n(x), n(y), n(w), n(h), n(rx), wire.stroke(), n(sw)
                );
            }
        }
    }

    pub fn line(
        &mut self,
        x1: f64,
        y1: f64,
        x2: f64,
        y2: f64,
        stroke: &str,
        sw: f64,
        dash: Option<&str>,
    ) {
        let d = dash
            .map(|d| format!(" stroke-dasharray=\"{d}\""))
            .unwrap_or_default();
        let _ = writeln!(
            self.body,
            "  <line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"{stroke}\" stroke-width=\"{}\"{d}/>",
            n(x1), n(y1), n(x2), n(y2), n(sw)
        );
    }

    pub fn path(&mut self, d: &str, stroke: &str, sw: f64, dash: Option<&str>, marker: bool) {
        let da = dash
            .map(|d| format!(" stroke-dasharray=\"{d}\""))
            .unwrap_or_default();
        let mk = if marker {
            " marker-end=\"url(#arrow)\""
        } else {
            ""
        };
        let _ = writeln!(
            self.body,
            "  <path d=\"{d}\" fill=\"none\" stroke=\"{stroke}\" stroke-width=\"{}\"{da}{mk}/>",
            n(sw)
        );
    }

    pub fn circle(&mut self, cx: f64, cy: f64, r: f64, fill: &str, stroke: Option<&str>, sw: f64) {
        let st = stroke
            .map(|s| format!(" stroke=\"{s}\" stroke-width=\"{}\"", n(sw)))
            .unwrap_or_default();
        let _ = writeln!(
            self.body,
            "  <circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"{fill}\"{st}/>",
            n(cx),
            n(cy),
            n(r)
        );
    }

    pub fn txt(&mut self, t: Txt<'_>) {
        if t.s.is_empty() {
            return;
        }
        let font = if t.mono {
            " font-family=\"'DejaVu Sans Mono', monospace\""
        } else {
            ""
        };
        let weight = if t.weight == 700 {
            " font-weight=\"700\""
        } else {
            ""
        };
        let op = if t.opacity < 1.0 {
            format!(" opacity=\"{}\"", n(t.opacity))
        } else {
            String::new()
        };
        let _ = writeln!(
            self.body,
            "  <text x=\"{}\" y=\"{}\" fill=\"{}\" font-size=\"{}\" text-anchor=\"{}\"{font}{weight}{op}>{}</text>",
            n(t.x), n(t.y), t.fill, n(t.size), t.anchor.as_str(), esc(t.s)
        );
    }

    /// Left-to-right arrow between two boxes on the same baseline.
    pub fn arrow_h(&mut self, x1: f64, x2: f64, y: f64, wire: Wiring) {
        let d = format!("M{} {} H{}", n(x1), n(y), n(x2 - 8.0));
        self.path(&d, wire.stroke(), 2.6, wire.dash(), true);
    }

    /// A paragraph wrapped to `width` px. Returns the y just past the last line.
    ///
    /// Wrapping is the difference between a caption and a caption that runs off
    /// the canvas — which is what every long note in the first render did, and
    /// which no XML check can see because the markup is perfectly valid.
    pub fn para(&mut self, x: f64, y: f64, width: f64, s: &str, fill: &str, size: f64) -> f64 {
        let cols = (width / (size * PROP_ADV)).floor() as usize;
        let lines = wrap(s, cols.max(20));
        for (i, l) in lines.iter().enumerate() {
            self.txt(
                Txt::new(x, y + i as f64 * (size + 3.0), l)
                    .fill(fill)
                    .size(size),
            );
        }
        y + lines.len() as f64 * (size + 3.0)
    }
}

/// Mean advance of DejaVu Sans as a fraction of font-size, over mixed-case
/// prose. Used only to decide where to wrap; a few px either way is invisible,
/// but the order of magnitude is not.
pub const PROP_ADV: f64 = 0.545;
/// Mean advance of DejaVu Sans Mono — this one is exact, the font is monospaced.
pub const MONO_ADV: f64 = 0.602;

/// A component box in the house style, with an optional list of slot rows.
struct Chip<'a> {
    x: f64,
    y: f64,
    w: f64,
    h: f64,
    title: &'a str,
    sub: &'a str,
    wire: Wiring,
    grad: &'a str,
}

impl<'a> Chip<'a> {
    fn draw(&self, c: &mut Canvas) {
        c.rect_wired(
            self.x,
            self.y,
            self.w,
            self.h,
            10.0,
            &format!("url(#{})", self.grad),
            self.wire,
            2.0,
        );
        let cx = self.x + self.w / 2.0;
        c.txt(
            Txt::new(cx, self.y + 24.0, self.title)
                .fill("#eaf6ff")
                .size(13.5)
                .bold()
                .anchor(Anchor::Middle)
                .mono(),
        );
        c.txt(
            Txt::new(cx, self.y + 40.0, self.sub)
                .fill("#9fc9e0")
                .size(10.0)
                .anchor(Anchor::Middle),
        );
    }
}

/// One inner slot row: a monospace label over a dim type.
fn slot(c: &mut Canvas, x: f64, y: f64, w: f64, label: &str, ty: &str, wire: Wiring) {
    if label.is_empty() && ty.is_empty() {
        return;
    }
    if label.is_empty() {
        // A note row rather than a field row: no pill, just the dim text.
        c.txt(
            Txt::new(x + w / 2.0, y + 13.0, ty)
                .fill("#7fa8c4")
                .size(9.0)
                .anchor(Anchor::Middle),
        );
        return;
    }
    let stroke = if matches!(wire, Wiring::Wired) {
        "#6fd3f5"
    } else {
        "#4a6f88"
    };
    c.rect(x, y, w, 19.0, 4.0, "url(#slot)", stroke, 1.1);
    c.txt(
        Txt::new(x + 6.0, y + 13.5, label)
            .fill("#bfe6ff")
            .size(9.5)
            .mono(),
    );
    c.txt(
        Txt::new(x + w - 6.0, y + 13.5, ty)
            .fill("#86b3cc")
            .size(8.5)
            .anchor(Anchor::End)
            .mono(),
    );
}

/// A panel heading: a numbered rule across the canvas.
pub fn heading(c: &mut Canvas, y: f64, num: &str, title: &str, sub: &str) {
    c.txt(Txt::new(M, y, num).fill("#6fd3f5").size(15.0).bold().mono());
    c.txt(
        Txt::new(M + 34.0, y, title)
            .fill("#eaf6ff")
            .size(15.0)
            .bold(),
    );
    c.txt(Txt::new(M + 34.0, y + 17.0, sub).fill("#9fc9e0").size(11.0));
    c.line(M, y + 26.0, W - M, y + 26.0, "#143a52", 1.0, None);
}

// ════════════════════════════════════════════════════════════════════════════
// Panel 1 — the five reserved sections
// ════════════════════════════════════════════════════════════════════════════

fn panel_sections(c: &mut Canvas, y0: f64) -> f64 {
    c.comment("PANEL 1 - the five reserved sections");
    heading(
        c,
        y0,
        "1",
        "The five reserved sections, and whether anything reads them",
        "plugins/native/znippy-plugin-git/src/ · reserved = ordinary list / decompress / iceberg readers skip them",
    );

    let top = y0 + 44.0;
    let gap = 20.0;
    let cw = (W - 2.0 * M - gap * (SECTIONS.len() as f64 - 1.0)) / SECTIONS.len() as f64;
    let ch = 224.0;

    for (i, s) in SECTIONS.iter().enumerate() {
        let x = M + i as f64 * (cw + gap);
        Chip {
            x,
            y: top,
            w: cw,
            h: ch,
            title: s.module,
            sub: s.payload,
            wire: s.wiring,
            grad: if matches!(s.wiring, Wiring::Wired) {
                "chip"
            } else {
                "cold"
            },
        }
        .draw(c);

        for (j, (f, t)) in s.fields.iter().enumerate() {
            slot(
                c,
                x + 10.0,
                top + 52.0 + j as f64 * 23.0,
                cw - 20.0,
                f,
                t,
                s.wiring,
            );
        }

        // Provenance, three lines: built / produced / consumed. An absent
        // producer or consumer is printed as a dash rather than omitted, so the
        // hole is visible instead of merely unmentioned.
        let ly = top + ch + 16.0;
        let ev = |e: Option<Evidence>| e.map(|e| e.label()).unwrap_or_else(|| "— none".into());
        for (k, (k_label, k_val, k_fill)) in [
            ("built", s.built_at.label(), "#86b3cc"),
            (
                "writes",
                ev(s.produced_by),
                if s.produced_by.is_some() {
                    "#8fe6b8"
                } else {
                    "#f0a9a0"
                },
            ),
            (
                "reads",
                ev(s.consumed_by),
                if s.consumed_by.is_some() {
                    "#8fe6b8"
                } else {
                    "#f0a9a0"
                },
            ),
        ]
        .iter()
        .enumerate()
        {
            let yy = ly + k as f64 * 13.0;
            c.txt(Txt::new(x, yy, k_label).fill("#5f8ba6").size(8.5));
            c.txt(
                Txt::new(x + cw, yy, k_val)
                    .fill(k_fill)
                    .size(8.5)
                    .anchor(Anchor::End)
                    .mono(),
            );
        }

        // The note, wrapped to the chip width.
        let vy = ly + 3.0 * 13.0 + 6.0;
        for (k, l) in wrap(s.note, (cw / 4.6) as usize)
            .into_iter()
            .take(4)
            .enumerate()
        {
            c.txt(
                Txt::new(x, vy + k as f64 * 11.0, &l)
                    .fill(if matches!(s.wiring, Wiring::Wired) {
                        "#a7e6c2"
                    } else {
                        "#e6b7ad"
                    })
                    .size(8.8),
            );
        }
    }

    top + ch + 16.0 + 3.0 * 13.0 + 6.0 + 4.0 * 11.0 + 10.0
}

/// Greedy word wrap. Good enough for an 8.8pt caption and it keeps the
/// generator dependency-free.
pub fn wrap(s: &str, cols: usize) -> Vec<String> {
    let mut out = Vec::new();
    let mut cur = String::new();
    for w in s.split_whitespace() {
        if !cur.is_empty() && cur.len() + 1 + w.len() > cols {
            out.push(std::mem::take(&mut cur));
        }
        if !cur.is_empty() {
            cur.push(' ');
        }
        cur.push_str(w);
    }
    if !cur.is_empty() {
        out.push(cur);
    }
    out
}

// ════════════════════════════════════════════════════════════════════════════
// The enclosing fact — a strip, because it changes how every panel reads
// ════════════════════════════════════════════════════════════════════════════

/// The single sentence that reframes the whole picture, drawn as its own band
/// rather than a footnote. A footnote is where this fact went for four
/// sessions.
const STRIP_BODY: &str = "Cargo.toml:55 excludes the old znippy engine crate from the workspace. znippy's codec cmake-builds C and C++ from a downloaded tarball during `cargo build`, and cargo reads a path dependency's manifest at resolve time even when the feature is off, so a `znippy` feature would need a sibling checkout on every machine that builds gunnar. gunnar's Cargo.lock holds 0 znippy entries (the desktop 2026-08-04), and inside a workspace member the three index sections exist only as comments.";

fn strip(c: &mut Canvas, y: f64) -> f64 {
    c.comment("ENCLOSING FACT - znippy is not in gunnar's resolved graph");
    let inner = W - 2.0 * M - 32.0;
    let mut probe = Canvas::default();
    let end = probe.para(0.0, 0.0, inner, STRIP_BODY, "#e6b7ad", 9.6);
    let h = 30.0 + end + 10.0;

    c.rect_wired(
        M,
        y,
        W - 2.0 * M,
        h,
        8.0,
        "url(#cold)",
        Wiring::NotEmitted,
        2.0,
    );
    c.txt(
        Txt::new(
            M + 16.0,
            y + 22.0,
            "the five sections sit outside a server build's dependency graph",
        )
        .fill("#f0a9a0")
        .size(13.0)
        .bold(),
    );
    let _ = c.para(M + 16.0, y + 38.0, inner, STRIP_BODY, "#e6b7ad", 9.6);
    y + h
}

// ════════════════════════════════════════════════════════════════════════════
// Panel 2 — per git operation, where the bytes go in and out
// ════════════════════════════════════════════════════════════════════════════

fn panel_ops(c: &mut Canvas, y0: f64, lanes: &[Lane]) -> f64 {
    c.comment("PANEL 2 - per git operation, bytes in and bytes out");
    heading(
        c,
        y0,
        "2",
        "Per git operation: where the bytes enter, what touches them, where they leave",
        "solid = verified call edge · dashed amber = written and never read · dotted coral = never produced on this path",
    );

    let mut y = y0 + 44.0;

    for lane in lanes {
        // Lane header.
        c.rect(M, y, W - 2.0 * M, 22.0, 5.0, "url(#rail)", "#3c6a86", 1.0);
        c.txt(
            Txt::new(M + 10.0, y + 15.5, lane.op)
                .fill("#dff1ff")
                .size(11.5)
                .bold()
                .mono(),
        );
        c.txt(
            Txt::new(M + 132.0, y + 15.5, lane.what)
                .fill("#9fc9e0")
                .size(10.0),
        );
        c.txt(
            Txt::new(
                W - M - 10.0,
                y + 15.5,
                &format!("in: {}   out: {}", lane.bytes_in, lane.bytes_out),
            )
            .fill("#7fa8c4")
            .size(9.0)
            .anchor(Anchor::End)
            .mono(),
        );

        // Stage boxes, auto-laid across the width.
        let sy = y + 32.0;
        let sh = 50.0;
        let inner = W - 2.0 * M;
        let ns = lane.stages.len() as f64;
        let sgap = 26.0;
        let sw = (inner - sgap * (ns - 1.0)) / ns;

        for (i, st) in lane.stages.iter().enumerate() {
            let x = M + i as f64 * (sw + sgap);
            let wire = if i == 0 {
                Wiring::Wired
            } else {
                lane.stages[i - 1].out
            };
            c.rect_wired(x, sy, sw, sh, 7.0, "url(#chip)", wire, 1.6);
            c.txt(
                Txt::new(x + sw / 2.0, sy + 19.0, st.label)
                    .fill("#eaf6ff")
                    .size(10.5)
                    .bold()
                    .anchor(Anchor::Middle)
                    .mono(),
            );
            c.txt(
                Txt::new(x + sw / 2.0, sy + 33.0, st.sub)
                    .fill("#9fc9e0")
                    .size(8.8)
                    .anchor(Anchor::Middle),
            );
            if let Some(e) = st.ev {
                c.txt(
                    Txt::new(x + sw / 2.0, sy + 45.0, &e.label())
                        .fill("#5f8ba6")
                        .size(7.6)
                        .anchor(Anchor::Middle)
                        .mono(),
                );
            }
            if i + 1 < lane.stages.len() {
                c.arrow_h(x + sw + 3.0, x + sw + sgap - 1.0, sy + sh / 2.0, st.out);
                c.circle(
                    x + sw + sgap / 2.0,
                    sy + sh / 2.0,
                    3.0,
                    "#bfecff",
                    None,
                    0.0,
                );
            }
        }

        // The lane's one honest caveat.
        let fill = match lane.note_wiring {
            Wiring::Wired => "#a7e6c2",
            Wiring::Unwired => "#ffcf7a",
            Wiring::NotEmitted => "#f0a9a0",
        };
        let end = c.para(M, sy + sh + 16.0, inner, lane.note, fill, 9.5);

        y = end + LANE_TAIL;
    }
    y + 4.0
}

/// Gap between the last line of a lane's note and the next lane's header.
const LANE_TAIL: f64 = 14.0;

// ════════════════════════════════════════════════════════════════════════════
// Panel 3 — over time: append, accumulate, re-seal
// ════════════════════════════════════════════════════════════════════════════

/// One point on the measured growth curve. Source:
/// the deleted engine crate’s lib.rs:300-312, arm `gunnar.znippy_engine_growth`,
/// the desktop 2026-08-03, reproduced twice at loadavg 36.3 and 56.7.
pub struct GrowthPoint {
    pub pushes: u32,
    pub archive_b: u64,
    pub objects_b: u64,
    pub ratio_x: f64,
    pub append_b: u64,
}

pub const GROWTH: &[GrowthPoint] = &[
    GrowthPoint {
        pushes: 10,
        archive_b: 32_272,
        objects_b: 18_015,
        ratio_x: 1.79,
        append_b: 1556,
    },
    GrowthPoint {
        pushes: 100,
        archive_b: 230_545,
        objects_b: 108_434,
        ratio_x: 2.13,
        append_b: 1558,
    },
    GrowthPoint {
        pushes: 1000,
        archive_b: 2_219_778,
        objects_b: 510_081,
        ratio_x: 4.35,
        append_b: 1557,
    },
];

/// Bytes after the re-seal that follows the 1000-push point above. Same source.
pub const RESEAL_B: u64 = 427_820;
pub const RESEAL_X: f64 = 5.19;
pub const RESEAL_MS: f64 = 2.4;

fn panel_time(c: &mut Canvas, y0: f64) -> f64 {
    c.comment("PANEL 3 - over time: one frame per push, packs accumulate, a re-seal compacts");
    heading(
        c,
        y0,
        "3",
        "Over time — the part a box diagram cannot show",
        "each push appends one self-contained Arrow frame · packs accumulate · a re-seal compacts",
    );

    let top = y0 + 46.0;

    // ── 3a. the push log, frame by frame ────────────────────────────────────
    c.txt(
        Txt::new(
            M,
            top,
            "the live push log — one RecordBatch per push, fsynced",
        )
        .fill("#bcd9ec")
        .size(11.0)
        .bold(),
    );
    c.txt(
        Txt::new(M, top + 14.0, "plugins/native/znippy-plugin-git/src/pushlog.rs:266 append() · frame = [ schema | batch | EOS ] · the frame boundary is the transaction")
            .fill("#7fa8c4")
            .size(9.0)
            .mono(),
    );

    let fy = top + 26.0;
    let fw = 118.0;
    let fh = 40.0;
    for i in 0..7 {
        let x = M + i as f64 * (fw + 8.0);
        let torn = i == 6;
        let wire = if torn {
            Wiring::NotEmitted
        } else {
            Wiring::Wired
        };
        c.rect_wired(x, fy, fw, fh, 5.0, "url(#slot)", wire, 1.4);
        // The three messages inside one frame.
        for (k, seg) in ["schema", "batch", "EOS"].iter().enumerate() {
            let segw = if k == 1 { 46.0 } else { 30.0 };
            let sx = x
                + 6.0
                + if k == 0 {
                    0.0
                } else if k == 1 {
                    32.0
                } else {
                    80.0
                };
            if torn && k == 2 {
                continue;
            }
            c.rect(
                sx,
                fy + 8.0,
                segw,
                14.0,
                2.0,
                "#0a2032",
                if torn { "#7a4038" } else { "#4a8fb0" },
                1.0,
            );
            c.txt(
                Txt::new(sx + segw / 2.0, fy + 18.5, seg)
                    .fill(if torn { "#e6b7ad" } else { "#a9d8ee" })
                    .size(7.5)
                    .anchor(Anchor::Middle)
                    .mono(),
            );
        }
        let label = if torn {
            "torn — discarded whole".to_string()
        } else {
            format!("push {}", i + 1)
        };
        c.txt(
            Txt::new(x + fw / 2.0, fy + 34.0, &label)
                .fill(if torn { "#f0a9a0" } else { "#9fc9e0" })
                .size(8.2)
                .anchor(Anchor::Middle),
        );
    }
    c.txt(
        Txt::new(W - M, fy + fh + 14.0, "scan_frames reads forward while frames parse and stops at the first that does not — pushlog.rs:126")
            .fill("#86b3cc")
            .size(9.0)
            .anchor(Anchor::End),
    );
    c.txt(
        Txt::new(
            M,
            fy + fh + 14.0,
            "frame = 2056 B · schema 448 B (21.8%) · batch 1600 B · 4 refs/push, the desktop 2026-08-03",
        )
        .fill("#86b3cc")
        .size(9.0),
    );

    // ── 3b. the growth curve ────────────────────────────────────────────────
    let gy = fy + fh + 34.0;
    c.txt(
        Txt::new(
            M,
            gy + 12.0,
            "what accumulates is packs, not payload — the append is flat, the pack count is not",
        )
        .fill("#bcd9ec")
        .size(11.0)
        .bold(),
    );
    // Wrapped to the chart's own column, not the canvas: the re-seal panel sits
    // to its right and a full-width line runs under it.
    c.para(
        M,
        gy + 26.0,
        620.0,
        "the desktop 2026-08-03 · arm gunnar.znippy_engine_growth · 1000 real pushes onto a 512 KiB tree · repeated at loadavg 36.3 and 56.7, agreeing to better than 0.01%",
        "#7fa8c4",
        9.0,
    );

    let bx = M;
    let by = gy + 50.0;
    let bh = 96.0;
    let bw = 640.0;
    // Log-ish scaling: the three points span 68.8x, so a linear bar chart shows
    // one bar and two slivers. The bar length is proportional to log10(bytes),
    // and the axis says so — an unlabelled log axis read as linear is its own
    // silent lie.
    let maxl = (GROWTH[2].archive_b as f64).log10();
    let minl = 4.0_f64;
    let scale = |v: u64| ((v as f64).log10() - minl) / (maxl - minl) * (bw - 150.0);

    for (i, p) in GROWTH.iter().enumerate() {
        let yy = by + i as f64 * 26.0;
        c.txt(
            Txt::new(bx + 44.0, yy + 13.0, &format!("{} pushes", p.pushes))
                .fill("#9fc9e0")
                .size(9.5)
                .anchor(Anchor::End)
                .mono(),
        );
        let ow = scale(p.objects_b).max(2.0);
        let aw = scale(p.archive_b).max(2.0);
        c.rect(
            bx + 52.0,
            yy + 3.0,
            aw,
            8.0,
            2.0,
            "url(#cold)",
            "#7a4038",
            1.0,
        );
        c.rect(
            bx + 52.0,
            yy + 13.0,
            ow,
            8.0,
            2.0,
            "url(#hot)",
            "#2f7d54",
            1.0,
        );
        c.txt(
            Txt::new(
                bx + 52.0 + aw + 8.0,
                yy + 11.0,
                &format!("archive {}", bytes(p.archive_b)),
            )
            .fill("#f0a9a0")
            .size(8.6)
            .mono(),
        );
        c.txt(
            Txt::new(
                bx + 52.0 + ow + 8.0,
                yy + 21.0,
                &format!("objects {}  →  {:.2}x", bytes(p.objects_b), p.ratio_x),
            )
            .fill("#a7e6c2")
            .size(8.6)
            .mono(),
        );
    }
    c.txt(
        Txt::new(bx + 52.0, by + 3.0 * 26.0 + 10.0, "bar length ∝ log10(bytes) — the span is 68.8x and a linear axis would show one bar and two slivers")
            .fill("#5f8ba6")
            .size(8.4),
    );
    c.txt(
        Txt::new(bx + 52.0, by + 3.0 * 26.0 + 22.0, &format!(
            "per-push append: {} / {} / {} B, flat · the growth is 1000 packs, rows_replaced 0 throughout",
            GROWTH[0].append_b, GROWTH[1].append_b, GROWTH[2].append_b
        ))
        .fill("#86b3cc")
        .size(8.8),
    );

    // ── 3c. the re-seal ─────────────────────────────────────────────────────
    let rx = bx + bw + 24.0;
    let rw = W - M - rx;
    c.rect(
        rx,
        by - 12.0,
        rw,
        bh + 44.0,
        8.0,
        "url(#rail)",
        "#7fc8ea",
        2.0,
    );
    c.txt(
        Txt::new(rx + rw / 2.0, by + 6.0, "the re-seal")
            .fill("#dff1ff")
            .size(12.0)
            .bold()
            .anchor(Anchor::Middle),
    );
    c.txt(
        Txt::new(
            rx + rw / 2.0,
            by + 21.0,
            "seal() — the compaction repack() calls",
        )
        .fill("#bcd9ec")
        .size(9.0)
        .anchor(Anchor::Middle),
    );
    c.txt(
        Txt::new(
            rx + rw / 2.0,
            by + 42.0,
            &format!("{} B", group(GROWTH[2].archive_b)),
        )
        .fill("#f0a9a0")
        .size(14.0)
        .bold()
        .anchor(Anchor::Middle)
        .mono(),
    );
    c.txt(
        Txt::new(rx + rw / 2.0, by + 58.0, "↓")
            .fill("#6fd3f5")
            .size(13.0)
            .anchor(Anchor::Middle),
    );
    c.txt(
        Txt::new(rx + rw / 2.0, by + 76.0, &format!("{} B", group(RESEAL_B)))
            .fill("#8fe6b8")
            .size(16.0)
            .bold()
            .anchor(Anchor::Middle)
            .mono(),
    );
    c.txt(
        Txt::new(
            rx + rw / 2.0,
            by + 94.0,
            &format!("{RESEAL_X:.2}x reclaimed · {RESEAL_MS} ms CPU"),
        )
        .fill("#a7e6c2")
        .size(9.5)
        .anchor(Anchor::Middle)
        .mono(),
    );
    c.txt(
        Txt::new(
            rx + rw / 2.0,
            by + 110.0,
            "staged · fsync · rename · fsync parent",
        )
        .fill("#7fa8c4")
        .size(8.6)
        .anchor(Anchor::Middle),
    );

    by + bh + 56.0
}

fn bytes(b: u64) -> String {
    if b >= 1_000_000 {
        format!("{:.2} MB", b as f64 / 1e6)
    } else {
        format!("{:.1} kB", b as f64 / 1e3)
    }
}

/// Thousands separated by a thin space, the way the design docs write numbers.
pub fn group(mut v: u64) -> String {
    let mut parts = Vec::new();
    if v == 0 {
        return "0".into();
    }
    while v > 0 {
        parts.push(v % 1000);
        v /= 1000;
    }
    parts.reverse();
    let mut s = format!("{}", parts[0]);
    for p in &parts[1..] {
        s.push(' ');
        let _ = write!(s, "{p:03}");
    }
    s
}

// ════════════════════════════════════════════════════════════════════════════
// The ground rail — the claim the whole picture makes
// ════════════════════════════════════════════════════════════════════════════

/// What the three seams ARE, in the present tense. One paragraph, because the
/// rail is a manual entry and not a changelog: what a reader needs is the state
/// the system is in, not the route it took there.
const RAIL_BODY: &str = "RepackOptions::cold_tier() has no caller in a server crate; `gc` builds \
    RepackOptions::new() at gunnar-cli/src/server/admin.rs:383, so the packfile is the znippy engine. \
    SealedSecretStore is constructed on the boot path at crates/gunnar-cli/src/server/stores.rs:279, \
    so the secret STORE is sealed while the archive section __gunnar_secrets__ has no producer. \
    __gunnar_oid__, __gunnar_graph__ and __gunnar_reach__ are not emitted at all, because \
    GitIndexBuilder::pack_tier returns an empty section list by construction \
    (plugins/native/znippy-plugin-git/src/sections.rs:249) — the pack's own .idx is the oid index, and the other \
    two derive from object payloads a pack tier never inflates.";

fn ground_rail(c: &mut Canvas, y: f64) -> f64 {
    c.comment("GROUND RAIL - the claim");
    let inner = W - 2.0 * M - 36.0;
    // Measure first, then draw the box around what the text needs. Drawing a
    // fixed-height rail and hoping the prose fits is how the first render put
    // three lines through the right-hand border.
    let mut probe = Canvas::default();
    let cy = probe.para(0.0, 0.0, inner, RAIL_BODY, "#bcd9ec", 9.8);
    let h = 34.0 + cy + 24.0;

    c.rect(M, y, W - 2.0 * M, h, 10.0, "url(#rail)", "#7fc8ea", 2.0);
    for i in 0..14 {
        c.circle(
            M + 24.0 + i as f64 * 80.0,
            y + h - 9.0,
            4.0,
            "#0a1c2c",
            Some("#7fc8ea"),
            1.5,
        );
    }
    c.txt(
        Txt::new(M + 18.0, y + 24.0, "where each seam stands")
            .fill("#dff1ff")
            .size(15.0)
            .bold(),
    );
    let _ = c.para(M + 18.0, y + 34.0 + 10.0, inner, RAIL_BODY, "#bcd9ec", 9.8);
    y + h
}

// ════════════════════════════════════════════════════════════════════════════
// Compose
// ════════════════════════════════════════════════════════════════════════════

/// Render the whole diagram. Pure: same input, same bytes, so the committed
/// asset can be regenerated and diffed.
/// The document preamble every generator in this crate shares: the board
/// gradient, the chip / rail / slot / hot / cold gradients, the arrow marker,
/// the rails and the three title lines.
///
/// Extracted rather than copied. Two generators with two copies of this block
/// is two house styles that drift, and the drift shows up as a diagram that
/// looks *nearly* like the others — which is worse than one that looks
/// different, because nobody notices.
pub fn svg_header(h: f64, title: &str, subtitle: &str, footnote: &str) -> String {
    let mut out = String::with_capacity(4096);
    let _ = write!(
        out,
        r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" font-family="'DejaVu Sans', 'Segoe UI', sans-serif">
  <!-- GENERATED. Do not hand-edit: every number here carries a measurement, and
       editing the picture instead of the datum is how a diagram starts lying. -->
  <defs>
    <linearGradient id="board" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="#0b1c2e"/><stop offset="1" stop-color="#06111d"/>
    </linearGradient>
    <linearGradient id="chip" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="#14384f"/><stop offset="1" stop-color="#0e2a3e"/>
    </linearGradient>
    <linearGradient id="rail" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="#173d57"/><stop offset="1" stop-color="#0c2438"/>
    </linearGradient>
    <linearGradient id="slot" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="#123448"/><stop offset="1" stop-color="#0a2032"/>
    </linearGradient>
    <linearGradient id="hot" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="#1c5a3a"/><stop offset="1" stop-color="#0f3a26"/>
    </linearGradient>
    <linearGradient id="cold" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="#3a2320"/><stop offset="1" stop-color="#281414"/>
    </linearGradient>
    <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0 0 L10 5 L0 10 z" fill="context-stroke"/>
    </marker>
  </defs>

  <rect x="0" y="0" width="{w}" height="{h}" fill="url(#board)"/>
  <g stroke="#143a52" stroke-width="1">
    <line x1="0" y1="76" x2="{w}" y2="76"/>
    <line x1="0" y1="{rail2}" x2="{w}" y2="{rail2}"/>
  </g>
  <g fill="#16415c">
    <circle cx="40" cy="40" r="2"/><circle cx="1140" cy="40" r="2"/>
    <circle cx="40" cy="{vy}" r="2"/><circle cx="1140" cy="{vy}" r="2"/>
  </g>

  <text x="40" y="44" fill="#eaf6ff" font-size="22" font-weight="700">{title}</text>
  <text x="{sx}" y="38" fill="#9fc9e0" font-size="13">{subtitle}</text>
  <text x="{sx}" y="56" fill="#7fa8c4" font-size="10.5">{footnote}</text>
"##,
        w = n(W),
        h = n(h),
        rail2 = n(h - 48.0),
        vy = n(h - 28.0),
        // 22pt bold caps advance a little wider than the prose mean; measured off
        // the rendered title, because a constant here fit "gunnar" and collided
        // the moment the title became "gunnar vs gitea vs Forgejo".
        sx = n(M + title.chars().count() as f64 * 22.0 * 0.60 + 24.0),
        title = esc(title),
        subtitle = esc(subtitle),
        footnote = esc(footnote),
    );
    out
}

/// Render the whole diagram. Pure: same input, same bytes, so the committed
/// asset can be regenerated and diffed.
pub fn render(lanes: &[Lane]) -> String {
    let mut c = Canvas::default();

    let mut y = panel_sections(&mut c, 108.0);
    y = strip(&mut c, y + 8.0);
    y = panel_ops(&mut c, y + 26.0, lanes);
    y = panel_time(&mut c, y + 14.0);
    y = ground_rail(&mut c, y + 18.0);

    let h = y + 44.0;

    let mut out = String::with_capacity(c.body.len() + 4096);
    out.push_str(&svg_header(
        h,
        "gunnar",
        "the write and read cycle through Arrow IPC and znippy \u{2014} per git operation, and over time",
        "every box and arrow carries the file:line it lives at",
    ));

    // Legend, top right on the title band.
    let _ = write!(
        out,
        r##"  <g font-size="11">
    <line x1="{lx}" y1="30" x2="{lx2}" y2="30" stroke="#6fd3f5" stroke-width="3"/>
    <text x="{tx}" y="34" fill="#cfe6f5">verified call edge</text>
    <line x1="{lx}" y1="48" x2="{lx2}" y2="48" stroke="#e0a86a" stroke-width="2.5" stroke-dasharray="7 5"/>
    <text x="{tx}" y="52" fill="#cfe6f5">built, nothing reads it</text>
    <line x1="{lx}" y1="66" x2="{lx2}" y2="66" stroke="#f0a9a0" stroke-width="2.5" stroke-dasharray="3 4"/>
    <text x="{tx}" y="70" fill="#cfe6f5">never produced on this path</text>
  </g>
"##,
        lx = n(W - M - 232.0),
        lx2 = n(W - M - 196.0),
        tx = n(W - M - 188.0),
    );

    out.push_str(&c.body);
    out.push_str("</svg>\n");
    out
}

/// The asset this generator owns, repo-relative.
///
/// `assets/` and not `.nornir/assets/`: see [`asset_dirs`]. This figure is a
/// design-record diagram that no shipped page embeds, so it gets ONE home.
pub const ASSET: &str = "assets/gunnar-znippy-cycle.svg";

/// The two directories a generated asset has to be in, and why there are two.
///
/// The markdown under `.nornir/` writes `![…](assets/x.svg)`, and that one
/// string has to resolve in three places: beside the source in `.nornir/`, in
/// the staged book tree `cargo xtask docs` builds (where the chapters sit at the
/// root and `docsbook::stage_tree` flattens both asset dirs into one), and in
/// the repo-root `README.md` nornir generates verbatim from
/// `.nornir/README.md`. The first needs `.nornir/assets/`, the last needs
/// `assets/`, and nothing rewrites the path in between — which is why the root
/// README's two SVG links were broken images on the forge until this was
/// written.
///
/// Duplication, but not a second source of truth (one implementation, not two): [`publish_asset`] is
/// the only writer of either copy, so the two cannot disagree. `gunnar.webp`
/// already lives in both, by hand.
pub const ASSET_DIRS: [&str; 2] = [".nornir/assets", "assets"];

/// The one directory a figure NO page embeds lives in.
///
/// `assets/`, the copy the repo tracks and the copy the published tree carries.
pub const ROOT_ASSET_DIR: [&str; 1] = ["assets"];

/// Which directories one asset is published to, read off the path its generator
/// owns.
///
/// Two copies are the exception, not the rule. [`ASSET_DIRS`] explains why a
/// figure that a `.nornir/` page **embeds** has to exist twice — one relative
/// link, three render roots. A figure that no page embeds has no such problem,
/// and giving it a second copy is worse than useless: `xtask/src/publish.rs`
/// excludes `.nornir/assets/*.svg` from the published tree as design-record
/// diagrams (measured 2026-09-08: zero references from `README.md`, `detail.md`
/// or the quick start), carving out only `gunnar-setup.svg`, which the quick
/// start does embed. So the `.nornir/assets/` copy of `gunnar-znippy-cycle.svg`
/// and `gunnar-data-path.svg` was a file that shipped nowhere and was embedded
/// nowhere — while [`assert_asset_fresh`] still demanded it, which is exactly
/// how a fresh clone (which has only the `assets/` copy git tracks) went red.
///
/// The generator's own `ASSET` const is the declaration: a `.nornir/assets/…`
/// path means "a page embeds this, publish both", an `assets/…` path means
/// "one home". One string, and the writer and the freshness guard read the same
/// one — they cannot drift apart.
pub fn asset_dirs(asset: &str) -> &'static [&'static str] {
    if asset.starts_with(".nornir/assets/") {
        &ASSET_DIRS
    } else {
        &ROOT_ASSET_DIR
    }
}

/// Write one generated asset to every directory in [`ASSET_DIRS`].
pub fn publish_asset(
    repo_root: &std::path::Path,
    file_name: &str,
    bytes: &[u8],
) -> std::io::Result<()> {
    publish_asset_to(repo_root, &ASSET_DIRS, file_name, bytes)
}

/// Write one generated asset to exactly the directories [`asset_dirs`] names
/// for it.
pub fn publish_owned_asset(
    repo_root: &std::path::Path,
    asset: &str,
    bytes: &[u8],
) -> std::io::Result<()> {
    let name = std::path::Path::new(asset).file_name().ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("{asset} does not name a file"),
        )
    })?;
    publish_asset_to(repo_root, asset_dirs(asset), &name.to_string_lossy(), bytes)
}

fn publish_asset_to(
    repo_root: &std::path::Path,
    dirs: &[&str],
    file_name: &str,
    bytes: &[u8],
) -> std::io::Result<()> {
    for dir in dirs {
        let dir = repo_root.join(dir);
        std::fs::create_dir_all(&dir)?;
        std::fs::write(dir.join(file_name), bytes)?;
    }
    Ok(())
}

/// **The committed asset must be what the model renders, in every directory it
/// is published to.**
///
/// The citation guard holds the *model* to the code. Nothing held the *picture*
/// to the model, and the picture is the artefact that actually ships — it is in
/// the repository, in the root `README.md` and in the rendered manual. MEASURED
/// 2026-08-05 at `fab9541`: both committed copies of `gunnar-data-path.svg` drew
/// `copy.rs:608` where the model said `1231`, and `copy.rs:790` where it said
/// `1452`. Every citation in the model was green at the same moment, because a
/// model is not a picture.
///
/// So this is the same disease one level out, and the fix is the same shape: the
/// generated bytes are the only truth, and a checked-in copy that differs from
/// them is stale by definition. It is also the only check that can see the two
/// published copies disagree with each other.
///
/// `subject` names the generator so a red says which `cargo xtask svg` to run.
pub fn assert_asset_fresh(repo_root: &std::path::Path, asset: &str, rendered: &str, subject: &str) {
    let name = std::path::Path::new(asset)
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or_else(|| panic!("{subject}: {asset} does not name a file"));
    // Exactly the directories this asset is published to — see [`asset_dirs`].
    // Demanding a `.nornir/assets/` copy of a figure whose generator does not
    // write one is how this guard turned a fresh clone red.
    let dirs = asset_dirs(asset);
    let mut checked = 0usize;
    for dir in dirs {
        let path = repo_root.join(dir).join(name);
        let on_disk = std::fs::read_to_string(&path).unwrap_or_else(|e| {
            panic!(
                "{subject}: {} is published to {dir}/ and will not open: {e}. \
                 Run `cargo xtask svg`.",
                path.display()
            )
        });
        if on_disk != rendered {
            // Say WHERE, not just that: a byte count tells a reader nothing
            // about which claim in the picture is now a lie.
            let first = rendered
                .lines()
                .zip(on_disk.lines())
                .position(|(a, b)| a != b);
            let detail = match first {
                Some(i) => format!(
                    "first difference at line {}:\n    committed: {}\n    model:     {}",
                    i + 1,
                    on_disk.lines().nth(i).unwrap_or("").trim(),
                    rendered.lines().nth(i).unwrap_or("").trim()
                ),
                None => format!(
                    "same prefix, different length: committed {} lines, model {}",
                    on_disk.lines().count(),
                    rendered.lines().count()
                ),
            };
            panic!(
                "{subject}: {} is STALE against the model that generates it, so the picture \
                 this repository ships states something the code no longer does. \
                 Run `cargo xtask svg` and commit both copies.\n  {detail}",
                path.display()
            );
        }
        checked += 1;
    }
    // The control: a loop over an empty list is green, and the directory list
    // is the thing being iterated.
    assert_eq!(
        checked,
        dirs.len(),
        "{subject}: {checked} of {} published copies were compared",
        dirs.len()
    );
    assert!(
        checked >= 1,
        "{subject}: {asset} is published to no directory at all"
    );
}

/// The one path prefix in either diagram's model that names a **different
/// repository**, so no guard running here can open it.
///
/// `znippy-plugin-git` lives in `znippy`, not in `gunnar`. Its citations are
/// still model data — the picture draws them — and they are still held to
/// something: [`assert_cites`]'s caller must count them, so a typo that turns an
/// in-repo path into an unopenable one is a failure rather than a skip.
pub const FOREIGN: &[&str] = &[
    // znippy's git plugin.
    "plugins/native/znippy-plugin-git/",
    // edda. `gunnar-pack`, `gunnar-bitmap`, `gunnar-pktline` and the gix
    // storage arm were gunnar crates until the 2026-08-09 split and are cited
    // all over both diagrams. After the move the citations still said
    // `crates/gunnar-pack/…`, a path that no longer opens, so the guard could
    // only be red — NINE of them, which is why this list is a list.
    //
    // Written with the repository name in front (`edda/crates/…`) rather than a
    // bare `../`: a citation is an address a person follows, and `..` from a
    // document is not one.
    "edda/crates/",
];

/// Is this citation in another repository, which this one cannot open?
pub fn is_foreign(file: &str) -> bool {
    FOREIGN.iter().any(|p| file.starts_with(p))
}

/// **Open a citation and read the line it names.**
///
/// Shared by both diagrams (one implementation, not two): `datapath_svg` wrote this first as a closure
/// inside its own test, and `cycle_svg` needs exactly the same check for exactly
/// the same reason. Two copies of it would be two places for the failure message
/// to drift.
///
/// Panics naming `who`, the address, the token that was wanted, the line that
/// was actually there, and **where the anchor now is** — which is the whole
/// value of it over `line > 0`.
///
/// # Why the line is written down rather than searched for
///
/// The obvious improvement is to drop the number and resolve it here: find the
/// anchor, cite the line it is found at, and a citation could never go stale.
/// It was considered on 2026-08-31, when two of these drifted in one afternoon,
/// and rejected for three reasons that are worth writing down so it is not
/// re-proposed every time one drifts:
///
/// 1. **The rendered picture would stop being a function of the model.** Both
///    diagrams print `file:line` into the SVG, and
///    [`assert_asset_fresh`] holds the committed asset to what
///    the model renders. Resolve lines from the source tree and the committed
///    SVG becomes a function of the whole repository: every edit anywhere above
///    a cited line turns the *asset* guard red and demands a regenerated,
///    committed SVG. That is the same staleness, moved somewhere more
///    expensive — a one-integer edit becomes a re-rendered file.
/// 2. **Half the citations cannot be resolved from here at all.** [`FOREIGN`]
///    addresses live in `edda` and `znippy`; nothing in this repository can
///    open them to find a line. They would keep written-down numbers while
///    in-repo ones were searched — two mechanisms for one datum, which is the
///    the twinning one implementation forbids, and the guard would be strong for the half it can
///    reach and absent for the half it cannot.
/// 3. **A search is only as sharp as the anchor is unique.** `select_objects`
///    occurs 19 times in `graph.rs`; searching would silently pick the doc
///    comment at the top of the file and call the citation good. Making every
///    anchor unique enough to search for is the same work as keeping the number
///    right, minus the proof that anyone did it.
///
/// So the number stays asserted, and the failure message does the work instead:
/// it names the line the anchor is on now, so fixing a drifted citation is
/// reading one number out of the panic rather than hunting for it.
pub fn assert_cites(root: &std::path::Path, file: &str, line: u32, anchor: &str, who: &str) {
    assert!(
        !anchor.is_empty(),
        "{who} cites {file}:{line} with no anchor, so nothing about that line is checked"
    );
    let path = root.join(file);
    let body = std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("{who} cites {file}, which will not open: {e}"));
    let lines: Vec<&str> = body.lines().collect();
    let idx = line as usize;
    assert!(
        idx >= 1 && idx <= lines.len(),
        "{who} cites {file}:{line} and the file has {} lines",
        lines.len()
    );
    let got = lines[idx - 1];
    if !got.contains(anchor) {
        // Where the anchor actually is, so a drifted citation is a one-number
        // fix rather than a hunt through a 3 000-line file.
        let found: Vec<usize> = lines
            .iter()
            .enumerate()
            .filter(|(_, l)| l.contains(anchor))
            .map(|(i, _)| i + 1)
            .collect();
        let now = match found.as_slice() {
            [] => "and it is nowhere in that file".to_string(),
            [n] => format!("and it is at line {n}"),
            many => format!(
                "and it is at lines {}",
                many.iter()
                    .map(usize::to_string)
                    .collect::<Vec<_>>()
                    .join(", ")
            ),
        };
        panic!(
            "{who} cites {file}:{line} for {anchor:?}, that line reads {:?}, {now}",
            got.trim()
        );
    }
}

/// Render and write. Returns the byte length so the caller can print an applied
/// output rather than "done".
pub fn write_asset(repo_root: &std::path::Path) -> std::io::Result<usize> {
    let svg = render(LANES);
    publish_owned_asset(repo_root, ASSET, svg.as_bytes())?;
    Ok(svg.len())
}

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

    /// The claim the legend makes, asserted on the markup.
    ///
    /// Red-first: swap `Wiring::Unwired`'s `dash()` to `None` and this fails.
    /// Without it, a legend that says "dashed means unwired" beside three
    /// identically-solid strokes renders perfectly and tells the reader the
    /// opposite of the truth.
    #[test]
    fn a_wiring_verdict_changes_the_markup() {
        assert!(
            Wiring::Wired.dash().is_none(),
            "a verified edge must be solid"
        );
        assert!(Wiring::Unwired.dash().is_some());
        assert!(Wiring::NotEmitted.dash().is_some());
        assert_ne!(Wiring::Unwired.dash(), Wiring::NotEmitted.dash());
        assert_ne!(Wiring::Unwired.stroke(), Wiring::Wired.stroke());
        assert_ne!(Wiring::NotEmitted.stroke(), Wiring::Wired.stroke());
    }

    #[test]
    fn every_unwired_section_renders_dashed_and_every_wired_one_does_not() {
        let svg = render(LANES);
        // A count alone is blind: it stays green while `Unwired` renders solid,
        // because `NotEmitted` still dashes. So assert the *specific* pattern
        // and the *specific* colour of each non-wired verdict, which is the
        // thing the legend promises.
        for w in [Wiring::Unwired, Wiring::NotEmitted] {
            let d = w.dash().expect("only Wired may be solid");
            assert!(
                svg.contains(&format!("stroke-dasharray=\"{d}\"")),
                "{:?} never rendered its dash pattern {d}",
                w
            );
            assert!(
                svg.contains(&format!("stroke=\"{}\"", w.stroke())),
                "{:?} never rendered its stroke {}",
                w,
                w.stroke()
            );
        }
        let dashed = svg.matches("stroke-dasharray").count();
        assert!(dashed > 10, "expected many dashed strokes, got {dashed}");

        // Each section's module name appears.
        for s in SECTIONS {
            assert!(svg.contains(s.module), "{} missing from the SVG", s.module);
        }
    }

    /// The stroke carries the verdict, and nothing writes it out as words.
    ///
    /// Red-first: put "WIRED" into any caption and this fails. It is the
    /// counterpart of the dash guard above — that one proves the stroke says
    /// something, this one proves the stroke is the only thing that does.
    #[test]
    fn no_verdict_is_printed_as_a_label() {
        let svg = render(LANES);
        for banned in ["WIRED", "BUILT, NOT READ", "NEVER EMITTED HERE"] {
            assert!(
                !svg.contains(banned),
                "the verdict {banned:?} is printed as text beside a box"
            );
        }
    }

    /// The register rules, asserted on the rendered bytes.
    ///
    /// Red-first: put "proven" into any caption and this fails.
    #[test]
    fn the_markup_uses_neither_banned_word() {
        let svg = render(LANES).to_lowercase();
        for banned in ["proven", " fair"] {
            assert!(!svg.contains(banned), "{banned:?} appears in the markup");
        }
    }

    #[test]
    fn the_markup_is_well_formed_enough_to_render() {
        let svg = render(LANES);
        assert!(svg.starts_with("<svg "), "no root element");
        assert!(svg.trim_end().ends_with("</svg>"), "unclosed root");
        // Every element this generator emits is self-closing except <text>,
        // <g>, <marker>, <linearGradient>, <defs> and <svg>. Count the opens
        // against the closes for the ones that nest.
        for tag in ["text", "g", "defs", "marker", "linearGradient", "svg"] {
            let open =
                svg.matches(&format!("<{tag} ")).count() + svg.matches(&format!("<{tag}>")).count();
            let close = svg.matches(&format!("</{tag}>")).count();
            assert_eq!(open, close, "<{tag}> opened {open} times, closed {close}");
        }
        // No raw ampersand survived escaping.
        for (i, _) in svg.match_indices('&') {
            // Char-safe: the SVG carries `·`, `→` and `−`, so a byte slice can
            // land mid-codepoint.
            let tail: String = svg[i..].chars().take(8).collect();
            let tail = tail.as_str();
            assert!(
                tail.starts_with("&amp;")
                    || tail.starts_with("&lt;")
                    || tail.starts_with("&gt;")
                    || tail.starts_with("&quot;")
                    || tail.starts_with("&#"),
                "unescaped & at {i}: {tail:?}"
            );
        }
    }

    /// Nothing may claim a `file:line` of 0 — that is what an un-filled datum
    /// looks like, and it renders indistinguishably from a real one.
    #[test]
    fn no_evidence_is_a_placeholder() {
        for s in SECTIONS {
            assert!(s.built_at.line > 0, "{}", s.module);
            assert!(s.built_at.file.contains('/'), "{}", s.module);
        }
        for l in LANES {
            for st in l.stages {
                if let Some(e) = st.ev {
                    assert!(e.line > 0, "{} / {}", l.op, st.label);
                }
            }
        }
    }

    /// The generator is pure: the committed asset can be regenerated and diffed.
    #[test]
    fn rendering_is_deterministic() {
        assert_eq!(render(LANES), render(LANES));
    }

    /// **The picture in the repository is the picture this model draws.**
    ///
    /// `rendering_is_deterministic` above says the generator repeats itself; it
    /// says nothing about the file anybody actually looks at. Nothing did, and
    /// the committed assets had drifted from the model that writes them — which
    /// is the citation disease one level out, since a stale SVG makes a claim
    /// about the code with no guard behind it at all.
    ///
    /// Red-first, seen: edit one character of `.nornir/assets/` or `assets/`'s
    /// copy and this fails naming the file and the first differing line.
    #[test]
    fn the_committed_asset_is_what_this_model_renders() {
        assert_asset_fresh(
            &crate::guard::workspace_root(),
            ASSET,
            &render(LANES),
            "cycle_svg",
        );
    }

    /// Nothing may be wider than the box it sits in.
    ///
    /// The first render put `ciphertext` and its type on top of each other and
    /// ran four lane captions off the right edge of the canvas, and **every**
    /// XML and content check above stayed green through all of it — the markup
    /// is perfectly valid when it is unreadable. This is the only guard here
    /// that looks at geometry, and it is the one that caught a real defect.
    #[test]
    fn no_label_overflows_the_box_it_sits_in() {
        // Panel 1 chips: label at 9.5pt mono on the left, type at 8.5pt mono
        // right-aligned, 6px padding each side.
        let cw = (W - 2.0 * M - 20.0 * (SECTIONS.len() as f64 - 1.0)) / SECTIONS.len() as f64;
        let inner = cw - 20.0 - 12.0;
        for s in SECTIONS {
            for (label, ty) in s.fields {
                if label.is_empty() {
                    continue;
                }
                let used = label.chars().count() as f64 * 9.5 * MONO_ADV
                    + ty.chars().count() as f64 * 8.5 * MONO_ADV
                    + 8.0; // minimum gap between the two runs
                assert!(
                    used <= inner,
                    "{} field `{label}` / `{ty}` needs {used:.0}px in a {inner:.0}px slot",
                    s.module
                );
            }
        }

        // Panel 2 stage boxes: label at 10.5pt mono, centred.
        for l in LANES {
            let ns = l.stages.len() as f64;
            let sw = (W - 2.0 * M - 26.0 * (ns - 1.0)) / ns;
            for st in l.stages {
                let used = st.label.chars().count() as f64 * 10.5 * MONO_ADV + 8.0;
                assert!(
                    used <= sw,
                    "{} stage `{}` needs {used:.0}px in {sw:.0}px",
                    l.op,
                    st.label
                );
            }
        }

        // Nothing anywhere may exceed the canvas. `para` wraps, so this is a
        // check that every long string actually goes through it.
        let svg = render(LANES);
        for line in svg.lines() {
            if let Some(rest) = line.split_once("<text x=\"") {
                let x: f64 = rest.1.split('"').next().unwrap().parse().unwrap();
                // `…" >BODY</text>` — take what sits between the last `>` of the
                // open tag and the `<` of the close tag.
                let body = line.split_once('>').map(|(_, b)| b).unwrap_or("");
                let body = body.split_once('<').map(|(b, _)| b).unwrap_or(body);
                let size: f64 = line
                    .split_once("font-size=\"")
                    .map(|(_, r)| r.split('"').next().unwrap().parse().unwrap())
                    .unwrap_or(11.0);
                let mono = line.contains("Mono");
                let anchored_end = line.contains("text-anchor=\"end\"");
                let anchored_mid = line.contains("text-anchor=\"middle\"");
                let w = body.chars().count() as f64 * size * if mono { MONO_ADV } else { PROP_ADV };
                let right = if anchored_end {
                    x
                } else if anchored_mid {
                    x + w / 2.0
                } else {
                    x + w
                };
                assert!(
                    right <= W + 2.0,
                    "text runs to {right:.0}px past the {W}px canvas: {body:?}"
                );
            }
        }
    }
}

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

    /// Every `Evidence` in the model, with who carries it.
    fn all() -> Vec<(Evidence, String)> {
        let mut v: Vec<(Evidence, String)> = Vec::new();
        for s in SECTIONS {
            v.push((s.built_at, format!("{} built_at", s.module)));
            if let Some(e) = s.produced_by {
                v.push((e, format!("{} produced_by", s.module)));
            }
            if let Some(e) = s.consumed_by {
                v.push((e, format!("{} consumed_by", s.module)));
            }
        }
        for lane in LANES {
            for stage in lane.stages {
                if let Some(e) = stage.ev {
                    v.push((e, format!("{} / {}", lane.op, stage.label)));
                }
            }
        }
        v
    }

    /// **Every citation this repository can open is opened and read.**
    ///
    /// `datapath_svg` had this guard and `cycle_svg` did not, and the difference
    /// showed: on 2026-08-05 **five** of these pointed somewhere else —
    /// the deleted engine crate’s lib.rs:186 and `gunnar-wire/src/receive_pack.rs:595`
    /// were both a bare `}`, `serve_session` was 5 lines past its `fn`, `ref CAS`
    /// 2 past its `MustExistAndMatch`, and `Stores::secrets()` 3 past its `fn`.
    /// All five were found by reading the lines, which is exactly the labour a
    /// guard exists to delete.
    ///
    /// Red-first: change any line number by one, or any anchor to a token that
    /// is not on that line, and this fails naming the box, the address, what it
    /// wanted and what it found.
    #[test]
    fn every_citation_this_repository_holds_still_points_at_what_it_names() {
        let root = crate::guard::workspace_root();
        let (mut opened, mut foreign) = (0usize, 0usize);
        for (ev, who) in all() {
            if is_foreign(ev.file) {
                // Not skipped quietly: counted, and the count is asserted below,
                // so a path that stops being foreign — or a new one that starts
                // — moves a number rather than sliding through.
                assert!(
                    ev.anchor.is_empty(),
                    "{who} cites another repository and still carries an anchor this one \
                     cannot check"
                );
                foreign += 1;
                continue;
            }
            assert_cites(&root, ev.file, ev.line, ev.anchor, &who);
            opened += 1;
        }
        // The control: a loop over an empty model is green, and so is one where
        // every citation quietly became foreign.
        assert_eq!(
            opened + foreign,
            all().len(),
            "not every citation was accounted for"
        );
        // The floor was 20 and is 10, and the reason is a MEASUREMENT rather
        // than a concession: on 2026-08-09 `gunnar-pack`, `gunnar-bitmap` and
        // the gix storage arm left for edda, so most of this model now cites
        // another repository. Exactly 12 citations remain openable from here.
        //
        // The floor is not what makes this guard work — `opened + foreign ==
        // all().len()` above is, because it cannot be satisfied by a citation
        // going missing. The floor only catches the model being gutted, and 10
        // still catches that. Raise it again the day the crates come home.
        assert!(
            opened >= 10,
            "only {opened} citations were opened; the guard has stopped reaching the model"
        );
        assert!(
            foreign > 0,
            "no citation is foreign any more, so the FOREIGN branch is dead and untested"
        );
    }

    /// Nothing may claim a line of 0 or a bare file name: both render exactly
    /// like a real citation.
    #[test]
    fn no_citation_is_a_placeholder() {
        for (ev, who) in all() {
            assert!(ev.line > 0, "{who} cites line 0");
            assert!(
                ev.file.contains('/'),
                "{who} cites a bare file {:?}",
                ev.file
            );
        }
    }
}