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('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
/// 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 ``, 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("&")
|| tail.starts_with("<")
|| tail.starts_with(">")
|| tail.starts_with(""")
|| 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
);
}
}
}