src/code_room/doctor.rs
//! Is this box ready to fill a code room? One line per requirement, each
//! with the command that fixes it (pro-list item 3: "installing the server is
//! one step and says what is missing").
//!
//! Asked by `dwarves-server --check` and at the end of `--install`, as the
//! user the service runs as: the driver, the toolchain and the model are
//! looked up in THAT user's home and PATH. GraphBLAS is not a line — a binary
//! that links the room does not start without it, so the check never runs on
//! a box that lacks it.
use std::path::{Path, PathBuf};
use mimir_ingest::DriverUnavailable;
/// How one requirement stands.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mark {
/// There and working.
Ok,
/// Missing: the room cannot be filled until it is fixed.
Missing,
/// Missing, and the room works without it (a feature is off).
Optional,
}
/// One requirement: what, how it stands, what was seen, how to fix it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Check {
pub what: &'static str,
pub mark: Mark,
pub detail: String,
pub fix: Option<String>,
}
impl Check {
fn ok(what: &'static str, detail: impl Into<String>) -> Self {
Self { what, mark: Mark::Ok, detail: detail.into(), fix: None }
}
fn missing(what: &'static str, detail: impl Into<String>, fix: impl Into<String>) -> Self {
Self { what, mark: Mark::Missing, detail: detail.into(), fix: Some(fix.into()) }
}
}
/// Every requirement of a server that fills code rooms, checked on this box
/// for the running user.
pub fn server_checks() -> Vec<Check> {
let mut out = Vec::new();
out.push(cargo_on_path(std::env::var_os("PATH").as_deref()));
let settings = super::driver::Settings::from_env();
match settings.locate() {
Err(r) => out.push(Check::missing(
"Mímir driver",
r.to_string(),
format!(
"build cargo-mimir-dump and mimir-driver (`cargo build --release` in nornir's crates/mimir-driver, on the pinned nightly) and put them into {}",
crate::config::nornir_home().join("mimir-driver").display()
),
)),
Ok(spec) => match spec.probe() {
Ok(ready) => {
out.push(Check::ok(
"Mímir driver",
format!("{} — {}", spec.cargo_mimir_dump.display(), ready.rustc),
));
out.push(std_sources(&ready.sysroot, &spec.toolchain));
}
Err(e) => out.push(Check::missing("Mímir driver", e.to_string(), driver_fix(&e))),
},
}
out.push(embed_model());
out
}
/// The command that fixes a driver probe failure.
fn driver_fix(e: &DriverUnavailable) -> String {
match e {
DriverUnavailable::MissingBinary { path, .. } => format!(
"put cargo-mimir-dump and mimir-driver (built in nornir's crates/mimir-driver) into {}",
path.parent().map(|p| p.display().to_string()).unwrap_or_default()
),
DriverUnavailable::HostFailed { .. } => {
"install rustup for this user (https://rustup.rs), or fix NORNIR_MIMIR_DRIVER_HOST".to_string()
}
DriverUnavailable::ToolchainMissing { toolchain, .. } | DriverUnavailable::NoCompilerLibs { toolchain, .. } => {
format!("rustup toolchain install {toolchain} --profile minimal --component rustc-dev,rust-src,llvm-tools")
}
DriverUnavailable::DriverDoesNotLoad { toolchain, .. } => format!(
"the driver was built for another nightly: rebuild it on {toolchain}, or set NORNIR_MIMIR_TOOLCHAIN to the one it was built with"
),
}
}
/// `cargo` is what the dumper starts in every member's tree.
fn cargo_on_path(path: Option<&std::ffi::OsStr>) -> Check {
let found = path
.map(|p| std::env::split_paths(p).map(|d| d.join("cargo")).find(|c| c.is_file()))
.unwrap_or(None);
match found {
Some(c) => Check::ok("cargo", c.display().to_string()),
None => Check::missing(
"cargo",
"no `cargo` on PATH",
"install rustup for this user (https://rustup.rs) or link ~/.cargo/bin/cargo into /usr/local/bin",
),
}
}
/// The standard library's sources: the std dump (once per toolchain) reads them.
fn std_sources(sysroot: &Path, toolchain: &str) -> Check {
let lib: PathBuf = sysroot.join("lib/rustlib/src/rust/library/std");
if lib.is_dir() {
Check::ok("std sources", lib.display().to_string())
} else {
Check::missing(
"std sources",
format!("no {}", lib.display()),
format!("rustup component add rust-src --toolchain {toolchain}"),
)
}
}
/// The embedding model: without it the vector lenses answer that vectors are off.
fn embed_model() -> Check {
#[cfg(any(feature = "embed-tract", feature = "embed-ort"))]
{
let dir = crate::vector::embed_support::model_dir();
if dir.join("tokenizer.json").is_file() {
return Check::ok("embedding model", dir.display().to_string());
}
Check {
what: "embedding model",
mark: Mark::Optional,
detail: format!("no model at {} — vector lenses are off", dir.display()),
fix: Some("copy the model directory to /opt/nornir/models (or set NORNIR_MODEL_DIR)".to_string()),
}
}
#[cfg(not(any(feature = "embed-tract", feature = "embed-ort")))]
Check {
what: "embedding model",
mark: Mark::Optional,
detail: "this build has no embedder — vector lenses are off".to_string(),
fix: None,
}
}
/// True when nothing required is missing.
pub fn ready(checks: &[Check]) -> bool {
checks.iter().all(|c| c.mark != Mark::Missing)
}
/// The checks as lines a person reads: `✓ what — detail`, `✗ what — detail`
/// and the fix under it.
pub fn render(checks: &[Check]) -> String {
let mut s = String::new();
for c in checks {
let sign = match c.mark {
Mark::Ok => "✓",
Mark::Missing => "✗",
Mark::Optional => "–",
};
s.push_str(&format!("{sign} {} — {}\n", c.what, c.detail));
if let (Some(fix), false) = (&c.fix, c.mark == Mark::Ok) {
s.push_str(&format!(" fix: {fix}\n"));
}
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cargo_is_found_on_the_path_or_said_missing_with_a_fix() {
let dir = std::env::temp_dir().join(format!("nornir-doctor-cargo-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let none = cargo_on_path(Some(dir.as_os_str()));
assert_eq!(none.mark, Mark::Missing);
assert!(none.fix.as_deref().unwrap().contains("rustup"), "{none:?}");
std::fs::write(dir.join("cargo"), "").unwrap();
let some = cargo_on_path(Some(dir.as_os_str()));
assert_eq!(some.mark, Mark::Ok, "{some:?}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn missing_std_sources_name_the_rustup_command() {
let c = std_sources(Path::new("/nonexistent-sysroot"), "nightly-2026-10-05");
assert_eq!(c.mark, Mark::Missing);
assert_eq!(c.fix.as_deref(), Some("rustup component add rust-src --toolchain nightly-2026-10-05"));
}
#[test]
fn each_driver_failure_has_its_own_fix() {
let tc = "nightly-2026-10-05".to_string();
let missing = driver_fix(&DriverUnavailable::MissingBinary { which: "mimir-driver", path: PathBuf::from("/h/.nornir/mimir-driver/mimir-driver") });
assert!(missing.contains("/h/.nornir/mimir-driver"), "{missing}");
let toolchain = driver_fix(&DriverUnavailable::ToolchainMissing { toolchain: tc.clone(), detail: String::new() });
assert!(toolchain.starts_with("rustup toolchain install nightly-2026-10-05"), "{toolchain}");
assert!(toolchain.contains("rustc-dev") && toolchain.contains("rust-src"), "{toolchain}");
let libs = driver_fix(&DriverUnavailable::NoCompilerLibs { toolchain: tc, sysroot: PathBuf::new() });
assert_eq!(libs, toolchain, "rustc-dev missing is fixed by the same install");
}
#[test]
fn render_shows_the_fix_only_where_something_is_wrong_and_ready_ignores_optional() {
let checks = vec![
Check::ok("cargo", "/usr/bin/cargo"),
Check::missing("std sources", "no /x", "rustup component add rust-src --toolchain t"),
Check { what: "embedding model", mark: Mark::Optional, detail: "off".into(), fix: Some("copy".into()) },
];
let text = render(&checks);
assert_eq!(
text,
"✓ cargo — /usr/bin/cargo\n✗ std sources — no /x\n fix: rustup component add rust-src --toolchain t\n– embedding model — off\n fix: copy\n"
);
assert!(!ready(&checks));
assert!(ready(&[checks[0].clone(), checks[2].clone()]), "an optional line does not block");
}
}