src/code_room/units.rs
//! **The build board**: every compile unit of a workspace read, merged across
//! its members, with a live state — what a Dwarves front page draws as a tree
//! (members → units) while a full read runs.
//!
//! * PLAN: per member, `cargo check --unit-graph` of its tree (the dumper runs
//! it before each commit's compile; a new commit re-plans only that member).
//! Units merge on ONE key — package (a path source named relative to the
//! workspace's clones, so `../skade` from nornir and skade itself are one
//! package), target, mode, features, profile, platform. Features are never
//! unified: another feature set is another unit. `shared_by` = the members
//! whose plan holds the unit.
//! * STATE, per unit: `queued` (its members' builds have not started),
//! `waiting` (its member is compiling, a dependency is not done),
//! `compiling` (its member is compiling, every dependency is done),
//! `reading` (compiled this build, the release is not written yet), `done`,
//! `failed`. Cargo's own messages drive it (`compiler-artifact`, `fresh`
//! included; `build-script-executed`; error diagnostics).
//!
//! One board per workspace lake, in memory: a restart starts empty and the
//! next read fills it. Read it with `Coderoom.Units` ([`answer`]).
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use serde_json::{Value, json};
/// A unit's live state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum UnitState {
Queued,
Waiting,
Compiling,
Reading,
Done,
Failed,
}
impl UnitState {
pub fn as_str(self) -> &'static str {
match self {
UnitState::Queued => "queued",
UnitState::Waiting => "waiting",
UnitState::Compiling => "compiling",
UnitState::Reading => "reading",
UnitState::Done => "done",
UnitState::Failed => "failed",
}
}
}
/// Where a member's turn is.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Queued,
Planning,
Compiling,
Reading,
Landed,
Failed,
}
impl Phase {
pub fn as_str(self) -> &'static str {
match self {
Phase::Queued => "queued",
Phase::Planning => "planning",
Phase::Compiling => "compiling",
Phase::Reading => "reading",
Phase::Landed => "landed",
Phase::Failed => "failed",
}
}
}
#[derive(Debug, Clone)]
struct Unit {
key: String,
package: String,
target: String,
mode: String,
features: Vec<String>,
test: bool,
deps: BTreeSet<usize>,
members: BTreeSet<String>,
/// Compiled (or found fresh) — by which member's build, when it was this
/// run's (`None` = before).
done: Option<Option<String>>,
failed: bool,
}
#[derive(Debug, Clone)]
struct MemberRow {
phase: Phase,
sha: String,
since: Instant,
units: BTreeSet<usize>,
/// The rustflags its builds get, before and after the linker policy.
flags: Option<mimir_ingest::RepoFlags>,
}
/// The board of one workspace.
#[derive(Debug, Default)]
pub struct Board {
/// `file://<clones>/` of the workspace: path packages are named from here.
clones: String,
units: Vec<Unit>,
by_key: HashMap<String, usize>,
/// (package, target, mode, features) → units (a lib and its test harness
/// share all four).
by_base: HashMap<String, Vec<usize>>,
members: BTreeMap<String, MemberRow>,
/// (member, head) whose plan was asked for in this process ([`Board::try_plan`]).
tried: BTreeSet<(String, String)>,
}
/// The process's boards, one per workspace lake (by its clones dir).
static BOARDS: Mutex<BTreeMap<PathBuf, Arc<Mutex<Board>>>> = Mutex::new(BTreeMap::new());
/// The board of the workspace whose consumer clones live in `clones`.
pub fn board(clones: &Path) -> Arc<Mutex<Board>> {
let mut all = BOARDS.lock().unwrap_or_else(|e| e.into_inner());
all.entry(clones.to_path_buf())
.or_insert_with(|| Arc::new(Mutex::new(Board { clones: format!("file://{}/", clones.display()), ..Default::default() })))
.clone()
}
impl Board {
/// A package id with a path source under the clones named relative to
/// them (`path+ws:skade/skade#0.7.0`): one name whichever member's tree
/// points at it.
fn package(&self, pkg_id: &str) -> String {
pkg_id.replacen(&self.clones, "ws:", 1)
}
fn base(package: &str, target: &str, mode: &str, features: &[String]) -> String {
format!("{package}|{target}|{mode}|{}", features.join(","))
}
/// A member's turn is coming (a full read queues every member first).
pub fn queue(&mut self, member: &str) {
let row = self.members.entry(member.to_string()).or_insert_with(|| MemberRow { phase: Phase::Queued, sha: String::new(), since: Instant::now(), units: BTreeSet::new(), flags: None });
if row.phase != Phase::Queued {
row.phase = Phase::Queued;
row.since = Instant::now();
}
}
pub fn phase(&mut self, member: &str, phase: Phase, sha: &str) {
let row = self.members.entry(member.to_string()).or_insert_with(|| MemberRow { phase, sha: sha.to_string(), since: Instant::now(), units: BTreeSet::new(), flags: None });
row.phase = phase;
row.since = Instant::now();
if !sha.is_empty() {
row.sha = sha.to_string();
}
if phase == Phase::Landed {
// the member's whole plan is in the log now: what it compiled,
// and every unit cargo's messages did not name apart (two units
// of one package, target and features under other dependencies)
let mine: Vec<usize> = row.units.iter().copied().collect();
for i in mine {
let u = &mut self.units[i];
match &u.done {
Some(Some(by)) if by == member => u.done = Some(None),
None if !u.failed => u.done = Some(None),
_ => {}
}
}
}
}
/// Has `member` a plan on the board?
pub fn has_plan(&self, member: &str) -> bool {
self.members.get(member).is_some_and(|r| !r.units.is_empty())
}
/// A plan of `member` at `sha` is being asked for (whether cargo gives it or not).
pub fn try_plan(&mut self, member: &str, sha: &str) {
self.tried.insert((member.to_string(), sha.to_string()));
}
/// Was a plan of `member` at `sha` asked for in this process?
pub fn tried(&self, member: &str, sha: &str) -> bool {
self.tried.contains(&(member.to_string(), sha.to_string()))
}
/// `member` is caught up at `sha`: every unit of its plan is in the log
/// (done, unless a build said it failed), its phase `landed`.
pub fn settle(&mut self, member: &str, sha: &str) {
if let Some(r) = self.members.get(member) {
for &i in &r.units {
let u = &mut self.units[i];
if !u.failed && u.done.is_none() {
u.done = Some(None);
}
}
}
self.phase(member, Phase::Landed, sha);
}
/// Merge `member`'s unit graph (`cargo … --unit-graph` JSON) into the
/// board: its units replace what the member planned before.
pub fn plan(&mut self, member: &str, graph: &Value) {
self.plan_with(member, graph, None);
}
/// [`Board::plan`] for a build that gets `flags` (they are in every
/// unit's key: cargo hashes rustflags into each unit).
pub fn plan_with(&mut self, member: &str, graph: &Value, flags: Option<&mimir_ingest::RepoFlags>) {
let units = graph["units"].as_array().map(Vec::as_slice).unwrap_or(&[]);
let flag_text = flags.map(|f| f.normalized.join(" ")).unwrap_or_default();
// a lib and its test harness: same package, target, mode, features;
// the harness is the one with more dependencies (the dev-deps)
let mut base_of: Vec<String> = Vec::with_capacity(units.len());
let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
for (i, u) in units.iter().enumerate() {
let package = self.package(u["pkg_id"].as_str().unwrap_or(""));
let target = format!("{}:{}", u["target"]["kind"].as_array().and_then(|k| k.first()).and_then(Value::as_str).unwrap_or("?"), u["target"]["name"].as_str().unwrap_or("?"));
let mut features: Vec<String> = u["features"].as_array().map(|f| f.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default();
features.sort();
let base = Self::base(&package, &target, u["mode"].as_str().unwrap_or(""), &features);
groups.entry(base.clone()).or_default().push(i);
base_of.push(base);
}
let ndeps = |i: usize| units[i]["dependencies"].as_array().map_or(0, Vec::len);
let tests: Vec<bool> = (0..units.len())
.map(|i| {
let group = &groups[&base_of[i]];
group.len() > 1 && group.iter().all(|&j| j == i || ndeps(j) < ndeps(i))
})
.collect();
// a unit's OWN key — package, target, mode, features, harness, the
// whole profile, platform, the build's flags — and its FULL key: its
// own plus the full keys of its dependencies, as cargo's unit hash
// mixes them in (two builds share a compiled unit only when both agree)
let own: Vec<String> = units
.iter()
.enumerate()
.map(|(i, u)| {
let profile = serde_json::to_string(&u["profile"]).unwrap_or_default();
let platform = u["platform"].as_str().unwrap_or("host");
format!("{}|{}|{profile}|{platform}|{flag_text}", base_of[i], if tests[i] { "test" } else { "" })
})
.collect();
let deps_of = |i: usize| -> Vec<usize> { units[i]["dependencies"].as_array().map(|d| d.iter().filter_map(|x| x["index"].as_u64().map(|j| j as usize)).filter(|&j| j < units.len()).collect()).unwrap_or_default() };
let mut full: Vec<Option<String>> = vec![None; units.len()];
for root in 0..units.len() {
// iterative post-order: a unit's key once its dependencies' are known
let mut stack = vec![(root, false)];
while let Some((i, expanded)) = stack.pop() {
if full[i].is_some() {
continue;
}
if !expanded {
stack.push((i, true));
for j in deps_of(i) {
if full[j].is_none() {
stack.push((j, false));
}
}
continue;
}
let mut dep_keys: Vec<&str> = deps_of(i).into_iter().filter_map(|j| full[j].as_deref()).collect();
dep_keys.sort_unstable();
// an in-process key: two 64-bit SipHashes (seeded apart) side by side
let mut out = String::with_capacity(32);
for seed in [0u8, 1] {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
seed.hash(&mut h);
own[i].hash(&mut h);
dep_keys.hash(&mut h);
out.push_str(&format!("{:016x}", h.finish()));
}
full[i] = Some(out);
}
}
let mut local: Vec<usize> = Vec::with_capacity(units.len());
for (i, _u) in units.iter().enumerate() {
let test = tests[i];
let key = format!("{}|{}", base_of[i], full[i].as_deref().unwrap_or(""));
let idx = match self.by_key.get(&key) {
Some(&k) => k,
None => {
let mut parts = base_of[i].splitn(4, '|');
let (package, target, mode) = (parts.next().unwrap_or("").to_string(), parts.next().unwrap_or("").to_string(), parts.next().unwrap_or("").to_string());
let features = parts.next().filter(|f| !f.is_empty()).map(|f| f.split(',').map(str::to_string).collect()).unwrap_or_default();
let k = self.units.len();
self.units.push(Unit { key: key.clone(), package, target, mode, features, test, deps: BTreeSet::new(), members: BTreeSet::new(), done: None, failed: false });
self.by_key.insert(key, k);
self.by_base.entry(base_of[i].clone()).or_default().push(k);
k
}
};
local.push(idx);
}
for (i, u) in units.iter().enumerate() {
for d in u["dependencies"].as_array().map(Vec::as_slice).unwrap_or(&[]) {
if let Some(j) = d["index"].as_u64().and_then(|j| local.get(j as usize)) {
self.units[local[i]].deps.insert(*j);
}
}
}
// the member's units are exactly this plan's
let row = self.members.entry(member.to_string()).or_insert_with(|| MemberRow { phase: Phase::Planning, sha: String::new(), since: Instant::now(), units: BTreeSet::new(), flags: None });
let old = std::mem::take(&mut row.units);
for i in old {
self.units[i].members.remove(member);
}
let row = self.members.get_mut(member).expect("inserted above");
row.units = local.iter().copied().collect();
if let Some(f) = flags {
row.flags = Some(f.clone());
}
for &i in &local {
self.units[i].members.insert(member.to_string());
}
}
/// One line of cargo's JSON stream of `member`'s build.
pub fn cargo_line(&mut self, member: &str, line: &str) {
let Ok(msg) = serde_json::from_str::<Value>(line) else { return };
let package = self.package(msg["package_id"].as_str().unwrap_or(""));
let target = || format!("{}:{}", msg["target"]["kind"].as_array().and_then(|k| k.first()).and_then(Value::as_str).unwrap_or("?"), msg["target"]["name"].as_str().unwrap_or("?"));
match msg["reason"].as_str() {
Some("compiler-artifact") => {
let target = target();
let mut features: Vec<String> = msg["features"].as_array().map(|f| f.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default();
features.sort();
let test = msg["profile"]["test"].as_bool() == Some(true);
let fresh = msg["fresh"].as_bool() == Some(true);
// a build script's compile is mode `build`; everything else of
// a check is `check`
let modes: &[&str] = if target.starts_with("custom-build:") { &["build"] } else { &["check", "build", "test"] };
for mode in modes {
let base = Self::base(&package, &target, mode, &features);
let Some(cands) = self.by_base.get(&base) else { continue };
let mine = |i: &usize| self.units[*i].members.contains(member);
let pick = cands
.iter()
.copied()
.find(|i| mine(i) && self.units[*i].test == test && self.units[*i].done.is_none())
.or_else(|| cands.iter().copied().find(|i| mine(i) && self.units[*i].test == test))
.or_else(|| cands.iter().copied().find(|i| self.units[*i].test == test))
.or_else(|| cands.first().copied());
if let Some(i) = pick {
let u = &mut self.units[i];
u.failed = false;
if u.done.is_none() || !fresh {
u.done = Some(if fresh { None } else { Some(member.to_string()) });
}
break;
}
}
}
Some("build-script-executed") => {
for u in self.units.iter_mut().filter(|u| u.package == package && u.mode == "run-custom-build") {
u.done.get_or_insert(Some(member.to_string()));
}
}
Some("compiler-message") if msg["message"]["level"].as_str() == Some("error") => {
let target = target();
for u in self.units.iter_mut().filter(|u| u.package == package && u.target == target && u.done.is_none()) {
u.failed = true;
}
}
_ => {}
}
}
fn state(&self, i: usize) -> UnitState {
let u = &self.units[i];
if u.failed {
return UnitState::Failed;
}
match &u.done {
Some(None) => return UnitState::Done,
Some(Some(by)) => {
return match self.members.get(by).map(|m| m.phase) {
Some(Phase::Landed) | None => UnitState::Done,
Some(Phase::Failed) => UnitState::Failed,
_ => UnitState::Reading,
};
}
None => {}
}
let building = u.members.iter().any(|m| self.members.get(m).is_some_and(|r| r.phase == Phase::Compiling));
if !building {
return UnitState::Queued;
}
if u.deps.iter().all(|&d| self.units[d].done.is_some()) { UnitState::Compiling } else { UnitState::Waiting }
}
/// The planner's order: the members whose units most others share
/// first (their builds compile the shared leaves), then by name.
pub fn order(&self) -> Vec<String> {
let mut m: Vec<(usize, &String)> = self.members.iter().map(|(name, r)| (self.shared_count(name).unwrap_or(0), name)).collect();
m.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(b.1)));
m.into_iter().map(|(_, n)| n.clone()).collect()
}
/// How many of `member`'s planned units another member needs too
/// (`None`: no plan for it).
pub fn shared_count(&self, member: &str) -> Option<usize> {
let r = self.members.get(member).filter(|r| !r.units.is_empty())?;
Some(r.units.iter().filter(|&&i| self.units[i].members.len() > 1).count())
}
/// The PLAN in numbers: units summed over the members, unique by the full
/// key, shared (needed by 2+ members), unique by package + target + mode
/// + features alone (what the hand count gave), the top repeats, and
/// every member's flags (configured → normalized).
pub fn plan_json(&self) -> Value {
let live: Vec<usize> = (0..self.units.len()).filter(|&i| !self.units[i].members.is_empty()).collect();
let summed: usize = self.members.values().map(|r| r.units.len()).sum();
let shared = live.iter().filter(|&&i| self.units[i].members.len() > 1).count();
let semantic: BTreeSet<String> = live.iter().map(|&i| {
let u = &self.units[i];
format!("{}|{}|{}|{}|{}", u.package, u.target, u.mode, u.features.join(","), u.test)
}).collect();
let mut top: Vec<usize> = live.clone();
top.sort_by(|a, b| self.units[*b].members.len().cmp(&self.units[*a].members.len()).then(self.units[*a].package.cmp(&self.units[*b].package)));
let top_repeats: Vec<Value> = top
.iter()
.take(20)
.map(|&i| {
let u = &self.units[i];
json!({ "package": u.package, "target": u.target, "mode": u.mode, "features": u.features, "shared_by": u.members.len() })
})
.collect();
let members: Vec<Value> = self
.members
.iter()
.map(|(m, r)| {
let own = r.units.iter().filter(|&&i| self.units[i].members.len() == 1).count();
json!({
"member": m, "units": r.units.len(), "own_units": own, "shared_units": r.units.len() - own,
"flags": r.flags.as_ref().map(|f| json!({ "configured": f.configured, "normalized": f.normalized, "changed": f.changed() })),
})
})
.collect();
json!({
"summed_units": summed,
"unique_units": live.len(),
"shared_units": shared,
"unique_by_package_features": semantic.len(),
"repeat_compiles_saved": summed.saturating_sub(live.len()),
"top_repeats": top_repeats,
"members": members,
"order": self.order(),
"flags_changed": self.members.iter().filter(|(_, r)| r.flags.as_ref().is_some_and(mimir_ingest::RepoFlags::changed)).map(|(m, _)| m.clone()).collect::<Vec<_>>(),
})
}
/// The board as JSON: members, units (all, or one member's), counts.
pub fn to_json(&self, only: Option<&str>, with_units: bool) -> Value {
let states: Vec<UnitState> = (0..self.units.len()).map(|i| self.state(i)).collect();
let selected: Vec<usize> = match only {
Some(m) => self.members.get(m).map(|r| r.units.iter().copied().collect()).unwrap_or_default(),
None => (0..self.units.len()).filter(|&i| !self.units[i].members.is_empty()).collect(),
};
let mut counts: BTreeMap<&str, u64> = BTreeMap::new();
for &i in &selected {
*counts.entry(states[i].as_str()).or_default() += 1;
}
let members: Vec<Value> = self
.members
.iter()
.filter(|(m, _)| only.is_none_or(|o| o == m.as_str()))
.map(|(m, r)| {
let done = r.units.iter().filter(|&&i| states[i] == UnitState::Done).count();
let alone = r.units.iter().filter(|&&i| self.units[i].members.len() == 1).count();
json!({ "member": m, "phase": r.phase.as_str(), "sha": r.sha, "for_ms": r.since.elapsed().as_millis() as u64, "units": r.units.len(), "done": done, "own_units": alone })
})
.collect();
let summed: usize = self.members.values().map(|r| r.units.len()).sum();
let mut out = json!({
"members": members,
"counts": counts,
"distinct_units": selected.len(),
"summed_units": summed,
"state": if selected.is_empty() { "empty_fact" } else { "complete" },
});
if with_units {
let pos: HashMap<usize, usize> = selected.iter().enumerate().map(|(p, &i)| (i, p)).collect();
let units: Vec<Value> = selected
.iter()
.map(|&i| {
let u = &self.units[i];
json!({
"key": u.key, "package": u.package, "target": u.target, "mode": u.mode, "test": u.test, "features": u.features,
"members": u.members, "shared_by": u.members.len(), "state": states[i].as_str(),
"deps": u.deps.iter().filter_map(|d| pos.get(d)).collect::<Vec<_>>(),
})
})
.collect();
out["units"] = Value::Array(units);
}
out
}
}
/// `Coderoom.Units`: the build board of the workspace (`member` narrows to
/// one member; `summary: true` leaves the unit rows out).
pub fn answer(clones: &Path, p: &Value) -> Value {
let b = board(clones);
let b = b.lock().unwrap_or_else(|e| e.into_inner());
let only = p.get("member").and_then(Value::as_str).filter(|m| !m.is_empty());
let summary = p.get("summary").and_then(Value::as_bool).unwrap_or(false);
let mut v = b.to_json(only, !summary);
v["plan"] = b.plan_json();
v["evidence"] = json!({
"notes": [
"one unit = package · target · mode · features · profile · platform, merged across the members (a path package named by its place under the workspace's clones); features are never unified: another feature set is another unit",
"states from cargo's own messages during the consumer's builds; the board is this server process's — after a restart it fills again with the next read",
]
});
v
}
/// The ONE linker flag set every repo of a read is built with (owner
/// 2026-10-15: "normalize, but DON'T ignore clang"): clang driving mold, as
/// dwarves' own `.cargo/config.toml` says. `NORNIR_MIMIR_LINKER_FLAGS`
/// (space-separated) replaces it for a box with another linker.
pub const LINKER_FLAGS: &str = "-C linker=clang -C link-arg=--ld-path=/usr/bin/ld.mold";
/// The workspace read's flag policy ([`LINKER_FLAGS`] or its override).
pub fn flag_policy() -> mimir_ingest::FlagPolicy {
let text = std::env::var("NORNIR_MIMIR_LINKER_FLAGS").ok().filter(|v| !v.trim().is_empty()).unwrap_or_else(|| LINKER_FLAGS.to_string());
mimir_ingest::FlagPolicy { linker: text.split_whitespace().map(str::to_string).collect() }
}
/// The board's sink for one member's dumper ([`mimir_ingest::DumpProgress`]).
pub struct Sink {
pub board: Arc<Mutex<Board>>,
pub member: String,
}
impl mimir_ingest::DumpProgress for Sink {
fn planned(&self, graph: &Value, flags: Option<&mimir_ingest::RepoFlags>) {
self.board.lock().unwrap_or_else(|e| e.into_inner()).plan_with(&self.member, graph, flags);
}
fn phase(&self, phase: &str, sha: &str) {
let p = match phase {
"planning" => Phase::Planning,
"compiling" => Phase::Compiling,
"reading" => Phase::Reading,
"landed" => Phase::Landed,
"failed" => Phase::Failed,
_ => Phase::Queued,
};
self.board.lock().unwrap_or_else(|e| e.into_inner()).phase(&self.member, p, sha);
}
fn cargo_line(&self, line: &str) {
self.board.lock().unwrap_or_else(|e| e.into_inner()).cargo_line(&self.member, line);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unit(pkg: &str, kind: &str, name: &str, mode: &str, features: &[&str], deps: &[usize]) -> Value {
json!({
"pkg_id": pkg, "target": { "kind": [kind], "name": name }, "profile": { "name": "dev" }, "platform": null,
"mode": mode, "features": features, "dependencies": deps.iter().map(|d| json!({ "index": d })).collect::<Vec<_>>(),
})
}
fn artifact(pkg: &str, kind: &str, name: &str, features: &[&str], test: bool, fresh: bool) -> String {
json!({ "reason": "compiler-artifact", "package_id": pkg, "target": { "kind": [kind], "name": name }, "profile": { "test": test }, "features": features, "fresh": fresh }).to_string()
}
const SERDE: &str = "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.0";
/// Two members of one workspace: a dependency with the SAME features is
/// one unit shared by both; another feature set is another unit; a path
/// package is one package whichever member's tree names it. States move
/// with cargo's messages: queued → waiting/compiling → reading → done.
#[test]
fn units_merge_on_their_key_and_their_state_follows_cargo() {
let mut b = Board { clones: "file:///ws/clones/".into(), ..Default::default() };
let skade = "path+file:///ws/clones/skade/skade#0.7.0";
let nornir = "path+file:///ws/clones/nornir#0.6.0";
// skade: serde (derive), skade lib, skade lib's test harness
b.plan("skade", &json!({ "units": [
unit(SERDE, "lib", "serde", "check", &["derive"], &[]),
unit(skade, "lib", "skade", "check", &[], &[0]),
unit(skade, "lib", "skade", "check", &[], &[0, 1]),
]}));
// nornir: serde (derive, the same unit) + serde (std: another), skade lib, nornir lib
b.plan("nornir", &json!({ "units": [
unit(SERDE, "lib", "serde", "check", &["derive"], &[]),
unit(SERDE, "lib", "serde", "check", &["std"], &[]),
unit(skade, "lib", "skade", "check", &[], &[0]),
unit(nornir, "lib", "nornir", "check", &[], &[1, 2]),
]}));
let v = b.to_json(None, true);
assert_eq!((v["distinct_units"].as_u64(), v["summed_units"].as_u64()), (Some(5), Some(7)));
let row = |v: &Value, key_part: &str, test: bool| v["units"].as_array().unwrap().iter().find(|u| u["key"].as_str().unwrap().contains(key_part) && u["test"] == test).cloned().unwrap();
assert_eq!(row(&v, "serde@1.0.0|lib:serde|check|derive", false)["shared_by"], 2, "one serde+derive for both");
assert_eq!(row(&v, "ws:skade/skade#0.7.0|lib:skade", false)["shared_by"], 2, "the path package is one package");
assert_eq!(row(&v, "ws:skade/skade#0.7.0|lib:skade", true)["shared_by"], 1, "the harness is skade's alone");
assert_eq!(v["counts"]["queued"], 5, "nothing started");
b.queue("nornir");
b.phase("skade", Phase::Compiling, "aaa");
let v = b.to_json(None, true);
assert_eq!(row(&v, "serde|check|derive", false)["state"], "compiling", "no dependency: compiling");
assert_eq!(row(&v, "ws:skade/skade#0.7.0|lib:skade", false)["state"], "waiting", "serde is not done");
b.cargo_line("skade", &artifact(SERDE, "lib", "serde", &["derive"], false, false));
let v = b.to_json(None, true);
assert_eq!(row(&v, "serde|check|derive", false)["state"], "reading", "compiled, not yet in the log");
assert_eq!(row(&v, "ws:skade/skade#0.7.0|lib:skade", false)["state"], "compiling");
b.cargo_line("skade", &artifact(skade, "lib", "skade", &[], false, false));
b.cargo_line("skade", &artifact(skade, "lib", "skade", &[], true, false));
b.phase("skade", Phase::Landed, "aaa");
let v = b.to_json(Some("skade"), true);
assert_eq!(v["counts"]["done"], 3, "{v}");
// nornir's build: serde+derive and skade are fresh (the shared target)
b.phase("nornir", Phase::Compiling, "bbb");
b.cargo_line("nornir", &artifact(SERDE, "lib", "serde", &["derive"], false, true));
b.cargo_line("nornir", &artifact(skade, "lib", "skade", &[], false, true));
b.cargo_line("nornir", &json!({ "reason": "compiler-message", "package_id": nornir, "target": { "kind": ["lib"], "name": "nornir" }, "message": { "level": "error" } }).to_string());
let v = b.to_json(Some("nornir"), true);
assert_eq!(row(&v, "serde|check|std", false)["state"], "compiling");
assert_eq!(row(&v, "nornir#0.6.0|lib:nornir", false)["state"], "failed");
assert_eq!(v["counts"]["done"], 2, "{v}");
let m = &v["members"][0];
assert_eq!((m["member"].as_str(), m["phase"].as_str(), m["units"].as_u64(), m["own_units"].as_u64()), (Some("nornir"), Some("compiling"), Some(4), Some(2)));
}
/// A new commit re-plans only its member: units it no longer has are no
/// longer its own.
#[test]
fn a_new_plan_replaces_the_members_units() {
let mut b = Board { clones: "file:///ws/clones/".into(), ..Default::default() };
b.plan("a", &json!({ "units": [unit(SERDE, "lib", "serde", "check", &[], &[])] }));
b.plan("a", &json!({ "units": [unit(SERDE, "lib", "serde", "check", &["std"], &[])] }));
let v = b.to_json(None, true);
assert_eq!(v["distinct_units"], 1, "{v}");
assert!(v["units"][0]["key"].as_str().unwrap().contains("|std|"));
}
/// The board is a surface of the door: answered on the request (no room
/// is opened), empty until a build ran in this process.
#[test]
fn the_board_is_asked_through_the_door() {
let ws = crate::code_room::reader::fixture::fresh_shop();
let reach = crate::code_room::Reach::room_only(&ws.handle);
let v = crate::code_room::reader::door("Coderoom.Units", &reach, "{}");
assert_eq!((v["surface"].as_str(), v["state"].as_str()), (Some("Coderoom.Units"), Some("empty_fact")), "{v}");
assert!(v["members"].as_array().is_some_and(|m| m.is_empty()) && v["units"].as_array().is_some());
let b = board(&ws.handle.layout().clones());
b.lock().unwrap().plan("shop", &json!({ "units": [unit(SERDE, "lib", "serde", "check", &[], &[])] }));
let v = crate::code_room::reader::door("Coderoom.Units", &reach, r#"{"summary":true}"#);
assert_eq!((v["state"].as_str(), v["distinct_units"].as_u64(), v.get("units").is_none()), (Some("complete"), Some(1), true), "{v}");
assert!(!crate::code_room::reader::is_queued("Coderoom.Units"), "the board is cheap: answered at once");
}
/// The PLANNER's key is cargo's: a unit is shared only when its own key
/// AND its dependencies' keys agree — `log` with the same features under
/// a `serde` with other features is another unit — and the build's flags
/// are in it (a repo with its own linker flags shares nothing until they
/// are normalized). The plan says summed / unique / shared, by package +
/// features alone, the top repeats, and the order (most shared first).
#[test]
fn the_plan_merges_on_the_full_key_with_dependencies_and_flags() {
let flags = |n: &[&str]| mimir_ingest::RepoFlags { configured: vec![], normalized: n.iter().map(|s| s.to_string()).collect() };
let clang = flags(&["-C", "linker=clang"]);
const LOG: &str = "registry+https://github.com/rust-lang/crates.io-index#log@0.4.0";
let graph = |serde_features: &[&str], name: &str| json!({ "units": [
unit(SERDE, "lib", "serde", "check", serde_features, &[]),
unit(LOG, "lib", "log", "check", &[], &[0]),
unit(&format!("path+file:///ws/clones/{name}#0.1.0"), "lib", name, "check", &[], &[1]),
]});
let mut b = Board { clones: "file:///ws/clones/".into(), ..Default::default() };
b.plan_with("a", &graph(&["std"], "a"), Some(&clang));
b.plan_with("b", &graph(&["std"], "b"), Some(&clang));
b.plan_with("c", &graph(&["std", "derive"], "c"), Some(&clang));
b.plan_with("d", &graph(&["std"], "d"), Some(&flags(&["-C", "linker=gcc"])));
let p = b.plan_json();
// serde(std)+log shared by a and b; c's serde differs → its log differs
// too (the cascade); d's flags differ → all of d's units are its own
assert_eq!((p["summed_units"].as_u64(), p["unique_units"].as_u64(), p["shared_units"].as_u64()), (Some(12), Some(10), Some(2)), "{p}");
assert_eq!(p["unique_by_package_features"].as_u64(), Some(7), "serde×2 features, log, the four member libs: {p}");
assert_eq!(p["top_repeats"][0]["shared_by"], 2);
assert_eq!(p["order"][0], "a", "a and b share the most; then by name");
let a = p["members"].as_array().unwrap().iter().find(|m| m["member"] == "a").unwrap();
assert_eq!((a["units"].as_u64(), a["shared_units"].as_u64()), (Some(3), Some(2)));
assert!(a["flags"]["changed"].as_bool().unwrap(), "{a}");
assert_eq!(p["flags_changed"].as_array().unwrap().len(), 4);
}
/// A member that is CAUGHT UP is on the board with its whole plan, every
/// unit done — the tree shows all members, not only those read since the
/// server started (live dwarf 2026-10-16: 13 of 21 repos showed 0/0).
#[test]
fn a_caught_up_member_is_planned_and_settled() {
let mut b = Board { clones: "file:///ws/clones/".into(), ..Default::default() };
assert!(!b.has_plan("a"));
b.plan("a", &json!({ "units": [unit(SERDE, "lib", "serde", "check", &[], &[]), unit("path+file:///ws/clones/a#0.1.0", "lib", "a", "check", &[], &[0])] }));
assert!(b.has_plan("a"));
b.settle("a", "abc");
let v = b.to_json(Some("a"), false);
assert_eq!((v["counts"]["done"].as_u64(), v["members"][0]["phase"].as_str(), v["members"][0]["units"].as_u64(), v["members"][0]["done"].as_u64()), (Some(2), Some("landed"), Some(2), Some(2)), "{v}");
}
/// A member that LANDED has every unit of its plan in the log — also a
/// unit cargo's messages never named apart (live dwarf 2026-10-16: nornir
/// landed with 103 units still queued/waiting: two units of one package,
/// target and features — another dependency set — and the one artifact
/// line marks only one of them).
#[test]
fn a_landed_member_has_every_unit_done() {
let mut b = Board { clones: "file:///ws/clones/".into(), ..Default::default() };
const LOG: &str = "registry+https://github.com/rust-lang/crates.io-index#log@0.4.0";
b.plan("n", &json!({ "units": [
unit(LOG, "lib", "log", "check", &[], &[]),
unit(SERDE, "lib", "serde", "check", &[], &[]),
unit(SERDE, "lib", "serde", "check", &[], &[0]),
]}));
b.phase("n", Phase::Compiling, "s1");
b.cargo_line("n", &artifact(LOG, "lib", "log", &[], false, false));
b.cargo_line("n", &artifact(SERDE, "lib", "serde", &[], false, false));
b.phase("n", Phase::Landed, "s1");
let v = b.to_json(Some("n"), false);
assert_eq!((v["counts"]["done"].as_u64(), v["members"][0]["done"].as_u64()), (Some(3), Some(3)), "{v}");
// a unit a build said FAILED stays failed
b.phase("n", Phase::Compiling, "s2");
b.cargo_line("n", &json!({ "reason": "compiler-message", "package_id": LOG, "target": { "kind": ["lib"], "name": "log" }, "message": { "level": "error" } }).to_string());
b.phase("n", Phase::Landed, "s2");
let v = b.to_json(Some("n"), false);
assert_eq!(v["counts"]["done"].as_u64(), Some(3), "a failure after done does not undo the log: {v}");
}
}