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

nordisk/gunnar

xtask/src/datapath_svg.rs

//! `cargo xtask svg` — the read path and the write path as data structures,
//! with the complexity each one costs and the `file:line` it lives at.
//!
//! # What this diagram is for
//!
//! A reader who wants to know what a fetch costs has to open eleven files to
//! find out that one byte-range copy owns nine tenths of it. This draws the two
//! paths as the structures they run over, annotates each with its complexity,
//! and prints the `file:line` so the code can be opened from the picture.
//!
//! It is a technical manual page. A box says **what the structure is**, **its
//! O()**, and **where it lives** — nothing else. No verdict is written out as
//! prose: a node's [`Wiring`] picks its stroke, so a node no server build
//! constructs cannot be drawn solid without changing the datum.
//!
//! # Every number carries its conditions
//!
//! Cost shares are not free-floating percentages. Each panel names the run its
//! shares come from in its subheading, and a node that has no measured share
//! prints none rather than a plausible one. The two runs quoted here are:
//!
//! * **the gix census** — the desktop 2026-08-04,
//!   tree `1a4df9a`, the 19 mirrored `workspace_nordisk` repositories,
//!   65 737 objects, timed arms pinned with `taskset -c 24-27`;
//! * **the oid-lookup probe** — same box, same
//!   day, same 19 repositories, `perf record -F 4999`, 8 238 samples.
//!
//! # Style and reuse
//!
//! Every primitive — the board, the gradients, the arrow marker, the title
//! band, [`Canvas`], [`Txt`], the wrapper — comes from [`crate::cycle_svg`]
//! (one implementation, not two). This module contributes a data model and a layout, and not one line
//! of SVG plumbing.

use std::fmt::Write as _;

use crate::cycle_svg::{heading, svg_header, wrap, Anchor, Canvas, Evidence, Txt, Wiring, M, W};

// ════════════════════════════════════════════════════════════════════════════
// The data model
// ════════════════════════════════════════════════════════════════════════════

/// One data structure on a path.
///
/// The four fields are exactly what a manual entry needs: the name to grep for,
/// what the structure is, what it costs asymptotically, and where it lives.
pub struct Node {
    /// The type or function, as it is spelled in the source.
    pub name: &'static str,
    /// What the structure is, in one phrase. Not what it achieves.
    pub what: &'static str,
    /// The complexity, in the variable the path actually scales in.
    pub big_o: &'static str,
    /// The measured share of the request, with the run named by the panel
    /// subheading. Empty when this lane has no measurement for the node — a
    /// blank is the honest rendering of an unmeasured stage.
    pub share: &'static str,
    pub ev: Evidence,
    /// A token that must still be on `ev`'s line for the citation to be an
    /// address rather than a memory of one. Defaults to `name`.
    ///
    /// A `file:line` is the one datum in this diagram that rots without
    /// rendering differently: the picture keeps drawing, the number keeps
    /// looking like a number, and it points at a closing brace. The guard in
    /// this module's tests opens the file and looks, which is the only check
    /// that can go red on drift.
    pub anchor: &'static str,
    /// Picks the stroke. Nothing prints it as text.
    pub wire: Wiring,
}

impl Node {
    pub const fn new(
        name: &'static str,
        what: &'static str,
        big_o: &'static str,
        ev: Evidence,
    ) -> Self {
        Self {
            name,
            what,
            big_o,
            share: "",
            ev,
            anchor: name,
            wire: Wiring::Wired,
        }
    }
    pub const fn anchor(mut self, a: &'static str) -> Self {
        self.anchor = a;
        self
    }
    pub const fn share(mut self, s: &'static str) -> Self {
        self.share = s;
        self
    }
    pub const fn wire(mut self, w: Wiring) -> Self {
        self.wire = w;
        self
    }
}

/// A path: a row of nodes, plus the nodes that sit *inside* one of them.
pub struct Path {
    pub title: &'static str,
    pub verb: &'static str,
    /// What enters and what leaves, both in bytes.
    pub bytes_in: &'static str,
    pub bytes_out: &'static str,
    pub nodes: &'static [Node],
    /// Index into `nodes` of the stage the `inner` nodes are *nested inside*,
    /// when they genuinely are. `None` draws the sub-row without a containment
    /// stub, because a stub that points at a stage the sub-row is not inside is
    /// a relation the picture asserts and the code does not have.
    pub inner_of: Option<usize>,
    /// What the sub-row is, in its own words.
    pub inner_label: &'static str,
    /// The structures drawn beneath the main row.
    pub inner: &'static [Node],
    /// The one structural fact the row of boxes cannot draw.
    pub tail: &'static str,
}

/// The read path: `git fetch` and `git clone`, answered by `upload-pack`.
///
/// Shares are the gix census stage frame: `pack_write` 89.98% wall / 90.59%
/// CPU, `select.bitmap` 9.21 / 9.41, everything else under 0.7%
/// (the gix census).
pub const READ: Path = Path {
    title: "read",
    verb: "git fetch · git clone — upload-pack",
    bytes_in: "ls-refs, want/have",
    bytes_out: "PACK, sideband-muxed",
    nodes: &[
        Node::new(
            "ls_refs",
            "ref advertisement, gix-ref file store",
            "O(refs)",
            Evidence::at("crates/gunnar-wire/src/upload_pack.rs", 850),
        )
        .share("under 0.5% CPU"),
        Node::new(
            "HaveMemo",
            "connection-lifetime set, one answer per distinct have",
            "O(distinct haves)",
            Evidence::at("crates/gunnar-wire/src/negotiate.rs", 150),
        )
        .share("under 0.5% CPU"),
        Node::new(
            "bitmap::select",
            "EWAH run-length maps, want ANDNOT have",
            "O(bitmap words)",
            Evidence::at("crates/gunnar-wire/src/reachable.rs", 86),
        )
        .anchor("fn select(")
        .share("9.41% CPU"),
        Node::new(
            "select_objects",
            "commit/tree closure walk, when no bitmap answers",
            "O(reachable objects)",
            Evidence::at("crates/gunnar-wire/src/graph.rs", 2315),
        )
        // The node name alone occurs 30 times in `graph.rs` — mostly in doc
        // comments — so it could sit on any of them and read as checked. The
        // signature occurs once.
        .anchor("fn select_objects("),
        Node::new(
            "Fanout16::lookup",
            "65 536 buckets, then bisect inside one",
            "O(log(n/65 536))",
            Evidence::at("edda/crates/storage-git-gix/src/fanout.rs", 196),
        )
        .anchor("fn lookup<")
        .share("5.45/obj · 47 ns · 0.93%"),
        Node::new(
            "write_pack_copy_thin",
            "byte-range copy of already-deflated entries",
            "O(bytes)",
            Evidence::at("edda/crates/gunnar-pack/src/copy.rs", 1258),
        )
        .share("89.98% wall · 90.59% CPU"),
    ],
    inner_of: None,
    inner_label: "the object tier the copy reads through, and the channel it writes out",
    inner: &[
        Node::new(
            "ObjectStore::packed",
            "the seam a pack tier answers on; None means write_pack",
            "O(1)",
            Evidence::at("plugins/native/znippy-plugin-git/src/git_ops.rs", 1141),
        )
        // No `.anchor(…)`: the pack-copy seam left gunnar with the packfile engine,
        // and a cross-repo line cannot be anchor-checked from here.
        .wire(Wiring::Unwired),
        Node::new(
            "SidebandWriter",
            "pkt-line channel 1, muxed out",
            "O(bytes)",
            Evidence::at("crates/gunnar-wire/src/wire_io.rs", 173),
        ),
        Node::new(
            "ZnippyObjects",
            "third ObjectStore, serves out of a sealed archive",
            "O(bytes)",
            Evidence::at("crates/gunnar-server/src/storage/znippy_engine.rs", 388),
        )
        .anchor("pub fn open(")
        .wire(Wiring::Unwired),
    ],
    tail: "A bitmap replaces the walk, it does not shrink it: when one answers, select_objects is \
           absent from the stage frame entirely. Nothing on this path inflates an object. The \
           entry travels as it lies — the header is rewritten, the deflated byte range is copied \
           — so the cost is O(bytes on the wire) and is flat in delta depth: 0.034 µs/object at \
           depth 0 against 0.022 µs at depth 50 (the desktop 2026-08-04).",
};

/// The write path: `git push`, answered by `receive-pack`.
///
/// Shares are the gix census push table: `Bundle::write_to_directory` 96.67% of
/// wall and 93.02% of attributed CPU, and inside it SHA-1 64.81% of cycles and
/// inflate 18.29% (the gix census, rows P1–P3).
pub const WRITE: Path = Path {
    title: "write",
    verb: "git push — receive-pack",
    bytes_in: "command list + PACK",
    bytes_out: "pack-<id>.pack/.idx + ref CAS",
    nodes: &[
        Node::new(
            "index_thin_pack",
            "streams the pack, writes .pack + .idx + .keep",
            "O(bytes)",
            Evidence::at("edda/crates/gunnar-pack/src/read.rs", 138),
        )
        .share("96.67% wall · 93.02% CPU"),
        Node::new(
            "verify_connectivity",
            "every new tip reaches the frontier",
            "O(new objects)",
            Evidence::at("crates/gunnar-wire/src/graph.rs", 2832),
        )
        // Same reason as `select_objects` above: the bare name sits on a `}`
        // just as convincingly as on the item.
        .anchor("fn verify_connectivity(")
        .share("6.66% CPU · 1 412 exists/req"),
        Node::new(
            "RefLog::push",
            "one Arrow batch of ref updates under one push_seq",
            "O(edits)",
            Evidence::at("plugins/native/znippy-plugin-git/src/refs.rs", 130),
        )
        .share("0.32% CPU"),
        Node::new(
            "ArrowLog::append",
            "one RecordBatch as one framed IPC message",
            "O(frame bytes)",
            // Moved 2026-08-10: `arrow_log.rs` left `gunnar-store` for
            // `gunnar-core` so the contract crate both `GitOps` backends
            // dev-depend on stops carrying Arrow. Same file, same function,
            // re-anchored rather than dropped.
            Evidence::at("crates/gunnar-core/src/arrow_log.rs", 843),
        )
        .anchor("fn append("),
    ],
    inner_of: Some(0),
    inner_label: "inside index_thin_pack",
    inner: &[
        Node::new(
            "gix_hash::hasher",
            "SHA-1 with UBC collision detection, inside gix",
            "O(bytes)",
            Evidence::at("edda/crates/gunnar-pack/src/read.rs", 159),
        )
        .anchor("Bundle::write_to_directory")
        .share("64.81% cycles"),
        Node::new(
            "inflate",
            "zlib stream decode and delta resolution",
            "O(bytes)",
            Evidence::at("edda/crates/gunnar-pack/src/read.rs", 159),
        )
        .anchor("Bundle::write_to_directory")
        .share("18.29% cycles"),
        Node::new(
            "memmove / memset",
            "kernel page init behind the two above",
            "O(bytes)",
            Evidence::at("edda/crates/gunnar-pack/src/read.rs", 159),
        )
        .anchor("Bundle::write_to_directory")
        .share("4.27% cycles"),
    ],
    tail: "The frame boundary is the transaction boundary: an Arrow IPC frame is [schema | batch \
           | EOS], and a frame that does not parse whole is discarded whole. Everything upstream \
           of it is bytes-bound, not object-bound, and SHA-1 owns two thirds of that — index \
           depth does not enter: index_thin_pack is 1.24× faster at depth 50 than at depth 1 \
           (the delta-chain measurement, the desktop 2026-08-04).",
};

// ════════════════════════════════════════════════════════════════════════════
// The store, as structures rather than as a path
// ════════════════════════════════════════════════════════════════════════════

/// One structure in the pack, with what addresses it and what that costs.
pub struct Store {
    pub name: &'static str,
    pub what: &'static str,
    pub keyed_by: &'static str,
    pub big_o: &'static str,
    pub ev: Evidence,
    /// Same contract as [`Node::anchor`].
    pub anchor: &'static str,
}

pub const STORE: &[Store] = &[
    Store {
        name: "pack entry",
        what: "type + size header, then a deflated byte range",
        keyed_by: "offset",
        big_o: "O(1) locate · O(bytes) copy",
        ev: Evidence::at("edda/crates/gunnar-pack/src/copy.rs", 1452),
        anchor: "options.ofs_delta",
    },
    Store {
        name: "OfsDelta",
        what: "base named as a backwards distance from this offset",
        keyed_by: "offset − distance",
        big_o: "O(depth) · 0 resolved serving",
        ev: Evidence::at("edda/crates/gunnar-pack/src/copy.rs", 1454),
        anchor: "base_offset",
    },
    Store {
        name: "RefDelta",
        what: "base named by oid, possibly outside this pack",
        keyed_by: "oid",
        big_o: "O(depth) + 1 probe per link",
        ev: Evidence::at("edda/crates/gunnar-pack/src/copy.rs", 1479),
        anchor: "RefDelta",
    },
    Store {
        name: ".idx fanout",
        what: "256 buckets over the first oid byte, sorted oids after",
        keyed_by: "oid",
        big_o: "O(log(n/256))",
        ev: Evidence::at("edda/crates/storage-git-gix/src/bitmap.rs", 236),
        anchor: "self.fanout",
    },
    Store {
        name: "Fanout16",
        what: "65 536 buckets over the first two bytes, in front of the .idx",
        keyed_by: "oid",
        big_o: "O(log(n/65 536))",
        ev: Evidence::at("edda/crates/storage-git-gix/src/fanout.rs", 67),
        anchor: "struct Fanout16",
    },
    Store {
        name: ".bitmap",
        what: "4 type EWAHs, then one run-length map per selected commit",
        keyed_by: "commit oid",
        big_o: "O(words), not O(objects)",
        ev: Evidence::at("edda/crates/gunnar-bitmap/src/file.rs", 133),
        anchor: "fn at(",
    },
];

/// The depth distribution the two paths run over, and the conditions it was
/// counted under. Drawn because the whole delta-chain question turns on it.
const DEPTH_BODY: &str =
    "git verify-pack -v over the single pack in each of the 19 mirrored workspace_nordisk \
     repositories, 60 739 objects, the desktop 2026-08-04. Max depth \
     50 — git's default cap — and five of the nineteen sit on it. Mean depth 4.32 counted by \
     object and 0.250 weighted by byte. Objects at depth ≥ 5 are 26.2% of objects and 0.70% of \
     raw bytes: deep chains sit on tiny objects, and the largest object at depth ≥ 25 anywhere \
     in the corpus is 4 716 bytes.";

/// The asymmetry the two panels exist to show, stated once.
const ASYMMETRY_READ: &str =
    "Read copies. The bytes leave the pack in the encoding they were stored in, so the cost is \
     the length of the answer and nothing on the path scales in object count or chain depth. \
     One byte-range copy owns 89.98% of the wall.";
const ASYMMETRY_WRITE: &str =
    "Write hashes. Every byte received is inflated once and hashed with collision detection, so \
     the cost is the length of the input and 64.81% of it is one hash. Both engines on this arm \
     are SHA-1-bound, neither is delta-bound.";

// ════════════════════════════════════════════════════════════════════════════
// Layout
// ════════════════════════════════════════════════════════════════════════════

/// Node box geometry. Six across the canvas is the widest row that still fits
/// `write_pack_copy_thin` at 11 pt mono inside its own box, which is the
/// binding constraint and is asserted in the tests.
const NODE_GAP: f64 = 24.0;
const NODE_H: f64 = 84.0;
const ROW: f64 = 6.0;

fn node_w() -> f64 {
    (W - 2.0 * M - NODE_GAP * (ROW - 1.0)) / ROW
}

/// Draw one node box. The `Wiring` reaches pixels only through `rect_wired`, so
/// there is no path by which an unwired node renders solid.
fn draw_node(c: &mut Canvas, x: f64, y: f64, w: f64, h: f64, node: &Node) {
    c.rect_wired(x, y, w, h, 8.0, "url(#chip)", node.wire, 1.8);
    let cx = x + w / 2.0;
    c.txt(
        Txt::new(cx, y + 17.0, node.name)
            .fill("#eaf6ff")
            .size(11.0)
            .bold()
            .anchor(Anchor::Middle)
            .mono(),
    );
    // What it is, wrapped: two lines at 8.4 pt inside the box.
    for (i, l) in wrap(node.what, ((w - 12.0) / 4.6) as usize)
        .into_iter()
        .take(2)
        .enumerate()
    {
        c.txt(
            Txt::new(cx, y + 30.0 + i as f64 * 10.0, &l)
                .fill("#9fc9e0")
                .size(8.4)
                .anchor(Anchor::Middle),
        );
    }
    // The complexity, on its own line and in the signal colour: it is the one
    // thing this diagram exists to carry.
    c.txt(
        Txt::new(cx, y + 57.0, node.big_o)
            .fill("#6fd3f5")
            .size(9.6)
            .bold()
            .anchor(Anchor::Middle)
            .mono(),
    );
    if !node.share.is_empty() {
        c.txt(
            Txt::new(cx, y + 69.0, node.share)
                .fill("#a7e6c2")
                .size(8.0)
                .anchor(Anchor::Middle)
                .mono(),
        );
    }
    c.txt(
        Txt::new(cx, y + h - 5.0, &node.ev.crate_label())
            .fill("#5f8ba6")
            .size(7.6)
            .anchor(Anchor::Middle)
            .mono(),
    );
}

fn panel_path(c: &mut Canvas, y0: f64, p: &Path, num: &str, sub: &str) -> f64 {
    c.comment(&format!("PATH - {}", p.title));
    heading(c, y0, num, p.verb, sub);

    let mut y = y0 + 44.0;

    // The lane rail: what enters, what leaves.
    c.rect(M, y, W - 2.0 * M, 20.0, 5.0, "url(#rail)", "#3c6a86", 1.0);
    c.txt(
        Txt::new(M + 10.0, y + 14.5, &format!("in: {}", p.bytes_in))
            .fill("#bfe6ff")
            .size(9.2)
            .mono(),
    );
    c.txt(
        Txt::new(W - M - 10.0, y + 14.5, &format!("out: {}", p.bytes_out))
            .fill("#bfe6ff")
            .size(9.2)
            .anchor(Anchor::End)
            .mono(),
    );
    y += 30.0;

    let w = node_w();
    let top = y;
    for (i, node) in p.nodes.iter().enumerate() {
        let x = M + i as f64 * (w + NODE_GAP);
        draw_node(c, x, top, w, NODE_H, node);
        if i + 1 < p.nodes.len() {
            c.arrow_h(
                x + w + 3.0,
                x + w + NODE_GAP - 1.0,
                top + NODE_H / 2.0,
                node.wire,
            );
        }
    }

    // The sub-row. When it really is nested, a stub from the enclosing node to
    // a bracket over the row draws the containment; when it is not, the row
    // carries its own caption and no stub is drawn.
    let iy = top + NODE_H + 30.0;
    let inner_w = w * 0.92;
    let span = p.inner.len() as f64 * inner_w + (p.inner.len() as f64 - 1.0) * NODE_GAP;
    c.line(
        M,
        iy - 14.0,
        M + span,
        iy - 14.0,
        "#4a8fb0",
        1.2,
        Some("4 4"),
    );
    if let Some(k) = p.inner_of {
        let cx = M + k as f64 * (w + NODE_GAP) + w / 2.0;
        c.line(cx, top + NODE_H, cx, iy - 14.0, "#4a8fb0", 1.4, Some("4 4"));
    }
    c.txt(
        Txt::new(M, iy - 21.0, p.inner_label)
            .fill("#7fa8c4")
            .size(8.6),
    );
    for (i, node) in p.inner.iter().enumerate() {
        let x = M + i as f64 * (inner_w + NODE_GAP);
        draw_node(c, x, iy, inner_w, NODE_H, node);
    }

    let end = c.para(M, iy + NODE_H + 18.0, W - 2.0 * M, p.tail, "#bcd9ec", 9.4);
    end + 6.0
}

fn panel_store(c: &mut Canvas, y0: f64) -> f64 {
    c.comment("STORE - what the two paths address, and by what key");
    heading(
        c,
        y0,
        "3",
        "the pack, as structures",
        "what addresses each one, and what a probe into it costs",
    );

    let top = y0 + 44.0;
    let gap = 18.0;
    let w = (W - 2.0 * M - gap * (STORE.len() as f64 - 1.0)) / STORE.len() as f64;
    let h = 94.0;

    for (i, s) in STORE.iter().enumerate() {
        let x = M + i as f64 * (w + gap);
        c.rect_wired(x, top, w, h, 8.0, "url(#slot)", Wiring::Wired, 1.6);
        let cx = x + w / 2.0;
        c.txt(
            Txt::new(cx, top + 17.0, s.name)
                .fill("#eaf6ff")
                .size(10.6)
                .bold()
                .anchor(Anchor::Middle)
                .mono(),
        );
        for (k, l) in wrap(s.what, ((w - 12.0) / 4.4) as usize)
            .into_iter()
            .take(3)
            .enumerate()
        {
            c.txt(
                Txt::new(cx, top + 30.0 + k as f64 * 9.6, &l)
                    .fill("#9fc9e0")
                    .size(8.2)
                    .anchor(Anchor::Middle),
            );
        }
        c.txt(
            Txt::new(cx, top + 66.0, &format!("keyed by {}", s.keyed_by))
                .fill("#86b3cc")
                .size(8.0)
                .anchor(Anchor::Middle)
                .mono(),
        );
        for (k, l) in wrap(s.big_o, ((w - 8.0) / 5.3) as usize)
            .into_iter()
            .take(2)
            .enumerate()
        {
            c.txt(
                Txt::new(cx, top + 78.0 + k as f64 * 9.0, &l)
                    .fill("#6fd3f5")
                    .size(8.8)
                    .bold()
                    .anchor(Anchor::Middle)
                    .mono(),
            );
        }
        c.txt(
            Txt::new(cx, top + h + 11.0, &s.ev.crate_label())
                .fill("#5f8ba6")
                .size(7.6)
                .anchor(Anchor::Middle)
                .mono(),
        );
    }

    let end = c.para(M, top + h + 28.0, W - 2.0 * M, DEPTH_BODY, "#bcd9ec", 9.4);
    end + 6.0
}

/// The closing band. Two columns, because the point is a contrast and a
/// contrast drawn as one paragraph is a paragraph.
fn asymmetry(c: &mut Canvas, y: f64) -> f64 {
    c.comment("the asymmetry: read copies, write hashes");
    let colw = (W - 2.0 * M - 56.0) / 2.0;
    let mut probe = Canvas::default();
    let a = probe.para(0.0, 0.0, colw - 32.0, ASYMMETRY_READ, "#bcd9ec", 10.0);
    let b = probe.para(0.0, 0.0, colw - 32.0, ASYMMETRY_WRITE, "#bcd9ec", 10.0);
    let h = 30.0 + a.max(b) + 14.0;

    for (i, (title, body)) in [("read", ASYMMETRY_READ), ("write", ASYMMETRY_WRITE)]
        .iter()
        .enumerate()
    {
        let x = M + i as f64 * (colw + 56.0);
        c.rect(x, y, colw, h, 10.0, "url(#rail)", "#7fc8ea", 2.0);
        c.txt(
            Txt::new(x + 16.0, y + 22.0, title)
                .fill("#dff1ff")
                .size(14.0)
                .bold()
                .mono(),
        );
        c.para(x + 16.0, y + 38.0, colw - 32.0, body, "#bcd9ec", 10.0);
    }
    y + h
}

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

/// The run the read and write shares come from, printed where they are drawn.
const CENSUS_CONDITIONS: &str =
    "shares: gix census, the desktop 2026-08-04, tree 1a4df9a, 19 workspace_nordisk repositories, \
     65 737 objects, timed arms pinned taskset -c 24-27";

/// Render. Pure: same input, same bytes, so the committed asset can be diffed.
pub fn render() -> String {
    let mut c = Canvas::default();

    let mut y = panel_path(&mut c, 108.0, &READ, "1", CENSUS_CONDITIONS);
    y = panel_path(&mut c, y + 22.0, &WRITE, "2", CENSUS_CONDITIONS);
    y = panel_store(&mut c, y + 22.0);
    y = asymmetry(&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 read path and the write path \u{2014} data structures, complexity, and where each one lives",
        "every box carries the file:line it lives at \u{00b7} every share carries the run it was measured on",
    ));

    // Legend. The stroke carries the verdict; this says what a stroke means,
    // and it is the only place a verdict appears as words.
    let _ = write!(
        out,
        r##"  <g font-size="11">
    <line x1="{lx}" y1="34" x2="{lx2}" y2="34" stroke="#6fd3f5" stroke-width="3"/>
    <text x="{tx}" y="38" fill="#cfe6f5">on a server build's path</text>
    <line x1="{lx}" y1="54" x2="{lx2}" y2="54" stroke="#e0a86a" stroke-width="2.5" stroke-dasharray="7 5"/>
    <text x="{tx}" y="58" fill="#cfe6f5">built, no server build constructs it</text>
  </g>
"##,
        lx = crate::cycle_svg::n(W - M - 244.0),
        lx2 = crate::cycle_svg::n(W - M - 208.0),
        tx = crate::cycle_svg::n(W - M - 200.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 [`crate::cycle_svg::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-data-path.svg";

/// Render and write, to every asset directory this asset is published to.
/// Returns the byte length so the caller prints an applied output rather than
/// "done".
pub fn write_asset(repo_root: &std::path::Path) -> std::io::Result<usize> {
    let svg = render();
    crate::cycle_svg::publish_owned_asset(repo_root, ASSET, svg.as_bytes())?;
    Ok(svg.len())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cycle_svg::{MONO_ADV, PROP_ADV};

    fn all_nodes() -> Vec<&'static Node> {
        let mut v: Vec<&Node> = Vec::new();
        for p in [&READ, &WRITE] {
            v.extend(p.nodes.iter());
            v.extend(p.inner.iter());
        }
        v
    }

    /// Every node states a complexity, and it is a complexity rather than a
    /// word. A blank `big_o` renders as an empty line and is indistinguishable
    /// from a node whose cost nobody worked out.
    ///
    /// Red-first: blank any `big_o` above and this fails.
    #[test]
    fn every_node_carries_a_complexity() {
        for n in all_nodes() {
            assert!(!n.big_o.is_empty(), "{} has no complexity", n.name);
            assert!(
                n.big_o.starts_with("O("),
                "{}'s complexity {:?} is not one",
                n.name,
                n.big_o
            );
        }
        for s in STORE {
            assert!(s.big_o.starts_with("O("), "{} : {:?}", s.name, s.big_o);
        }
    }

    /// **Every citation is opened and read.**
    ///
    /// `line > 0` and `file.contains('/')` cannot fail on a citation that has
    /// drifted — the number stays a number and the picture keeps rendering
    /// while it points at a closing brace. This guard opens the file at the
    /// cited line and looks for the token the box names.
    ///
    /// Red-first: change any line number by one, or any anchor to a token that
    /// is not there, and it fails naming the box and what it found instead.
    /// Run against `.nornir/assets/gunnar-znippy-cycle.svg`'s data model it goes
    /// red on 8 of 24 citations, which is what a stale `file:line` looks like.
    #[test]
    fn every_citation_still_points_at_what_it_names() {
        let root = crate::guard::workspace_root();
        let mut checked = 0usize;
        // `cycle_svg::assert_cites` is the one reader of a citation in this
        // crate; this test wrote it first and `cycle_svg`'s guard needs the same
        // check, so it moved rather than being copied (one implementation, not two).
        let mut foreign = 0usize;
        let mut cite = |file: &str, line: u32, anchor: &str, who: &str| {
            // A citation into another repository cannot be opened from here, so
            // it is COUNTED rather than checked — and counted rather than
            // skipped, so a path that stops being foreign moves a number instead
            // of sliding through. `gunnar-pack`, `gunnar-bitmap` and the gix
            // storage arm all left for edda on 2026-08-09 and half this diagram
            // now points there.
            if crate::cycle_svg::is_foreign(file) {
                foreign += 1;
                checked += 1;
                return;
            }
            crate::cycle_svg::assert_cites(&root, file, line, anchor, who);
            checked += 1;
        };
        for n in all_nodes() {
            cite(n.ev.file, n.ev.line, n.anchor, n.name);
        }
        for s in STORE {
            cite(s.ev.file, s.ev.line, s.anchor, s.name);
        }
        // The count is the control: a loop over an empty collection is green.
        assert_eq!(
            checked,
            all_nodes().len() + STORE.len(),
            "not every citation was opened"
        );
        assert!(checked >= 20, "only {checked} citations exist to check");
        assert!(
            foreign > 0,
            "no citation is foreign any more, so that branch is dead and untested"
        );
    }

    /// Nothing may claim a `file:line` of 0, and nothing may name a bare file:
    /// that is what an un-filled datum looks like, and it renders exactly like
    /// a real one.
    #[test]
    fn no_evidence_is_a_placeholder() {
        for n in all_nodes() {
            assert!(n.ev.line > 0, "{} has line 0", n.name);
            assert!(n.ev.file.contains('/'), "{} has a bare file", n.name);
        }
        for s in STORE {
            assert!(s.ev.line > 0, "{}", s.name);
            assert!(s.ev.file.contains('/'), "{}", s.name);
        }
    }

    /// A wiring verdict has to change the markup, or the legend is a caption
    /// over two identical strokes.
    ///
    /// Red-first: give `Wiring::Unwired` a `dash()` of `None` and this fails.
    /// Asserting the *specific* pattern and colour rather than a count, because
    /// a count stays green while `Unwired` renders solid.
    #[test]
    fn an_unwired_node_cannot_render_solid() {
        let unwired: Vec<&Node> = all_nodes()
            .into_iter()
            .filter(|n| !matches!(n.wire, Wiring::Wired))
            .collect();
        assert!(
            !unwired.is_empty(),
            "the guard needs at least one unwired node to be able to fail"
        );
        let svg = render();
        // Driven off the verdicts the model actually carries rather than a
        // hand-written list: add a `NotEmitted` node tomorrow and this checks
        // it without being edited, and it cannot silently check nothing,
        // because the emptiness assertion above is what makes that a failure.
        for n in &unwired {
            let d = n.wire.dash().expect("only Wired may be solid");
            assert!(
                svg.contains(&format!(
                    "stroke=\"{}\" stroke-width=\"1.8\" stroke-dasharray=\"{d}\"",
                    n.wire.stroke()
                )),
                "{}'s verdict {:?} never rendered as a dashed box",
                n.name,
                n.wire
            );
        }
        // And the solid case really is solid, so the two are distinguishable.
        assert!(
            svg.contains(&format!(
                "stroke=\"{}\" stroke-width=\"1.8\"/>",
                Wiring::Wired.stroke()
            )),
            "no wired node rendered solid"
        );
    }

    /// No verdict is written out as prose anywhere in the markup. The stroke
    /// carries it; the legend defines the stroke.
    ///
    /// Red-first: add `.badge()` back to `draw_node` and this fails.
    #[test]
    fn no_verdict_is_printed_as_a_label() {
        let svg = render();
        for banned in ["WIRED", "BUILT, NOT READ", "NEVER EMITTED HERE"] {
            assert!(
                !svg.contains(banned),
                "the verdict {banned:?} is printed as text; the stroke is what carries it"
            );
        }
    }

    /// The register rules, asserted on the rendered bytes rather than trusted.
    ///
    /// Red-first: put "proven" into any caption and this fails.
    #[test]
    fn the_markup_uses_neither_banned_word() {
        let svg = render().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();
        assert!(svg.starts_with("<svg "), "no root element");
        assert!(svg.trim_end().ends_with("</svg>"), "unclosed root");
        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}");
        }
        for (i, _) in svg.match_indices('&') {
            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:?}"
            );
        }
    }

    #[test]
    fn rendering_is_deterministic() {
        assert_eq!(render(), render());
    }

    /// **The picture in the repository is the picture this model draws.**
    ///
    /// MEASURED 2026-08-05 at `fab9541`, which is what this was written for:
    /// both committed copies of `gunnar-data-path.svg` printed `copy.rs:608`
    /// where the model said `1231` and `copy.rs:790` where it said `1452`, while
    /// every citation guard was green. The model was right and the shipped
    /// picture was two refactors behind it.
    ///
    /// Red-first, seen: change one line number in the model, or one character in
    /// either committed copy, and this fails naming the file and the line.
    #[test]
    fn the_committed_asset_is_what_this_model_renders() {
        crate::cycle_svg::assert_asset_fresh(
            &crate::guard::workspace_root(),
            ASSET,
            &render(),
            "datapath_svg",
        );
    }

    /// Nothing may be wider than the box it sits in, and nothing may run off
    /// the canvas. Valid markup is not readable markup — this is the only guard
    /// here that looks at geometry.
    #[test]
    fn no_label_overflows_the_box_it_sits_in() {
        let w = node_w();
        for p in [&READ, &WRITE] {
            for n in p.nodes {
                let used = n.name.chars().count() as f64 * 11.0 * MONO_ADV + 8.0;
                assert!(
                    used <= w,
                    "{} needs {used:.0}px in a {w:.0}px node box",
                    n.name
                );
                let o = n.big_o.chars().count() as f64 * 9.6 * MONO_ADV + 8.0;
                assert!(
                    o <= w,
                    "{}'s {} needs {o:.0}px in {w:.0}px",
                    n.name,
                    n.big_o
                );
                let s = n.share.chars().count() as f64 * 8.0 * MONO_ADV + 8.0;
                assert!(s <= w, "{}'s share needs {s:.0}px in {w:.0}px", n.name);
            }
            let iw = w * 0.92;
            for n in p.inner {
                let used = n.name.chars().count() as f64 * 11.0 * MONO_ADV + 8.0;
                assert!(used <= iw, "{} needs {used:.0}px in {iw:.0}px", n.name);
            }
        }

        let svg = render();
        for line in svg.lines() {
            if let Some(rest) = line.split_once("<text x=\"") {
                let x: f64 = rest.1.split('"').next().unwrap().parse().unwrap();
                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 adv = if mono { MONO_ADV } else { PROP_ADV };
                let bw = body.chars().count() as f64 * size * adv;
                let right = if anchored_end {
                    x
                } else if anchored_mid {
                    x + bw / 2.0
                } else {
                    x + bw
                };
                assert!(
                    right <= W + 2.0,
                    "text runs to {right:.0}px past the {W}px canvas: {body:?}"
                );
                let left = if anchored_end {
                    x - bw
                } else if anchored_mid {
                    x - bw / 2.0
                } else {
                    x
                };
                assert!(left >= -2.0, "text starts at {left:.0}px: {body:?}");
            }
        }
    }
}