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("&")
|| tail.starts_with("<")
|| tail.starts_with(">")
|| tail.starts_with(""")
|| 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:?}");
}
}
}
}