use super::escape;
use crate::event::State;
use crate::model::{Aggregates, Node, Tree};
use serde_json::json;
use std::collections::{BTreeSet, HashMap};
const MAP_JS: &str = include_str!("map.js");
const ROW: u32 = 32;
struct Lane {
step: u32,
origin: u32,
control: u32,
when: &'static str,
}
const WIDE: Lane = Lane {
step: 18,
origin: 34,
control: 11,
when: "wide",
};
const NARROW: Lane = Lane {
step: 8,
origin: 24,
control: 9,
when: "narrow",
};
impl Lane {
fn x(&self, depth: usize) -> u32 {
self.origin + self.step * depth as u32
}
}
const NAMED_MIN: usize = 5;
const NAMED_MAX: usize = 8;
const HUB: usize = 10;
#[derive(Default)]
struct Fold {
shut: BTreeSet<u64>,
depth: Option<usize>,
}
impl Fold {
fn parse(query: &str, tree: &Tree) -> Fold {
let mut shut = BTreeSet::new();
let mut depth = None;
for field in query.split('&') {
match field.split_once('=') {
Some(("fold", value)) => {
for alias in value.split(',') {
if let Some(n) = tree.resolve(alias) {
shut.insert(n.num);
}
}
}
Some(("depth", value)) => depth = value.parse().ok().filter(|&d| d > 0),
_ => {}
}
}
Fold { shut, depth }
}
fn hides(&self, n: &Node) -> bool {
self.shut.contains(&n.num)
}
fn cuts(&self, depth: usize) -> bool {
self.depth.is_some_and(|d| depth + 1 >= d)
}
fn url(tree: &Tree, shut: &BTreeSet<u64>, depth: Option<usize>, standing: &str) -> String {
let mut parts = Vec::new();
if let Some(d) = depth {
parts.push(format!("depth={d}"));
}
let mut aliases: Vec<String> = shut
.iter()
.filter_map(|&num| tree.node_by_num(num).map(|x| x.alias()))
.collect();
aliases.sort();
if !aliases.is_empty() {
parts.push(format!("fold={}", aliases.join(",")));
}
let query = match parts.is_empty() {
true => "?".to_string(),
false => format!("?{}", parts.join("&")),
};
match standing.is_empty() {
true => query,
false => format!("{query}#{standing}"),
}
}
fn toggled(&self, tree: &Tree, n: &Node) -> String {
let mut shut = self.shut.clone();
if !shut.remove(&n.num) {
shut.insert(n.num);
}
Fold::url(tree, &shut, self.depth, &n.alias())
}
fn at_depth(&self, tree: &Tree, depth: Option<usize>) -> String {
Fold::url(tree, &self.shut, depth, "")
}
}
struct Stop<'t> {
node: &'t Node,
depth: usize,
parent: Option<usize>,
children: Vec<usize>,
fan: usize,
row: Option<usize>,
last_row: Option<usize>,
drawn_below: usize,
hidden: usize,
line: usize,
}
struct Map<'t> {
stops: Vec<Stop<'t>>,
lines: Lines,
index: HashMap<u64, usize>,
depth: usize,
lanes: usize,
}
#[allow(clippy::too_many_arguments)]
fn walk<'t>(
tree: &'t Tree,
ag: &Aggregates,
fold: &Fold,
n: &'t Node,
depth: usize,
parent: Option<usize>,
drawn: bool,
stops: &mut Vec<Stop<'t>>,
next_row: &mut usize,
) -> usize {
let me = stops.len();
let row = drawn.then(|| {
let r = *next_row;
*next_row += 1;
r
});
stops.push(Stop {
node: n,
depth,
parent,
children: Vec::new(),
fan: tree.children(n.num).len(),
row,
last_row: row,
drawn_below: 0,
hidden: 0,
line: 0,
});
let shows = drawn && !fold.hides(n) && !fold.cuts(depth);
let mut children = Vec::new();
let mut drawn_below = 0;
let mut last_row = row;
for c in tree.children(n.num) {
let at = walk(
tree,
ag,
fold,
c,
depth + 1,
Some(me),
shows,
stops,
next_row,
);
drawn_below += stops[at].drawn_below + usize::from(stops[at].row.is_some());
if stops[at].last_row.is_some() {
last_row = stops[at].last_row;
}
children.push(at);
}
stops[me].children = children;
stops[me].last_row = last_row;
stops[me].drawn_below = drawn_below;
stops[me].hidden = ag.counts(n.num).total.saturating_sub(drawn_below);
me
}
struct Lines {
of: HashMap<u64, usize>,
chains: Vec<Vec<u64>>,
named: Vec<usize>,
names: HashMap<usize, String>,
}
fn thread(tree: &Tree, ag: &Aggregates) -> Lines {
let mut of: HashMap<u64, usize> = HashMap::new();
let mut chains: Vec<Vec<u64>> = Vec::new();
let mut pending: Vec<u64> = tree.roots().iter().rev().map(|r| r.num).collect();
while let Some(start) = pending.pop() {
let id = chains.len();
chains.push(Vec::new());
let mut at = start;
loop {
of.insert(at, id);
chains[id].push(at);
let children = tree.children(at);
let Some(first) = children.first() else {
break;
};
let mut next = first.num;
let mut best = ag.counts(first.num).total;
for c in children.iter().skip(1) {
let weight = ag.counts(c.num).total;
if weight > best {
next = c.num;
best = weight;
}
}
for c in children.iter().rev() {
if c.num != next {
pending.push(c.num);
}
}
at = next;
}
}
let mut rank: Vec<usize> = (0..chains.len()).collect();
rank.sort_by_key(|&i| (std::cmp::Reverse(chains[i].len()), chains[i][0]));
let named: Vec<usize> = rank
.into_iter()
.filter(|&i| chains[i].len() >= NAMED_MIN)
.take(NAMED_MAX)
.collect();
let names: HashMap<usize, String> = named
.iter()
.filter_map(|&i| tree.node_by_num(chains[i][0]).map(|n| (i, n.alias())))
.collect();
Lines {
of,
chains,
named,
names,
}
}
impl<'t> Map<'t> {
fn of(tree: &'t Tree, ag: &Aggregates, fold: &Fold) -> Map<'t> {
let lines = thread(tree, ag);
let mut stops = Vec::new();
let mut next_row = 0;
for root in tree.roots() {
walk(
tree,
ag,
fold,
root,
0,
None,
true,
&mut stops,
&mut next_row,
);
}
for s in stops.iter_mut() {
s.line = *lines.of.get(&s.node.num).unwrap_or(&usize::MAX);
}
let index = stops
.iter()
.enumerate()
.map(|(i, s)| (s.node.num, i))
.collect();
let depth = stops
.iter()
.filter(|s| s.row.is_some())
.map(|s| s.depth)
.max()
.unwrap_or(0);
Map {
stops,
lines,
index,
depth,
lanes: levels(tree).saturating_sub(1),
}
}
fn colour(&self, line: usize) -> String {
match self.lines.named.iter().position(|&l| l == line) {
Some(i) => format!("l{i}"),
None => "lx".to_string(),
}
}
fn line_name(&self, line: usize) -> String {
self.lines.names.get(&line).cloned().unwrap_or_default()
}
fn width(&self, lane: &Lane) -> u32 {
lane.x(self.lanes) + 14
}
fn height(&self) -> u32 {
self.shown() as u32 * ROW
}
fn shown(&self) -> usize {
self.drawn().count()
}
fn drawn(&self) -> impl Iterator<Item = (usize, &Stop<'t>)> {
self.stops
.iter()
.enumerate()
.filter(|(_, s)| s.row.is_some())
}
}
fn radius(children: usize) -> f32 {
match children {
0 => 2.4,
1..=4 => 3.6,
5..=9 => 5.0,
10..=29 => 6.5,
_ => 9.0,
}
}
fn y(i: usize) -> u32 {
i as u32 * ROW + ROW / 2
}
fn gutter(tree: &Tree, map: &Map, lane: &Lane, fold: &Fold) -> String {
let mut out = format!(
"<svg class=\"rails {when}\" width=\"{w}\" height=\"{h}\" \
data-step=\"{step}\" data-origin=\"{origin}\" data-row=\"{ROW}\">\n\
<g aria-hidden=\"true\">\n",
when = lane.when,
w = map.width(lane),
h = map.height(),
step = lane.step,
origin = lane.origin,
);
for (_, s) in map.drawn() {
let (x, mid) = (lane.x(s.depth), y(s.row.unwrap_or(0)));
let c = map.colour(s.line);
if s.drawn_below > 0 {
out.push_str(&format!(
"<path class=\"rail {c}\" d=\"M{x} {mid} V{end}\"/>\n",
end = y(s.last_row.unwrap_or(0)),
));
}
if let Some(p) = s.parent.filter(|&p| map.stops[p].row.is_some()) {
let px = lane.x(map.stops[p].depth);
out.push_str(&format!(
"<path class=\"elbow {c}\" d=\"M{px} {top} V{turn} Q{px} {mid} {corner} {mid} H{x}\"/>\n",
top = mid - ROW / 2,
turn = mid - 7,
corner = px + 7,
));
}
}
for &line in &map.lines.named {
let end = *map.lines.chains[line]
.last()
.expect("a line in `named` has at least NAMED_MIN stations");
let Some(last) = map
.index
.get(&end)
.map(|&at| &map.stops[at])
.filter(|s| s.row.is_some())
else {
continue;
};
let (x, mid) = (lane.x(last.depth), y(last.row.unwrap_or(0)));
out.push_str(&format!(
"<path class=\"terminus {c}\" d=\"M{left} {below} H{right}\"/>\n",
c = map.colour(line),
left = x - 5,
right = x + 5,
below = mid + 9,
));
}
for (i, s) in map.drawn() {
let (x, mid) = (lane.x(s.depth), y(s.row.unwrap_or(0)));
let r = radius(s.fan);
out.push_str(&format!(
"<circle class=\"station {c}{state}\" data-stop=\"{i}\" \
cx=\"{x}\" cy=\"{mid}\" r=\"{r}\"/>\n",
c = map.colour(s.line),
state = if s.node.state.is_open() {
" open"
} else {
" shut"
},
));
if s.node.blocks {
out.push_str(&format!(
"<circle class=\"waits\" cx=\"{x}\" cy=\"{mid}\" r=\"{ring}\"/>\n",
ring = r + 3.6,
));
}
}
out.push_str("<g class=\"route\"></g></g>\n");
out.push_str("<g class=\"folds\">\n");
for (_, s) in map.drawn() {
if s.fan == 0 {
continue;
}
let n = s.node;
let shut = fold.hides(n);
let mid = y(s.row.unwrap_or(0));
let (left, top, text) = (lane.control.saturating_sub(9), mid - 9, mid + 5);
if fold.cuts(s.depth) {
out.push_str(&format!(
"<g class=\"fold off\"><title>{id} is at the last level the \
depth is set to</title>\
<text x=\"{x}\" y=\"{text}\">▸</text></g>\n",
id = escape(&n.alias()),
x = lane.control,
));
continue;
}
out.push_str(&format!(
"<a class=\"fold{on}\" href=\"{href}\" aria-label=\"{label}\">\
<rect x=\"{left}\" y=\"{top}\" width=\"18\" height=\"18\"/>\
<text x=\"{x}\" y=\"{text}\">{glyph}</text></a>\n",
on = if shut { " on" } else { "" },
href = escape(&fold.toggled(tree, n)),
label = escape(&if shut {
format!("unfold {} nodes under {}", s.hidden, n.alias())
} else {
format!("fold everything under {}", n.alias())
}),
x = lane.control,
glyph = if shut { "▸" } else { "▾" },
));
if shut {
let x = lane.x(s.depth);
out.push_str(&format!(
"<path class=\"cut {c}\" d=\"M{a} {up} L{b} {down} M{a} {below} L{b} {rest}\"/>\n",
c = map.colour(s.line),
a = x - 5,
b = x + 5,
up = mid + 11,
down = mid + 5,
below = mid + 15,
rest = mid + 9,
));
}
}
out.push_str("</g></svg>\n");
out
}
fn rows(project: &str, tree: &Tree, map: &Map, ag: &Aggregates, fold: &Fold) -> String {
let mut out = String::from("<ol class=\"stops\">\n");
for (i, s) in map.drawn() {
let n = s.node;
let alias = n.alias();
let mut class = String::from("stop");
if !n.state.is_open() {
class.push_str(" shut");
}
if n.state == State::Suspended {
class.push_str(" parked");
}
if s.fan >= HUB {
class.push_str(" hub");
}
if n.state == State::Done && ag.blockers(n.num) > 0 {
class.push_str(" false-close");
}
if fold.hides(n) || (s.fan > 0 && fold.cuts(s.depth)) {
class.push_str(" folded");
}
let notes = match n.notes.len() {
0 | 1 => String::new(),
many => format!("<span class=\"notes\">{many} notes</span>"),
};
let fan = match s.fan {
0 => String::new(),
c => format!("<span class=\"fan\">{c}</span>"),
};
let control = match s.fan > 0 && (fold.hides(n) || fold.cuts(s.depth)) {
true => format!("<span class=\"held\">+{}</span>", s.hidden),
false => String::new(),
};
out.push_str(&format!(
"<li class=\"{class}\" id=\"{id}\" data-stop=\"{i}\">\
<a class=\"alias {c}\" href=\"/p/{p}/why/{id}\" \
title=\"{id} · {w}{b} · {t}\">{id}{mark}</a>\
<span class=\"title\">{t}</span>{notes}{fan}{control}</li>\n",
c = map.colour(s.line),
id = escape(&alias),
p = escape(project),
w = escape(n.state.word(n.kind)),
b = if n.blocks { " · blocking" } else { "" },
t = escape(n.title(tree)),
mark = if n.blocks {
"<span class=\"mark\" aria-hidden=\"true\">*</span>"
} else {
""
},
));
}
out.push_str("</ol>\n");
out
}
fn legend(map: &Map) -> String {
let mut out = String::from("<div class=\"legend\">\n");
for (i, &line) in map.lines.named.iter().enumerate() {
out.push_str(&format!(
"<button class=\"line l{i}\" data-line=\"{name}\" type=\"button\">\
<b></b>{name} · {n}</button>\n",
name = escape(&map.line_name(line)),
n = map.lines.chains[line].len(),
));
}
out.push_str("</div>\n");
out
}
fn payload(project: &str, tree: &Tree, map: &Map, ag: &Aggregates, fold: &Fold) -> String {
let stops: Vec<serde_json::Value> = map
.stops
.iter()
.map(|s| {
let hides_here = s.fan > 0 && (fold.hides(s.node) || fold.cuts(s.depth));
let n = s.node;
let below = ag.counts(n.num);
let blocking = crate::render::blocking_of(tree, n);
json!({
"a": n.alias(),
"t": n.title(tree),
"k": n.kind.word(),
"s": n.state.word(n.kind),
"b": n.blocks,
"w": n.why(tree),
"o": n.outcome(tree),
"nt": n.notes(tree)
.iter()
.map(|(at, text)| json!({"at": at, "n": text}))
.collect::<Vec<_>>(),
"rf": n.refs(tree),
"gv": n.governs(tree),
"op": n.opened(tree),
"cl": n.closed(tree),
"fc": n.state == State::Done && ag.blockers(n.num) > 0,
"d": s.depth,
"p": s.parent,
"c": s.fan,
"ob": below.open_count,
"tb": below.total,
"ln": map.line_name(s.line),
"r": s.row,
"hd": if hides_here { s.hidden } else { 0 },
"bn": blocking.len(),
"bl": blocking
.iter()
.filter_map(|b| map.index.get(&b.num))
.collect::<Vec<_>>(),
})
})
.collect();
let folded: Vec<String> = fold
.shut
.iter()
.filter_map(|&num| tree.node_by_num(num).map(|n| n.alias()))
.collect();
let raw = json!({
"project": project,
"fold": folded,
"depth": fold.depth,
"stops": stops,
})
.to_string();
raw.replace('<', "\\u003c")
.replace('>', "\\u003e")
.replace('&', "\\u0026")
}
fn key(tree: &Tree) -> String {
format!(
"<div class=\"resting\">\n<p class=\"code\">nothing selected</p>\n\
<h2>The whole tree, {n} nodes</h2>\n\
<p class=\"prose\">Every station is a node and every rail a line of \
provenance. Click a row or a station and the route from the root is \
drawn across the map and lit along the titles.</p>\n\
<h3>How to read it</h3>\n<dl>\n\
<dt>size</dt><dd>direct children: the biggest circles are the places \
nearly all the work hangs from</dd>\n\
<dt>fill</dt><dd>solid is open, hollow is closed</dd>\n\
<dt>ring</dt><dd>blocks its parent, which cannot close until this \
one does</dd>\n\
<dt>cap</dt><dd>the end of a line. Provenance is a tree, so no line \
ever rejoins another</dd>\n\
<dt>struck out</dt><dd>closed</dd>\n\
<dt>triangle</dt><dd>folds everything under that node, so its \
neighbours come within reach</dd>\n\
<dt>numbers above</dt><dd>fold a whole depth at once, one per lane</dd>\n\
<dt>keys</dt><dd>up and down walk the list, / finds, Esc closes</dd>\n\
</dl>\n</div>\n",
n = tree.total(),
)
}
fn stats(map: &Map, tree: &Tree) -> String {
let open = map.drawn().filter(|(_, s)| s.node.state.is_open()).count();
let blocking = map.drawn().filter(|(_, s)| s.node.blocks).count();
let folded = tree.total().saturating_sub(map.shown());
let away = match folded {
0 => String::new(),
n => format!(" · {n} folded away"),
};
format!(
"{drawn} of {n} nodes{away} · {open} open · {blocking} blocking · \
depth {d} · {lines} lines, {named} of them named",
drawn = map.shown(),
n = tree.total(),
d = map.depth,
lines = map.lines.chains.len(),
named = map.lines.named.len(),
)
}
fn levels(tree: &Tree) -> usize {
fn deepest(tree: &Tree, n: &Node, depth: usize) -> usize {
tree.children(n.num)
.iter()
.map(|c| deepest(tree, c, depth + 1))
.max()
.unwrap_or(depth)
}
tree.roots()
.iter()
.map(|r| deepest(tree, r, 0))
.max()
.map(|d| d + 1)
.unwrap_or(0)
}
fn heads(tree: &Tree, fold: &Fold, lane: &Lane) -> String {
let deep = levels(tree);
if deep == 0 {
return String::new();
}
let mut out = format!("<div class=\"heads {when}\">", when = lane.when);
for level in 1..=deep {
let here = fold.depth == Some(level);
out.push_str(&format!(
"<a class=\"head{on}\" style=\"left:{left}px\" href=\"{href}\" \
title=\"{what}\">{level}</a>",
on = if here { " on" } else { "" },
left = lane.x(level - 1).saturating_sub(9),
href = escape(&fold.at_depth(tree, if here { None } else { Some(level) })),
what = if here {
"showing this many levels -- click for the whole tree".to_string()
} else {
format!(
"show {level} level{s}",
s = if level == 1 { "" } else { "s" }
)
},
));
}
out.push_str("</div>\n");
out
}
fn depths(tree: &Tree, fold: &Fold) -> String {
let deep = levels(tree);
if deep == 0 {
return String::new();
}
let mut out = String::from("<span class=\"depths\">levels ");
for level in 1..=deep {
let here = fold.depth == Some(level);
out.push_str(&format!(
"<a class=\"depth{on}\" href=\"{href}\">{level}</a>",
on = if here { " on" } else { "" },
href = escape(&fold.at_depth(tree, if here { None } else { Some(level) })),
));
}
out.push_str(&format!(
"<a class=\"depth{on}\" href=\"{href}\">all</a></span>",
on = if fold.depth.is_none() { " on" } else { "" },
href = escape(&fold.at_depth(tree, None)),
));
out
}
fn unfold_all(map: &Map, tree: &Tree) -> String {
let folded = tree.total().saturating_sub(map.shown());
match folded {
0 => String::new(),
n => format!("<a class=\"tool\" href=\"?\">Unfold everything · {n}</a>\n"),
}
}
fn fold_all(map: &Map, tree: &Tree) -> String {
if map.shown() <= tree.roots().len() {
return String::new();
}
"<a class=\"tool\" href=\"?depth=1\">Fold everything</a>\n".to_string()
}
pub(super) fn map_page(project: &str, name: &str, tree: &Tree, query: &str) -> String {
let ag = tree.aggregates();
let fold = Fold::parse(query, tree);
let map = Map::of(tree, &ag, &fold);
if tree.is_empty_tree() {
return shell(
project,
name,
"<p class=\"empty\">Empty tree.</p>\n".to_string(),
String::new(),
String::new(),
String::new(),
);
}
let focus = tree
.focus()
.and_then(|f| map.stops.iter().position(|s| s.node.num == f.num));
let here = match focus {
Some(i) => format!(
"<button class=\"tool\" id=\"here\" type=\"button\" data-stop=\"{i}\">\
Where am I?</button>\n"
),
None => String::new(),
};
let body = format!(
"<div class=\"map\">\n{heads}<div class=\"gutter\">{wide}{narrow}</div>\n\
{rows}<aside id=\"detail\" class=\"detail\">{key}</aside>\n</div>\n",
heads = heads(tree, &fold, &WIDE),
wide = gutter(tree, &map, &WIDE, &fold),
narrow = gutter(tree, &map, &NARROW, &fold),
rows = rows(project, tree, &map, &ag, &fold),
key = key(tree),
);
shell(
project,
name,
body,
format!(
"<p class=\"stats\">{stats}</p>\n{legend}<div class=\"tools\">{here}{shut}{back}\
<input id=\"find\" type=\"search\" \
placeholder=\"Find in alias, title and why…\" \
aria-label=\"Find a station\">\
<span class=\"hits\" id=\"hits\" role=\"status\"></span>{depths}</div>\n",
stats = escape(&stats(&map, tree)),
shut = fold_all(&map, tree),
back = unfold_all(&map, tree),
legend = legend(&map),
depths = depths(tree, &fold),
),
payload(project, tree, &map, &ag, &fold),
MAP_JS.to_string(),
)
}
fn shell(
project: &str,
name: &str,
body: String,
head: String,
data: String,
script: String,
) -> String {
let carried = if data.is_empty() {
String::new()
} else {
format!(
"<script type=\"application/json\" id=\"map-data\">{data}</script>\n\
<script>{script}</script>\n"
)
};
format!(
"<!doctype html>\n\
<html lang=\"en\"><head><meta charset=\"utf-8\">\n\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\
<title>Map - {name_t}</title>\n\
<style>\n{css}</style></head>\n\
<body class=\"wide-page\"><div class=\"page\">\n\
<header><p class=\"crumb\"><a href=\"/p/{p}/\">{name_t}</a></p>\n\
<h1>The map</h1>\n\
<p class=\"promise\">Why a node exists, read without letting go of the \
tree you found it in -- and how much of what surrounds it is already \
closed, at a glance.</p>\n{head}</header>\n\
<main>\n{body}</main>\n\
<footer>The same readings in a terminal: <code>vivac tree --all</code> \
and <code>vivac why <id></code></footer>\n\
</div>\n{carried}</body></html>\n",
name_t = escape(name),
p = escape(project),
css = super::WEB_CSS,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Body, Kind};
const REAL_DEGREES: &[usize] = &[
56, 30, 12, 11, 8, 7, 6, 4, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ];
fn fixture_node(tree: &mut Tree, seq: &mut u64, num: &mut u64, parent: Option<&str>) -> String {
*seq += 1;
*num += 1;
let id = format!("n{num}");
tree.apply(
*seq,
"2026-09-04T10:00:00Z",
&Body::NodeCreated {
node: id.clone(),
num: *num,
kind: Kind::Task,
title: format!("node {num}"),
why: "fixture".to_string(),
parent: parent.map(str::to_string),
blocks: false,
refs: vec![],
governs: vec![],
},
);
id
}
fn fixture_children(
tree: &mut Tree,
seq: &mut u64,
num: &mut u64,
parent: &str,
count: usize,
) -> Vec<String> {
(0..count)
.map(|_| fixture_node(tree, seq, num, Some(parent)))
.collect()
}
fn real_shape() -> Tree {
let mut tree = Tree::default();
let mut seq = 0u64;
let mut num = 0u64;
let root_a = fixture_node(&mut tree, &mut seq, &mut num, None);
let root_b = fixture_node(&mut tree, &mut seq, &mut num, None);
fixture_node(&mut tree, &mut seq, &mut num, None);
let root_a_children = fixture_children(&mut tree, &mut seq, &mut num, &root_a, 56);
let p11_children = fixture_children(&mut tree, &mut seq, &mut num, &root_a_children[0], 11);
let p12_children = fixture_children(&mut tree, &mut seq, &mut num, &p11_children[0], 12);
let p6_children = fixture_children(&mut tree, &mut seq, &mut num, &p12_children[0], 6);
let p30_children = fixture_children(&mut tree, &mut seq, &mut num, &p6_children[0], 30);
let root_b_children = fixture_children(&mut tree, &mut seq, &mut num, &root_b, 4);
let p7_children = fixture_children(&mut tree, &mut seq, &mut num, &root_b_children[0], 7);
let p8_children = fixture_children(&mut tree, &mut seq, &mut num, &p7_children[0], 8);
let mut slots: Vec<String> = Vec::new();
slots.extend(root_a_children[1..].iter().cloned());
slots.extend(root_b_children[1..].iter().cloned());
slots.extend(p11_children[1..].iter().cloned());
slots.extend(p7_children[1..].iter().cloned());
slots.extend(p12_children[1..].iter().cloned());
slots.extend(p8_children.iter().cloned());
slots.extend(p6_children[1..].iter().cloned());
let mut remaining: Vec<usize> = REAL_DEGREES.to_vec();
for placed in [56usize, 11, 12, 6, 30, 4, 7, 8] {
let at = remaining
.iter()
.position(|&d| d == placed)
.expect("every placed degree is in REAL_DEGREES");
remaining.remove(at);
}
let one_at = remaining
.iter()
.position(|&d| d == 1)
.expect("REAL_DEGREES carries a 1 to spend at depth 6");
remaining.remove(one_at);
fixture_node(&mut tree, &mut seq, &mut num, Some(&p30_children[0]));
for (slot, degree) in slots.iter().zip(remaining.iter()) {
for _ in 0..*degree {
fixture_node(&mut tree, &mut seq, &mut num, Some(slot));
}
}
tree.sort_nodes();
tree
}
fn even_shape() -> Tree {
let mut tree = Tree::default();
let mut seq = 0u64;
let mut num = 0u64;
let root = fixture_node(&mut tree, &mut seq, &mut num, None);
let mut levels: Vec<Vec<String>> = vec![vec![root]];
let branching = [1usize, 3, 9, 15, 27, 9];
for parents_with_children in branching {
let mut next = Vec::new();
for parent in levels.last().unwrap().iter().take(parents_with_children) {
next.extend(fixture_children(&mut tree, &mut seq, &mut num, parent, 3));
}
levels.push(next);
}
for parent in levels[5].iter().skip(9).take(2) {
fixture_node(&mut tree, &mut seq, &mut num, Some(parent));
}
tree.sort_nodes();
tree
}
fn blocked_shape() -> Tree {
let mut tree = Tree::default();
let (mut seq, mut num) = (0u64, 0u64);
let root = fixture_node(&mut tree, &mut seq, &mut num, None);
let branch = fixture_node(&mut tree, &mut seq, &mut num, Some(&root));
seq += 1;
num += 1;
tree.apply(
seq,
"2026-09-09T10:00:00Z",
&Body::NodeCreated {
node: "n3".to_string(),
num,
kind: Kind::Question,
title: "the blocker".to_string(),
why: "fixture".to_string(),
parent: Some(branch.clone()),
blocks: true,
refs: vec![],
governs: vec![],
},
);
seq += 1;
tree.apply(
seq,
"2026-09-09T11:00:00Z",
&Body::StateChanged {
node: branch,
state: State::Done,
outcome: "closed with a condition still open".to_string(),
forced: true,
},
);
tree.sort_nodes();
tree
}
fn max_depth(tree: &Tree) -> usize {
fn under(tree: &Tree, n: &Node, depth: usize) -> usize {
tree.children(n.num)
.iter()
.map(|c| under(tree, c, depth + 1))
.max()
.unwrap_or(depth)
}
tree.roots()
.iter()
.map(|r| under(tree, r, 0))
.max()
.unwrap_or(0)
}
#[test]
fn real_shape_matches_the_measured_degree_sequence() {
let tree = real_shape();
assert_eq!(tree.total(), 195, "node count");
assert_eq!(tree.roots().len(), 3, "root count");
let mut degrees: Vec<usize> = tree
.nodes_iter()
.map(|n| tree.children(n.num).len())
.filter(|&d| d > 0)
.collect();
degrees.sort_unstable_by(|a, b| b.cmp(a));
assert_eq!(degrees, REAL_DEGREES, "the degree sequence must match");
assert_eq!(tree.total() - degrees.len(), 147, "leaf count");
assert_eq!(max_depth(&tree), 6, "max depth");
}
#[test]
fn every_node_is_one_row_and_one_station_in_each_gutter() {
let tree = real_shape();
let page = map_page("vivac", "vivac", &tree, "");
assert_eq!(page.matches("<li class=\"stop").count(), tree.total());
assert_eq!(
page.matches("class=\"station ").count(),
tree.total() * 2,
"one station per node in each of the two gutters"
);
}
#[test]
fn the_page_carries_one_gutter_per_lane_width() {
let page = map_page("vivac", "vivac", &real_shape(), "");
assert_eq!(page.matches("<svg class=\"rails").count(), 2);
assert_eq!(page.matches("class=\"rails wide\"").count(), 1);
assert_eq!(page.matches("class=\"rails narrow\"").count(), 1);
assert!(
!super::super::WEB_CSS.contains("svg.rails"),
"selecting the gutter by element is what broke it the first time"
);
}
#[test]
fn every_stop_is_on_exactly_one_line_and_a_line_walks_downwards() {
let tree = real_shape();
let ag = tree.aggregates();
let map = Map::of(&tree, &ag, &Fold::default());
let mut seen = vec![0usize; map.stops.len()];
for line in map.lines.chains.iter().map(|c| {
c.iter()
.filter_map(|num| map.index.get(num).copied())
.collect::<Vec<usize>>()
}) {
let line = &line;
for &s in line {
seen[s] += 1;
}
for pair in line.windows(2) {
assert_eq!(
map.stops[pair[1]].parent,
Some(pair[0]),
"a line only ever continues into a child"
);
}
}
assert!(
seen.iter().all(|&c| c == 1),
"every stop on exactly one line"
);
}
#[test]
fn only_the_long_lines_earn_a_colour() {
let tree = real_shape();
let ag = tree.aggregates();
let map = Map::of(&tree, &ag, &Fold::default());
assert!(map.lines.named.len() <= NAMED_MAX);
for &line in &map.lines.named {
assert!(map.lines.chains[line].len() >= NAMED_MIN);
}
assert!(
map.lines.chains.len() > map.lines.named.len(),
"the real shape has more lines than colours, which is the point"
);
}
#[test]
fn the_line_continues_into_the_heaviest_child() {
let mut tree = Tree::default();
let (mut seq, mut num) = (0u64, 0u64);
let root = fixture_node(&mut tree, &mut seq, &mut num, None);
let light = fixture_node(&mut tree, &mut seq, &mut num, Some(&root));
let heavy = fixture_node(&mut tree, &mut seq, &mut num, Some(&root));
fixture_children(&mut tree, &mut seq, &mut num, &heavy, 3);
tree.sort_nodes();
let ag = tree.aggregates();
let map = Map::of(&tree, &ag, &Fold::default());
let at = |id: &str| {
map.stops
.iter()
.position(|s| s.node.id == id)
.expect("the fixture node is a stop")
};
assert_eq!(
map.stops[at(&root)].line,
map.stops[at(&heavy)].line,
"the line goes where the work is"
);
assert_ne!(
map.stops[at(&root)].line,
map.stops[at(&light)].line,
"the sibling that turns off starts its own line"
);
}
#[test]
fn the_detail_of_every_node_travels_with_the_page() {
let tree = real_shape();
let ag = tree.aggregates();
let map = Map::of(&tree, &ag, &Fold::default());
let data = payload("vivac", &tree, &map, &ag, &Fold::default());
let parsed: serde_json::Value =
serde_json::from_str(&data.replace("\\u003c", "<").replace("\\u003e", ">"))
.expect("the payload is JSON");
let stops = parsed["stops"].as_array().expect("an array of stops");
assert_eq!(stops.len(), tree.total());
assert!(
stops.iter().all(|s| s["w"] == "fixture"),
"every why is here"
);
}
#[test]
fn every_note_travels_with_the_page() {
let mut tree = Tree::default();
let (mut seq, mut num) = (0u64, 0u64);
let id = fixture_node(&mut tree, &mut seq, &mut num, None);
for (at, text) in [
("2026-09-08T10:00:00Z", "the first note"),
("2026-09-09T10:00:00Z", "the correction"),
] {
seq += 1;
tree.apply(
seq,
at,
&Body::NodeNoted {
node: id.clone(),
note: text.to_string(),
},
);
}
tree.sort_nodes();
let page = map_page("vivac", "vivac", &tree, "");
assert!(page.contains("the first note"), "the covered note is here");
assert!(page.contains("the correction"));
assert!(page.contains("2 notes"), "and the row says there are two");
}
#[test]
fn a_node_that_blocks_its_parent_is_marked_in_glyph_and_in_word() {
let page = map_page("vivac", "vivac", &blocked_shape(), "");
assert_eq!(page.matches("class=\"mark\"").count(), 1);
assert!(page.contains("· blocking ·"), "the word, not only the mark");
assert!(
page.contains("*</span></a>"),
"the mark has to close before the alias does:\n{page}"
);
assert_eq!(
page.matches("class=\"waits\"").count(),
2,
"the ring, once in each gutter"
);
}
#[test]
fn a_node_closed_over_an_open_condition_is_marked_on_its_row() {
let page = map_page("vivac", "vivac", &blocked_shape(), "");
assert!(page.contains("false-close"), "{page}");
assert!(page.contains("\"fc\":true"), "and it travels in the detail");
}
#[test]
fn nothing_is_selected_when_the_page_lands() {
let page = map_page("vivac", "vivac", &real_shape(), "");
assert!(!page.contains("class=\"stop on"), "no row arrives selected");
assert!(
page.contains("<g class=\"route\"></g>"),
"and no route arrives drawn"
);
}
#[test]
fn a_node_title_cannot_close_the_script_it_travels_in() {
let mut tree = Tree::default();
let (mut seq, mut num) = (0u64, 0u64);
let id = format!("n{}", num + 1);
seq += 1;
num += 1;
tree.apply(
seq,
"2026-09-09T10:00:00Z",
&Body::NodeCreated {
node: id,
num,
kind: Kind::Task,
title: "</script><script>alert(1)</script>".to_string(),
why: "</script>".to_string(),
parent: None,
blocks: false,
refs: vec![],
governs: vec![],
},
);
tree.sort_nodes();
let page = map_page("vivac", "vivac", &tree, "");
assert!(!page.contains("<script>alert(1)"), "{page}");
assert_eq!(
page.matches("</script>").count(),
2,
"the two this page opens, and no third one a title smuggled in"
);
}
fn biggest_hub(tree: &Tree, ag: &Aggregates) -> String {
let map = Map::of(tree, ag, &Fold::default());
map.stops
.iter()
.max_by_key(|s| s.fan)
.expect("the fixture has a node with children")
.node
.alias()
}
#[test]
fn folding_a_node_takes_its_subtree_off_the_page_and_counts_it() {
let tree = real_shape();
let ag = tree.aggregates();
let alias = biggest_hub(&tree, &ag);
let under = ag
.counts(tree.resolve(&alias).expect("the hub resolves").num)
.total;
let page = map_page("vivac", "vivac", &tree, &format!("fold={alias}"));
assert_eq!(
page.matches("<li class=\"stop").count(),
tree.total() - under,
"the subtree should be off the page"
);
assert_eq!(
page.matches("class=\"station ").count(),
(tree.total() - under) * 2,
"and off both gutters, or a rail points at the wrong row"
);
assert!(
page.contains(&format!("<span class=\"held\">+{under}</span>")),
"{page}"
);
}
#[test]
fn only_the_folded_node_offers_to_unfold() {
let tree = real_shape();
let ag = tree.aggregates();
let alias = biggest_hub(&tree, &ag);
let page = map_page("vivac", "vivac", &tree, &format!("fold={alias}"));
assert_eq!(
page.matches("class=\"fold on\"").count(),
2,
"one node is folded, so one control per gutter says so:\n{page}"
);
assert_eq!(
page.matches("class=\"stop hub folded\"").count()
+ page.matches("class=\"stop folded\"").count(),
1,
"and one row is marked folded:\n{page}"
);
}
#[test]
fn the_depth_ruler_is_one_number_and_not_a_pile() {
let tree = real_shape();
let deep = levels(&tree);
assert!(deep > 3, "the fixture is deep enough to test this");
let page = map_page("vivac", "vivac", &tree, "");
assert_eq!(page.matches("class=\"head\"").count(), deep);
assert_eq!(page.matches("class=\"head on\"").count(), 0);
let page = map_page("vivac", "vivac", &tree, "depth=3");
assert_eq!(page.matches("class=\"head on\"").count(), 1, "{page}");
let three = page.matches("<li class=\"stop").count();
let page = map_page("vivac", "vivac", &tree, "depth=3&depth=2");
assert_eq!(
page.matches("<li class=\"stop").count(),
map_page("vivac", "vivac", &tree, "depth=2")
.matches("<li class=\"stop")
.count(),
"the last number wins, and nothing accumulates"
);
assert!(
page.matches("<li class=\"stop").count() < three,
"two levels draw fewer rows than three"
);
}
#[test]
fn a_depth_of_one_draws_the_roots_and_nothing_else() {
let tree = real_shape();
let page = map_page("vivac", "vivac", &tree, "depth=1");
assert_eq!(
page.matches("<li class=\"stop").count(),
tree.roots().len(),
"{page}"
);
assert!(page.contains("class=\"tool\" href=\"?\""), "{page}");
}
#[test]
fn a_row_the_depth_cuts_says_so_instead_of_offering_to_fold() {
let tree = real_shape();
let page = map_page("vivac", "vivac", &tree, "depth=2");
assert!(page.contains("class=\"fold off\""), "{page}");
assert!(
page.contains("is at the last level the depth is set to"),
"{page}"
);
}
#[test]
fn a_folded_page_always_shows_the_way_back() {
let tree = real_shape();
let ag = tree.aggregates();
let alias = biggest_hub(&tree, &ag);
let under = ag
.counts(tree.resolve(&alias).expect("the hub resolves").num)
.total;
let whole = map_page("vivac", "vivac", &tree, "");
assert!(
!whole.contains("class=\"tool\" href=\"?\""),
"nothing is folded, so there is nothing to come back from"
);
let folded = map_page("vivac", "vivac", &tree, &format!("fold={alias}"));
assert!(folded.contains("class=\"tool\" href=\"?\""), "{folded}");
assert!(
folded.contains(&format!("Unfold everything · {under}")),
"and it says how much it brings back:\n{folded}"
);
}
#[test]
fn folding_does_not_change_which_lines_have_a_colour() {
let tree = real_shape();
let ag = tree.aggregates();
let alias = biggest_hub(&tree, &ag);
let whole = Map::of(&tree, &ag, &Fold::default());
let folded = Map::of(&tree, &ag, &Fold::parse(&format!("fold={alias}"), &tree));
let names =
|m: &Map| -> Vec<String> { m.lines.named.iter().map(|&l| m.line_name(l)).collect() };
assert_eq!(names(&whole), names(&folded), "the legend must not move");
for s in &folded.stops {
let before = whole.index[&s.node.num];
assert_eq!(
folded.colour(s.line),
whole.colour(whole.stops[before].line),
"{} changed colour when something else was folded",
s.node.alias()
);
}
}
#[test]
fn a_folded_node_still_says_how_branched_it_is() {
let tree = real_shape();
let ag = tree.aggregates();
let alias = biggest_hub(&tree, &ag);
let fan = tree
.children(tree.resolve(&alias).expect("the hub resolves").num)
.len();
let page = map_page("vivac", "vivac", &tree, &format!("fold={alias}"));
assert!(
page.contains(&format!("<span class=\"fan\">{fan}</span>")),
"{page}"
);
assert!(
page.contains("r=\"9\""),
"the folded hub keeps the largest radius:\n{page}"
);
}
#[test]
fn a_fold_that_names_nothing_folds_nothing_and_echoes_nothing() {
let tree = real_shape();
let page = map_page("vivac", "vivac", &tree, "fold=not-a-node,%2e%2e&x=1");
assert_eq!(page.matches("<li class=\"stop").count(), tree.total());
assert!(!page.contains("not-a-node"), "{page}");
assert!(!page.contains("%2e"), "{page}");
}
#[test]
fn folding_hides_a_blocker_without_hiding_that_it_is_waiting() {
let mut tree = Tree::default();
let (mut seq, mut num) = (0u64, 0u64);
let root = fixture_node(&mut tree, &mut seq, &mut num, None);
seq += 1;
num += 1;
tree.apply(
seq,
"2026-09-09T10:00:00Z",
&Body::NodeCreated {
node: "n2".to_string(),
num,
kind: Kind::Question,
title: "the blocker".to_string(),
why: "fixture".to_string(),
parent: Some(root.clone()),
blocks: true,
refs: vec![],
governs: vec![],
},
);
tree.sort_nodes();
let whole = map_page("vivac", "vivac", &tree, "");
assert!(whole.contains("\"bn\":1"), "{whole}");
assert!(whole.contains("\"bl\":[1]"), "{whole}");
let folded = map_page("vivac", "vivac", &tree, "fold=g1");
assert!(
folded.contains("\"bn\":1"),
"still waiting on one:\n{folded}"
);
assert!(
folded.contains("\"bl\":[]"),
"and none of them has a row to link to:\n{folded}"
);
}
#[test]
fn even_shape_has_the_same_total_as_real_shape() {
assert_eq!(even_shape().total(), real_shape().total());
assert_eq!(max_depth(&even_shape()), 6, "max depth");
}
#[test]
fn the_real_shape_does_not_render_like_an_even_tree() {
let real = map_page("vivac", "vivac", &real_shape(), "");
let even = map_page("vivac", "vivac", &even_shape(), "");
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/shape");
std::fs::create_dir_all(&dir).expect("target/shape can be created");
let real_path = dir.join("real.html");
let even_path = dir.join("even.html");
std::fs::write(&real_path, &real).expect("real.html can be written");
std::fs::write(&even_path, &even).expect("even.html can be written");
eprintln!("real shape: {}", real_path.display());
eprintln!("even shape: {}", even_path.display());
}
}