Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-config/src/lib.rs

//! Loader for the consumer's `nornir.toml`.
//!
//! Discovery rule (used by the CLI when no explicit path is given):
//! walk up from `cwd` looking for `workspace_holger/release/nornir.toml`.
//! The discovered file's *grandparent of the grandparent* is the
//! **workspace root** — the dir containing `workspace_holger/`,
//! `holger/`, `znippy/`, etc. All relative paths inside `nornir.toml`
//! (including `[guard].forbidden`) are interpreted against that root.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::OnceLock;

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};

// ── The nornir data root: `<base>/.nornir`, base = the running user's home ──
//
// ONE rule for everyone, NO path env-vars. The nornir data root is
// `<base>/.nornir`, where `<base>` defaults to the RUNNING USER's home:
//   - a login user `alice` (HOME=/home/alice) → `/home/alice/.nornir`
//   - the server (system user `nornir`, HOME=/home/nornir) → `/home/nornir/.nornir`
// Containers get the right path by setting the user's `$HOME` (the standard
// way) — never a nornir-specific env var. Home is resolved with the `home`
// crate, which reads `$HOME` then falls back to getpwuid, so it's correct for
// the systemd service user AND for `sudo -H -u nornir`.
//
// ── The EMBEDDER seam ([`pin_nornir_base`]) ─────────────────────────────────
//
// An EMBEDDING product (`dwarves-server`/`dwarves-solo` compile nornir in) has a
// data root of its OWN that it declares to its uninstaller. Before this seam the
// embedded nornir still wrote to `$HOME/.nornir` — a SECOND root beside the
// declared one, which is why dwarves' uninstall carried a hardcoded legacy-root
// special case for `/home/dwarves/.nornir`. A maintained list of known roots is
// exactly what the uninstall law exists to eliminate.
//
// The only lever that existed was `$HOME`, and moving that also moves `~/.cargo`,
// `~/.rustup` and the server's PATH injection — which is why nobody pulled it.
//
// This is NOT a path env-var and the policy above stands unchanged: it is an
// in-process, set-once override an embedder installs before it starts the server.
// The SHAPE is still `<base>/.nornir` derived by the one writer
// [`nornir_home_from`] — only the base moves, so no caller learns a second layout.

/// Pure, testable: the nornir data root for a given base directory.
/// `nornir_home_from(Path::new("/home/alice")) == "/home/alice/.nornir"`.
pub fn nornir_home_from(home: &Path) -> PathBuf {
    home.join(".nornir")
}

/// The process-wide base pinned by [`pin_nornir_base`]. Unset ⇒ the running
/// user's home.
static PINNED_BASE: OnceLock<PathBuf> = OnceLock::new();

/// Has [`nornir_home`] already answered? Set on every resolution, so a pin that
/// arrives too late is REFUSED rather than silently splitting the process across
/// two roots — the exact failure this seam exists to remove.
static BASE_RESOLVED: AtomicBool = AtomicBool::new(false);

/// The minimum base depth. `/` and one-component bases (`/home`, `/var`) would
/// put the data root at `/.nornir` or `/home/.nornir`; a sweep of either is a
/// catastrophe, so the mistake fails where it is written. Mirrors
/// `skidbladnir::uninstall::MIN_ROOT_COMPONENTS`.
const MIN_BASE_COMPONENTS: usize = 2;

/// WHY a [`pin_nornir_base`] was refused.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PinRefused {
    /// Not absolute — it would resolve against the process CWD, and a systemd
    /// unit's `WorkingDirectory=/` makes that the filesystem root.
    NotAbsolute { attempted: PathBuf },
    /// Fewer than [`MIN_BASE_COMPONENTS`] components.
    TooShallow { attempted: PathBuf, components: usize },
    /// [`nornir_home`] already answered with the default; pinning now would move
    /// the root out from under whatever already opened it.
    AlreadyResolved { attempted: PathBuf },
    /// A different base is already pinned. Two bases IS the two-roots bug.
    Conflict { pinned: PathBuf, attempted: PathBuf },
}

impl std::fmt::Display for PinRefused {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PinRefused::NotAbsolute { attempted } => write!(
                f,
                "nornir data-root base {} is not an absolute path",
                attempted.display()
            ),
            PinRefused::TooShallow { attempted, components } => write!(
                f,
                "nornir data-root base {} is only {components} component(s) deep \
                 (minimum {MIN_BASE_COMPONENTS})",
                attempted.display()
            ),
            PinRefused::AlreadyResolved { attempted } => write!(
                f,
                "cannot pin the nornir data-root base to {} — the root was already \
                 resolved in this process; pin BEFORE anything reads it",
                attempted.display()
            ),
            PinRefused::Conflict { pinned, attempted } => write!(
                f,
                "the nornir data-root base is already pinned to {} — refusing to \
                 re-pin it to {}",
                pinned.display(),
                attempted.display()
            ),
        }
    }
}

impl std::error::Error for PinRefused {}

/// Pure decision, dependency-injected: may `want` become the base, given the
/// currently-pinned base and whether the root has already been resolved?
///
/// Split out from [`pin_nornir_base`] because the global is set-once per PROCESS
/// — a test can exercise the real setter exactly once, so every rule lives here
/// where each can be proven independently.
fn decide_pin(
    pinned: Option<&Path>,
    already_resolved: bool,
    want: &Path,
) -> Result<PathBuf, PinRefused> {
    if let Some(p) = pinned {
        return if p == want {
            Ok(want.to_path_buf()) // idempotent — the same pin twice is fine
        } else {
            Err(PinRefused::Conflict { pinned: p.to_path_buf(), attempted: want.to_path_buf() })
        };
    }
    if !want.is_absolute() {
        return Err(PinRefused::NotAbsolute { attempted: want.to_path_buf() });
    }
    let components = want
        .components()
        .filter(|c| matches!(c, std::path::Component::Normal(_)))
        .count();
    if components < MIN_BASE_COMPONENTS {
        return Err(PinRefused::TooShallow { attempted: want.to_path_buf(), components });
    }
    if already_resolved {
        return Err(PinRefused::AlreadyResolved { attempted: want.to_path_buf() });
    }
    Ok(want.to_path_buf())
}

/// **The embedder seam.** Pin the base the nornir data root hangs under for the
/// rest of THIS PROCESS, so an embedding product's nornir state lands inside the
/// root that product declares to its uninstaller — one declared root, one eraser.
///
/// Returns the resulting data root (`<base>/.nornir`). Set-once: pinning the same
/// base again is a no-op; a DIFFERENT base, or a pin after the root has already
/// been read, is [`PinRefused`] — never a quiet second root.
///
/// Call it before starting the server (dwarves does, in its fat serve leg). It is
/// deliberately not an env var: `NORNIR_ROOT`/`NORNIR_WAREHOUSE_ROOT` were removed
/// on purpose and the no-path-env-vars policy above is unchanged.
pub fn pin_nornir_base(base: &Path) -> Result<PathBuf, PinRefused> {
    let decided = decide_pin(
        PINNED_BASE.get().map(PathBuf::as_path),
        BASE_RESOLVED.load(Ordering::Acquire),
        base,
    )?;
    // A racing setter can win the OnceLock; the loser must still agree, or it is
    // the two-roots bug wearing a thread.
    match PINNED_BASE.set(decided.clone()) {
        Ok(()) => Ok(nornir_home_from(&decided)),
        Err(_) => {
            let winner = PINNED_BASE.get().expect("set() failed ⇒ a value is present");
            if winner == &decided {
                Ok(nornir_home_from(&decided))
            } else {
                Err(PinRefused::Conflict { pinned: winner.clone(), attempted: decided })
            }
        }
    }
}

/// The currently-pinned base, or `None` when the root still derives from `$HOME`.
pub fn pinned_nornir_base() -> Option<PathBuf> {
    PINNED_BASE.get().cloned()
}

/// The nornir data root: `<base>/.nornir`, where `<base>` is the
/// [`pin_nornir_base`] pin when an embedder set one, else the **running user's**
/// home. Panics only if the OS can resolve no home at all (no `$HOME`, no passwd
/// entry) and nothing was pinned — a misconfigured environment we can't sensibly
/// proceed in.
pub fn nornir_home() -> PathBuf {
    // Record the read BEFORE answering: a pin arriving after this point must be
    // refused, even if it races us.
    BASE_RESOLVED.store(true, Ordering::Release);
    if let Some(base) = PINNED_BASE.get() {
        return nornir_home_from(base);
    }
    nornir_home_from(&home::home_dir().expect("resolve running user's HOME"))
}

/// The workspace **registry root** — `<home>/.nornir/workspaces`. Holds
/// `registry.redb` and the per-workspace `<name>/{git,builds}/` trees.
pub fn registry_root() -> PathBuf {
    nornir_home().join("workspaces")
}

/// The default embedded/fat **warehouse** — `<home>/.nornir/warehouse`.
pub fn warehouse_default_root() -> PathBuf {
    nornir_home().join("warehouse")
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Nornir {
    #[serde(default)]
    pub guard: Guard,
    #[serde(default)]
    pub storage: Storage,
    #[serde(default)]
    pub repo: BTreeMap<String, Repo>,
    /// Declarative build variants (task #19) — the set of valid custom builds a
    /// repo offers (holger's server/agent/mannequin/gui, …). Empty by default;
    /// drives `nornir build --variant <name>`. The same `[build]` /
    /// `[[build.variant]]` schema can instead live in a standalone
    /// `.nornir/build-variants.toml` (see `nornir_build_thing::variant`).
    #[serde(default)]
    pub build: nornir_build_thing::variant::BuildVariantSet,
    /// Registry-state check backend (release-improvements item #10). The release
    /// preflight asks this registry "what version is actually PUBLISHED?" per
    /// crate to catch a local-vs-published DESYNC. Default = crates.io (no config
    /// needed); set `backend = "custom"` + `url = "<sparse-index-base>"` to point
    /// at a private registry / holger mirror. A `--registry-url` CLI flag overrides.
    #[serde(default)]
    pub registry: RegistryConfig,
}

/// The `[registry]` section — which backend the release preflight's
/// registry-state check queries. Defaults to crates.io.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RegistryConfig {
    /// `"crates-io"` (default) or `"custom"`.
    #[serde(default = "default_registry_backend")]
    pub backend: String,
    /// Sparse-index base URL when `backend = "custom"` (e.g. a private registry /
    /// holger mirror). Ignored for `crates-io`.
    #[serde(default)]
    pub url: String,
}

fn default_registry_backend() -> String {
    "crates-io".to_string()
}

impl Default for RegistryConfig {
    fn default() -> Self {
        Self { backend: default_registry_backend(), url: String::new() }
    }
}

// NB (config seam): `nornir.toml` owns release *recipes* (gates/bench/
// publish_order) keyed by member name. It does **not** own where source comes
// from or whether a workspace is monitored:
//   - member *source* (path | git + branch/ref) lives in the workspace
//     descriptor `nornir-workspace.toml` (`crate::workspace::descriptor`);
//   - *monitored-ness* + poll interval + per-member sync state live in the
//     server's registry record (`crate::registry`);
//   - the server *root* (`<root>/<ws>/{git,builds}`) is server-global —
//     the home-derived `registry_root()` (`<home>/.nornir/workspaces`).
// `[repo].remote` below is kept only as doc-link metadata (README/CHANGELOG
// "repository" links); for monitored source the descriptor's `git` is
// authoritative.

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Guard {
    #[serde(default)]
    pub forbidden: Vec<String>,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Storage {
    /// `"local"` (default) or `"remote"`. Empty string also = local.
    #[serde(default)]
    pub kind: String,
    /// Workspace-root-relative dir holding `warehouse/` and `cache/`.
    /// Defaults to `<home>/.nornir/workspaces/<name>/builds` (see `warehouse_root`)
    /// when empty.
    #[serde(default)]
    pub local_path: String,
    /// The warehouse ROOT, named VERBATIM — no `warehouse/` appended.
    ///
    /// `local_path` can only ever name `<x>/warehouse`, because the resolver
    /// appends that segment. The per-workspace layout the server serves is
    /// `<home>/.nornir/workspaces/<name>/builds`, which HOLDS `catalog.redb`
    /// beside a nested `warehouse/` data dir — so that root was unreachable
    /// through `local_path` and could only be obtained by leaving `[storage]`
    /// out entirely and letting the resolver guess it from the workspace name.
    /// That guess is ?41 defect 1: it reads the CONFIG PATH, which is a property
    /// of how a verb found its config rather than of the directory the operator
    /// is standing in, so two verbs could each answer honestly and land on two
    /// warehouses.
    ///
    /// `root_path` closes the guess by making its answer sayable. Absolute is
    /// honored verbatim; relative is joined under `workspace_root`. It wins over
    /// `local_path` when both are set.
    #[serde(default)]
    pub root_path: String,
    /// Flight endpoint URL when `kind = "remote"`.
    #[serde(default)]
    pub remote_url: String,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Repo {
    /// On-disk directory of this repo's checkout, **relative to the workspace
    /// root** (or absolute). Overrides the default `<workspace_root>/<name>`
    /// convention — needed when the `[repo.<name>]` key and the directory
    /// differ, e.g. a temp config that renders nornir's own book from a
    /// `nornir-orch` worktree (`[repo.nornir] path = "nornir-orch"`). Empty =
    /// fall back to the by-name convention (case-insensitive). Honored by
    /// [`Nornir::repo_dir_for`].
    #[serde(default)] pub path: String,
    /// Doc-link metadata only (README/CHANGELOG "repository" links). NOT the
    /// source-of-truth for monitored fetch — that's the descriptor's `git`.
    #[serde(default)] pub remote: String,
    #[serde(default)] pub history: String,
    #[serde(default)] pub readme: String,
    #[serde(default)] pub publish_order: Vec<Vec<String>>,
    #[serde(default)] pub gates: Gates,
    #[serde(default)] pub bench: BenchSpec,
    /// `true` ⇒ this repo is PRIVATE and must NEVER be published to crates.io. The
    /// crate-metadata gate fails it ("private — do not publish") and the promote path
    /// hard-refuses it. Set for SSH-cloned nordisk repos (nornir, holger, korp,
    /// ordning); the public HTTPS repos (facett, skade, znippy, knut) leave it false.
    #[serde(default)] pub private: bool,
    /// Extra cargo features to build this repo's **test matrix** with, comma-
    /// separated (e.g. `gui,testmatrix`). Honored by `nornir test run <repo>`:
    /// it seeds `NORNIR_TEST_FEATURES`, which the matrix's unit aspect turns into
    /// `cargo test --features <list>`. Without it the unit aspect runs the repo's
    /// DEFAULT feature set, so any feature-gated test (e.g. holger's
    /// `--features gui` robot-UI cell) is silently skipped instead of landing as a
    /// matrix row. An explicit `NORNIR_TEST_FEATURES` in the environment always
    /// wins (a per-session override). Empty = default feature set.
    #[serde(default)] pub test_features: String,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Gates {
    #[serde(default)] pub no_path_patches: bool,
    #[serde(default)] pub nexus_floor: bool,
    #[serde(default)] pub no_regression: bool,
    #[serde(default)] pub max_regression_pct: f64,
    /// The CORE-SATURATION LAW gate: fail the release if any PARALLEL bench in
    /// the newest run under-used the machine's cores (peak occupancy below
    /// `min_core_busy_frac × n_cores`). Reads the warehouse `bench_telemetry`
    /// table's `proc_cores_max` — **this process's** task-clock ÷ wall from
    /// `/proc/self/stat`, not `/proc/stat`'s host-wide `cores_busy_max`, which
    /// counts the neighbours and so let this gate pass a strictly serial arm on
    /// any loaded box. Single-thread `_st` variants and single-core hosts are
    /// exempt; rows written before `proc_cores_max` existed are reported as
    /// SKIPPED rather than counted green.
    #[serde(default)] pub core_saturation: bool,
    /// Minimum peak core-occupancy fraction (0..1) a parallel bench must reach
    /// when `core_saturation` is on. `0.0` → the release gate's default
    /// `CORE_SATURATION_MIN_BUSY_FRAC` (0.9).
    #[serde(default)] pub min_core_busy_frac: f64,
    #[serde(default)] pub integration_roundtrip: Vec<String>,
    #[serde(default)] pub docs_fresh: bool,
    /// Fail the release if any `[guard].forbidden` path drifted from the
    /// recorded manifest (perm re-grant or out-of-band content change).
    #[serde(default)] pub guard_intact: bool,
    /// Plan #5: run the repo's TEST MATRIX at release time and fail the gate on
    /// red (any failing/stalled case in the newest run) or a regression vs the
    /// last green run. HEAVY (it executes the suite) — opt-in per repo, and the
    /// runner is invoked release/on-request only, never on workspace load.
    #[serde(default)] pub tests: bool,
    /// AUT5: the autonom completeness gate — fail the release if any discovered
    /// surface (viz tab × {thin,fat}, CLI subcommand, MCP tool, unreached fn) has
    /// NO covering test row and is NOT excused by a live `.nornir/autonom-allow.toml`
    /// entry (`Surface − Covered ≠ ∅`), OR an allowlist entry is stale. HARD zero,
    /// not a ratchet. Reads the warehouse `surface_coverage` table; LIGHT (no
    /// suite run), so it can default-on per repo unlike the heavy `tests` gate.
    #[serde(default)] pub coverage: bool,
}

/// Per-repo bench contract.
///
/// **Corpus shape (decided 2026-05-31):**
///
/// 1. A *corpus* is identified by a flat string name (e.g. `text_500mb`,
///    `rust_crate_mt32`) listed in [`Self::required_results`]. The list
///    is **the schema** — every release-time `BenchRun` must produce at
///    least one `nornir_bench::BenchResult` with each of these names.
///    Missing names → release gate fails *before* regression checks run.
/// 2. Result name format: `<corpus>[_<variant>]`, snake_case, ASCII.
///    Variants are free-form and meaningful only to the repo (e.g.
///    `_st` / `_mt32` for single- vs 32-thread variants in holger).
/// 3. Per-result `metrics` is a free-form JSON map — but cross-repo
///    rollups (Time-Travel viz, regression gate) only auto-discover
///    numeric metrics. Recommended (not enforced) suffix convention:
///      - throughput  → `*_mbs` (MiB/s)
///      - latency     → `*_ms`  or `*_us`
///      - ratios      → `*_pct`
///      - counts      → plain noun (`files`, `chunks`)
/// 4. Stability requirement: once a name lands in `required_results`,
///    it MUST keep producing comparable numbers across releases. Renames
///    are schema breaks and require a workspace.toml update.
///
/// **Environmental requirements:**
///
/// - [`Self::network_required`] — set `true` if the corpus pulls real
///   artifacts at test-time (e.g. ljar's maven_artifacts suite hits
///   Maven Central). When true the upcoming funnel `network_probe`
///   node must pass before `cargo_test` / `cargo_bench` run for this
///   repo; in offline mode the pipeline records `status=skipped_offline`
///   for these stages rather than failing them.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct BenchSpec {
    #[serde(default)]
    pub required_results: Vec<String>,
    #[serde(default)]
    pub network_required: bool,
}

impl BenchSpec {
    /// Verify a `BenchRun` produces every required result name. Returns
    /// the sorted list of missing names (empty == ok).
    pub fn missing_in<'a>(&'a self, run: &nornir_bench::BenchRun) -> Vec<&'a str> {
        let produced: std::collections::HashSet<&str> =
            run.results.iter().map(|r| r.name.as_str()).collect();
        let mut missing: Vec<&str> = self
            .required_results
            .iter()
            .map(|s| s.as_str())
            .filter(|n| !produced.contains(n))
            .collect();
        missing.sort();
        missing
    }

    /// Fail with a clear error if any required result is missing.
    pub fn validate(&self, run: &nornir_bench::BenchRun) -> anyhow::Result<()> {
        let missing = self.missing_in(run);
        if !missing.is_empty() {
            anyhow::bail!(
                "bench corpus is missing {} required result(s): [{}]",
                missing.len(),
                missing.join(", ")
            );
        }
        Ok(())
    }
}

/// A loaded config plus the paths it was discovered through.
///
/// `Default` yields an EMPTY config (no repos, empty paths) — used by the CLI in
/// thin-client mode, where every command routes to a remote server and no local
/// `nornir.toml` is required.
#[derive(Debug, Clone, Default)]
pub struct Loaded {
    pub nornir: Nornir,
    pub config_path: PathBuf,
    pub workspace_root: PathBuf,
}

/// Where a warehouse root came from — the provenance of the ONE derivation in
/// [`Loaded::warehouse_root_resolved`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WarehouseOrigin {
    /// `[storage].root_path` named the root outright — no `warehouse/` appended.
    PinnedRoot,
    /// `[storage].local_path` is an absolute path, honored verbatim.
    PinnedAbsolute,
    /// `[storage].local_path` is relative, joined under `workspace_root`.
    PinnedRelative,
    /// No `[storage].local_path`. The root can only be derived from the
    /// workspace NAME, which is read out of the config PATH — an input that
    /// differs between verbs. This branch is what silently moved the warehouse.
    Unpinned { workspace_name: String },
}

/// One warehouse root and the reason it is that path.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WarehouseRoot {
    pub path: PathBuf,
    pub origin: WarehouseOrigin,
}

/// A config that does not pin `[storage].local_path`, refused BY NAME.
///
/// Carries the config it read, the name it would have had to guess from, and
/// the path it would have invented — so the operator is told the whole story in
/// one line instead of discovering it as an empty `history-show`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnpinnedWarehouse {
    pub config_path: PathBuf,
    pub workspace_name: String,
    pub would_be: PathBuf,
}

impl std::fmt::Display for UnpinnedWarehouse {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{} declares no [storage].local_path, so the warehouse root could only be guessed \
             from the workspace name `{}` (which is read out of the config PATH, and two verbs \
             need not discover the same config): it would have been {}. \
             Refusing rather than writing where another verb will not read. \
             Add a [storage] section naming local_path (absolute, or relative to the workspace \
             root), or pass --workspace <name> to say which warehouse you mean.",
            self.config_path.display(),
            self.workspace_name,
            self.would_be.display(),
        )
    }
}

impl std::error::Error for UnpinnedWarehouse {}

impl Loaded {
    /// The canonical workspace name — the `<name>` from the `workspace_<name>`
    /// component of the config path (the same derivation the viz picker uses),
    /// e.g. `…/workspace_matrix/release/nornir.toml` → `matrix`. Falls back to
    /// `default` when no `workspace_<name>` is present (a bare/ad-hoc config).
    pub fn workspace_name(&self) -> String {
        self.config_path
            .components()
            .filter_map(|c| c.as_os_str().to_str())
            .find_map(|s| s.strip_prefix("workspace_").map(str::to_string))
            .filter(|s| !s.is_empty())
            .unwrap_or_else(|| "default".to_string())
    }

    /// **THE** warehouse derivation — the only place in the house where a
    /// `nornir.toml` becomes a warehouse directory. Everything else
    /// ([`Loaded::warehouse_root`], [`Loaded::warehouse_root_checked`]) is a
    /// view of this one answer, so two verbs on one config cannot land on two
    /// roots.
    ///
    /// Three origins, and the third is the defect:
    ///
    /// * [`WarehouseOrigin::PinnedAbsolute`] — an absolute `[storage].local_path`,
    ///   honored verbatim (a config may pin an explicit fast path).
    /// * [`WarehouseOrigin::PinnedRelative`] — a relative `[storage].local_path`,
    ///   joined under `workspace_root`.
    /// * [`WarehouseOrigin::Unpinned`] — no `[storage].local_path` at all. The
    ///   root is then derived from the workspace NAME, and the name is read out
    ///   of the CONFIG PATH — which different verbs discover differently. That
    ///   is how `bench run` wrote one warehouse while `history-show`, in the
    ///   same directory on the same config, read another and still printed
    ///   `N result(s) persisted`. An unpinned config is therefore reported as
    ///   such rather than silently answered; see [`Loaded::warehouse_root_checked`].
    ///
    /// In every case the Iceberg data dir is `warehouse/` under the answer.
    pub fn warehouse_root_resolved(&self) -> WarehouseRoot {
        let storage = &self.nornir.storage;
        if !storage.root_path.is_empty() {
            // The root, said outright. `join` with an absolute path discards
            // `workspace_root`, so an absolute value wins as intended.
            return WarehouseRoot {
                path: self.workspace_root.join(&storage.root_path),
                origin: WarehouseOrigin::PinnedRoot,
            };
        }
        if storage.local_path.is_empty() {
            let workspace_name = self.workspace_name();
            WarehouseRoot {
                path: registry_root().join(&workspace_name).join("builds"),
                origin: WarehouseOrigin::Unpinned { workspace_name },
            }
        } else {
            // `join` with an absolute `local_path` discards `workspace_root`, so
            // an absolute fast path (e.g. `/home/alice/.nornir`) wins as intended.
            let origin = if Path::new(&storage.local_path).is_absolute() {
                WarehouseOrigin::PinnedAbsolute
            } else {
                WarehouseOrigin::PinnedRelative
            };
            WarehouseRoot {
                path: self.workspace_root.join(&storage.local_path).join("warehouse"),
                origin,
            }
        }
    }

    /// Per-workspace Iceberg warehouse root — the path half of
    /// [`Loaded::warehouse_root_resolved`], with no second derivation of its own.
    ///
    /// PER-WORKSPACE layout (decided 2026-06-12): with no `[storage].local_path`
    /// the answer is `<home>/.nornir/workspaces/<name>/builds` —
    /// **byte-for-byte the path the server opens** (`nornir-server` serves, and
    /// `monitor::republish` builds, `<root>/<name>/builds`; `catalog.redb` lives
    /// right there, with Iceberg's own `warehouse/` data dir nested inside). So
    /// fat and server read the *same* warehouse. An explicit `[storage].local_path`
    /// still overrides (absolute path honored verbatim; relative joined under
    /// `workspace_root`).
    ///
    /// ⚠ This accessor answers even for an unpinned config. A verb that
    /// **writes**, or that a reader must later meet, calls
    /// [`Loaded::warehouse_root_checked`] instead.
    pub fn warehouse_root(&self) -> PathBuf {
        self.warehouse_root_resolved().path
    }

    /// The same one answer, but an **unpinned** config is refused BY NAME
    /// instead of answered.
    ///
    /// This is the ruling behind ?41 defect 1: a `nornir.toml` with no
    /// `[storage]` section does not get to decide the warehouse, because the
    /// only thing left to derive it from is the config PATH, and two verbs in
    /// one directory do not have to discover the same config path. Rather than
    /// make the derivation "identical for all verbs" — which cannot be promised
    /// while the input itself varies per verb — the unpinned branch is closed:
    /// the operator names the storage, or nothing is opened. A refusal is
    /// recoverable; a success line over a write nobody can read is not.
    pub fn warehouse_root_checked(&self) -> Result<PathBuf, UnpinnedWarehouse> {
        let resolved = self.warehouse_root_resolved();
        match resolved.origin {
            WarehouseOrigin::PinnedRoot
            | WarehouseOrigin::PinnedAbsolute
            | WarehouseOrigin::PinnedRelative => Ok(resolved.path),
            WarehouseOrigin::Unpinned { workspace_name } => Err(UnpinnedWarehouse {
                config_path: self.config_path.clone(),
                workspace_name,
                would_be: resolved.path,
            }),
        }
    }

    /// Per-workspace funnel store — `<home>/.nornir/workspaces/<name>/funnel`
    /// (a workspace's planning DAG belongs to the workspace, not a global dir).
    pub fn funnel_root(&self) -> PathBuf {
        registry_root().join(self.workspace_name()).join("funnel")
    }
}

impl Nornir {
    pub fn load(path: &Path) -> Result<Self> {
        let text = std::fs::read_to_string(path)
            .with_context(|| format!("read {}", path.display()))?;
        toml::from_str(&text).with_context(|| format!("parse {}", path.display()))
    }

    /// Resolve the repo's filesystem path inside `workspace_root`
    /// (convention: `<workspace_root>/<repo_name>/`).
    pub fn repo_dir(workspace_root: &Path, name: &str) -> PathBuf {
        workspace_root.join(name)
    }

    /// Resolve `name`'s on-disk checkout, honoring an explicit
    /// `[repo.<name>].path` override before falling back to the by-name
    /// convention ([`repo_dir_resolved`]).
    ///
    /// The override is the only way a config can point a repo at a directory
    /// whose name differs from the `[repo.<name>]` key — e.g. rendering
    /// nornir's *own* book from a sibling `nornir-orch` worktree via
    /// `[repo.nornir] path = "nornir-orch"`. Without it, `docs book nornir`
    /// silently resolved to the by-name `<workspace_root>/nornir` checkout (or,
    /// when that was absent, fell through to a bogus path) instead of the
    /// configured one. An **absolute** override is honored verbatim; a
    /// **relative** one is joined under `workspace_root`. Empty (the default)
    /// preserves the prior behavior exactly, so other repos are unaffected.
    pub fn repo_dir_for(&self, workspace_root: &Path, name: &str) -> PathBuf {
        if let Some(repo) = self.repo.get(name) {
            if !repo.path.trim().is_empty() {
                let p = Path::new(repo.path.trim());
                return if p.is_absolute() {
                    p.to_path_buf()
                } else {
                    workspace_root.join(p)
                };
            }
        }
        repo_dir_resolved(workspace_root, name)
    }
}

/// Resolve a repo's on-disk directory inside `workspace_root`, tolerating a
/// case mismatch between the configured `[repo.<name>]` key and the actual
/// directory name (e.g. `[repo.njord]` ↔ a `Njord/` checkout). Tries the exact
/// `<workspace_root>/<name>` first; if that doesn't exist, falls back to a
/// case-insensitive match among the workspace's sub-directories. Returns the
/// exact join when nothing matches, so callers still get a sensible path to
/// report in errors.
pub fn repo_dir_resolved(workspace_root: &Path, name: &str) -> PathBuf {
    let exact = Nornir::repo_dir(workspace_root, name);
    if exact.exists() {
        return exact;
    }
    // An empty `workspace_root` means "relative to the current directory";
    // `read_dir("")` errors, which would silently skip the case-insensitive
    // fallback — scan "." instead so e.g. `[repo.njord]` still resolves to a
    // `Njord/` checkout in the cwd.
    let scan_root: &Path = if workspace_root.as_os_str().is_empty() {
        Path::new(".")
    } else {
        workspace_root
    };
    if let Ok(entries) = std::fs::read_dir(scan_root) {
        for entry in entries.flatten() {
            if entry.file_name().to_string_lossy().eq_ignore_ascii_case(name)
                && entry.path().is_dir()
            {
                // When workspace_root is empty the caller expects a bare
                // relative name (not "./Njord"); otherwise the full join.
                return if workspace_root.as_os_str().is_empty() {
                    PathBuf::from(entry.file_name())
                } else {
                    entry.path()
                };
            }
        }
    }
    exact
}

/// Env knob that pins the constellation **workspace/git root** for
/// [`discover`], independent of the current working directory.
///
/// WHY (task #74 — worktree collision): `discover` otherwise only walks UP from
/// `cwd`, so it resolves config only from a canonical sibling checkout (e.g.
/// `/run/media/$USER/T9/git`). An agent running inside a git **worktree**
/// (`git worktree add …`) has a cwd whose ancestry does NOT contain the
/// constellation's `workspace_*/release/nornir.toml`, so discovery fails and
/// parallel-in-worktree runs break. Exporting
/// `NORNIR_WORKSPACE_ROOT=/run/media/$USER/T9/git` points discovery straight
/// at the real root — no hardcoded `nornir` sibling assumption, works from any
/// cwd. Unset/empty, or set to a dir that holds no config, falls through to the
/// existing walk-up unchanged (L2 additive — nothing regresses).
pub const WORKSPACE_ROOT_ENV: &str = "NORNIR_WORKSPACE_ROOT";

/// The `NORNIR_WORKSPACE_ROOT` override as a path, or `None` when unset/empty.
fn env_workspace_root() -> Option<PathBuf> {
    std::env::var_os(WORKSPACE_ROOT_ENV)
        .filter(|v| !v.is_empty())
        .map(PathBuf::from)
}

/// Try to resolve a `Loaded` config treating `dir` as a single candidate
/// directory — the body of the [`discover`] walk, factored out so the
/// `NORNIR_WORKSPACE_ROOT` override reuses the EXACT same three-way rule the
/// walk-up uses (no twinned resolution logic). `Ok(None)` = "nothing here, keep
/// looking"; `Err` = a config was found but failed to load/parse.
fn probe_dir(cur: &Path) -> Result<Option<Loaded>> {
    // Standard: a `nornir.toml` right here.
    let plain = cur.join("nornir.toml");
    if plain.is_file() {
        return Ok(Some(Loaded {
            nornir: Nornir::load(&plain)?,
            config_path: plain,
            workspace_root: cur.to_path_buf(),
        }));
    }
    // Standing INSIDE a `workspace_*/` dir: prefer its OWN `release/nornir.toml`
    // over the alphabetical sibling scan one level up — so `cd workspace_nordisk &&
    // nornir release …` uses the nordisk catalog, not whichever workspace sorts
    // first. workspace_root is the parent (the git/ root), matching the sibling
    // scan + `load_explicit`, so each `[repo.<name>]` resolves to `<git>/<name>`.
    let here = cur.join("release/nornir.toml");
    if here.is_file() {
        if let Some(ws_root) = cur.parent() {
            return Ok(Some(Loaded {
                nornir: Nornir::load(&here)?,
                config_path: here,
                workspace_root: ws_root.to_path_buf(),
            }));
        }
    }
    // Mono-repo fallback: the first `workspace_*/release/nornir.toml` here.
    if let Ok(rd) = std::fs::read_dir(cur) {
        let mut hits: Vec<PathBuf> = rd
            .flatten()
            .map(|e| e.path())
            .filter(|p| {
                p.file_name()
                    .and_then(|n| n.to_str())
                    .is_some_and(|n| n.starts_with("workspace_"))
                    && p.join("release/nornir.toml").is_file()
            })
            .map(|p| p.join("release/nornir.toml"))
            .collect();
        hits.sort();
        if let Some(candidate) = hits.into_iter().next() {
            let nornir = Nornir::load(&candidate)?;
            return Ok(Some(Loaded {
                nornir,
                config_path: candidate,
                workspace_root: cur.to_path_buf(),
            }));
        }
    }
    Ok(None)
}

/// Discover `nornir.toml` by walking up from `start` (cargo-style): the first
/// ancestor containing a `nornir.toml` wins, and the workspace root is that
/// directory. Fallback for the dev mono-repo: any `workspace_*/release/nornir.toml`
/// under an ancestor (any `workspace_*`, NOT a hardcoded personal name) — there
/// the workspace root is the dir holding `workspace_*`.
///
/// Worktree escape hatch (task #74): if [`WORKSPACE_ROOT_ENV`]
/// (`NORNIR_WORKSPACE_ROOT`) names a directory that holds config, it is tried
/// FIRST — so a git-worktree cwd whose ancestry lacks the constellation still
/// resolves. It sits BELOW an explicit `--config`/`NORNIR_CONFIG` in the CLI
/// precedence (those call [`load_explicit`], not `discover`), and a
/// stale/empty override simply falls through to the walk-up.
pub fn discover(start: &Path) -> Result<Loaded> {
    // L2 additive worktree knob — pin the root regardless of cwd.
    if let Some(root) = env_workspace_root() {
        if let Some(loaded) = probe_dir(&root)? {
            return Ok(loaded);
        }
        // The override named no usable config; don't hard-fail — fall through to
        // the ordinary walk-up so a leftover env var never breaks a run that
        // would otherwise resolve from cwd.
    }
    let mut cur = start
        .canonicalize()
        .unwrap_or_else(|_| start.to_path_buf());
    loop {
        if let Some(loaded) = probe_dir(&cur)? {
            return Ok(loaded);
        }
        if !cur.pop() {
            return Err(anyhow!(
                "no nornir.toml found walking up from {}. Put a nornir.toml at your project \
                 root, pass --config / set NORNIR_CONFIG, set NORNIR_WORKSPACE_ROOT to the \
                 constellation git root (useful from a git worktree), or (MCP/CLI client \
                 mode) set NORNIR_SERVER + NORNIR_SERVER_TOKEN to relay to a remote \
                 nornir-server with no local config.",
                start.display()
            ));
        }
    }
}

/// Load from an explicit config path; workspace root = the two-up
/// ancestor (so `…/workspace_holger/release/nornir.toml` → `…/`).
pub fn load_explicit(config_path: &Path) -> Result<Loaded> {
    let nornir = Nornir::load(config_path)?;
    let workspace_root = config_path
        .parent()
        .and_then(Path::parent)
        .and_then(Path::parent)
        .ok_or_else(|| anyhow!("config path lacks grandparent dirs: {}", config_path.display()))?
        .to_path_buf();
    Ok(Loaded {
        nornir,
        config_path: config_path.to_path_buf(),
        workspace_root,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::{Path, PathBuf};
    use std::sync::Mutex;

    /// `$HOME` is process-global; serialize the HOME-mutating tests so they
    /// can't observe each other's value under the parallel test runner.
    static HOME_LOCK: Mutex<()> = Mutex::new(());

    /// The pure home→root rule: `<home>/.nornir`, identical for ANY user.
    #[test]
    fn home_root_is_home_dot_nornir_for_any_user() {
        // A login user.
        assert_eq!(
            nornir_home_from(Path::new("/home/alice")),
            PathBuf::from("/home/alice/.nornir"),
        );
        // The server's system user — same code, same shape.
        assert_eq!(
            nornir_home_from(Path::new("/home/nornir")),
            PathBuf::from("/home/nornir/.nornir"),
        );
    }

    /// The composed roots (registry + default warehouse) hang off the home root
    /// exactly as documented — assert the full path, not just "didn't panic".
    #[test]
    fn registry_and_warehouse_compose_under_home() {
        let _g = HOME_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        // Drive the running-user resolver through a known HOME so the assertion
        // is deterministic on any host.
        // TODO: Audit that the environment access only happens in single-threaded code.
        unsafe { std::env::set_var("HOME", "/home/bob") };
        assert_eq!(nornir_home(), PathBuf::from("/home/bob/.nornir"));
        assert_eq!(registry_root(), PathBuf::from("/home/bob/.nornir/workspaces"));
        assert_eq!(
            warehouse_default_root(),
            PathBuf::from("/home/bob/.nornir/warehouse"),
        );
    }

    /// An empty `[storage].local_path` falls back to the home-derived warehouse
    /// (NOT the old `workspace_holger/.nornir/warehouse`), while an absolute
    /// `local_path` is still honored verbatim.
    #[test]
    fn warehouse_root_default_is_home_derived() {
        let _g = HOME_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        // TODO: Audit that the environment access only happens in single-threaded code.
        unsafe { std::env::set_var("HOME", "/home/carol") };
        let empty = Loaded {
            nornir: Nornir::default(),
            config_path: PathBuf::from("/ws/workspace_holger/release/nornir.toml"),
            workspace_root: PathBuf::from("/ws"),
        };
        // PER-WORKSPACE default = <home>/.nornir/workspaces/<name>/builds (the exact
        // path the server opens), where <name> is the `workspace_<name>` component.
        assert_eq!(
            empty.warehouse_root(),
            PathBuf::from("/home/carol/.nornir/workspaces/holger/builds"),
        );
        assert_eq!(empty.workspace_name(), "holger");
        assert_eq!(
            empty.funnel_root(),
            PathBuf::from("/home/carol/.nornir/workspaces/holger/funnel"),
        );

        // Absolute pin still wins, joined with `warehouse/`.
        let mut pinned = Loaded {
            nornir: Nornir::default(),
            config_path: PathBuf::from("/ws/workspace_holger/release/nornir.toml"),
            workspace_root: PathBuf::from("/ws"),
        };
        pinned.nornir.storage.local_path = "/fast/disk".into();
        assert_eq!(
            pinned.warehouse_root(),
            PathBuf::from("/fast/disk/warehouse"),
        );
    }

    /// `[repo.<name>].path` redirects a repo to a directory whose name differs
    /// from the key — the seam that lets `docs book nornir` render nornir's own
    /// book from a configured `nornir-orch` worktree. A relative override joins
    /// the workspace root; an absolute override is honored verbatim; an empty
    /// override falls back to the by-name convention (so other repos are
    /// untouched). Inject distinct paths, assert the exact resolved dir.
    #[test]
    fn repo_dir_for_honors_path_override_relative_and_absolute() {
        let ws = Path::new("/ws");
        let mut nornir = Nornir::default();
        // Self-repo override: the `nornir` key points at a `nornir-orch` dir.
        nornir.repo.insert(
            "nornir".into(),
            Repo { path: "nornir-orch".into(), ..Default::default() },
        );
        // Absolute override wins over the join.
        nornir.repo.insert(
            "skade".into(),
            Repo { path: "/abs/skade-checkout".into(), ..Default::default() },
        );
        // No override → by-name convention.
        nornir.repo.insert("znippy".into(), Repo::default());

        assert_eq!(
            nornir.repo_dir_for(ws, "nornir"),
            PathBuf::from("/ws/nornir-orch"),
            "relative override must join the workspace root, not the by-name dir",
        );
        assert_eq!(
            nornir.repo_dir_for(ws, "skade"),
            PathBuf::from("/abs/skade-checkout"),
            "absolute override must be honored verbatim",
        );
        assert_eq!(
            nornir.repo_dir_for(ws, "znippy"),
            PathBuf::from("/ws/znippy"),
            "empty override must fall back to the by-name convention",
        );
        // A repo absent from the table still resolves by-name (no panic).
        assert_eq!(
            nornir.repo_dir_for(ws, "ghost"),
            PathBuf::from("/ws/ghost"),
        );
    }

    // ── the embedder seam ([`pin_nornir_base`]) ─────────────────────────────
    //
    // The GLOBAL setter is set-once per PROCESS, so a unit test can drive it at
    // most once and could never cover the refusal rules. Every rule therefore
    // lives in the pure `decide_pin`, proven independently here; the two
    // integration tests (`tests/pin_base.rs`, `tests/pin_after_read.rs`) each get
    // their own process and prove the GLOBAL actually routes through it.

    /// The happy path AND the shape: a pin yields `<base>/.nornir` — derived by
    /// the same one writer, not a second layout.
    #[test]
    fn decide_pin_accepts_an_absolute_deep_base() {
        let base = Path::new("/home/dwarves/.dwarves");
        assert_eq!(decide_pin(None, false, base).unwrap(), PathBuf::from(base));
        assert_eq!(
            nornir_home_from(&decide_pin(None, false, base).unwrap()),
            PathBuf::from("/home/dwarves/.dwarves/.nornir"),
            "the pinned root must still be <base>/.nornir",
        );
    }

    /// A relative base would resolve against the CWD — and a systemd unit's
    /// `WorkingDirectory=/` makes that the filesystem root.
    #[test]
    fn decide_pin_refuses_a_relative_base() {
        let e = decide_pin(None, false, Path::new(".dwarves")).unwrap_err();
        assert_eq!(e, PinRefused::NotAbsolute { attempted: PathBuf::from(".dwarves") });
        assert!(e.to_string().contains("not an absolute path"), "{e}");
    }

    /// `/` and `/home` would put the data root at `/.nornir` or `/home/.nornir`.
    /// A sweep of either is the catastrophe the depth floor exists to prevent.
    #[test]
    fn decide_pin_refuses_a_shallow_base() {
        for (p, n) in [("/", 0usize), ("/home", 1), ("/var", 1)] {
            let e = decide_pin(None, false, Path::new(p)).unwrap_err();
            assert_eq!(
                e,
                PinRefused::TooShallow { attempted: PathBuf::from(p), components: n },
                "{p} must be refused as too shallow",
            );
        }
        // …and the floor is exactly 2: `/home/dwarves` is accepted.
        assert!(decide_pin(None, false, Path::new("/home/dwarves")).is_ok());
    }

    /// A pin that arrives AFTER the root was read is refused. Accepting it would
    /// move the root out from under whatever already opened it — a process split
    /// across two roots, which is the whole bug.
    #[test]
    fn decide_pin_refuses_a_pin_that_arrives_after_the_root_was_read() {
        let want = Path::new("/home/dwarves/.dwarves");
        assert!(decide_pin(None, false, want).is_ok(), "before any read it is fine");
        let e = decide_pin(None, true, want).unwrap_err();
        assert_eq!(e, PinRefused::AlreadyResolved { attempted: want.to_path_buf() });
        assert!(e.to_string().contains("pin BEFORE anything reads it"), "{e}");
    }

    /// Two DIFFERENT bases is the two-roots bug wearing a function call — refused,
    /// loudly. The SAME base twice is a no-op, so an idempotent caller is safe.
    #[test]
    fn decide_pin_is_idempotent_but_refuses_a_second_different_base() {
        let a = Path::new("/home/dwarves/.dwarves");
        let b = Path::new("/home/dwarves/.nornir-elsewhere");
        assert_eq!(decide_pin(Some(a), true, a).unwrap(), a.to_path_buf(), "same base ⇒ no-op");
        let e = decide_pin(Some(a), false, b).unwrap_err();
        assert_eq!(
            e,
            PinRefused::Conflict { pinned: a.to_path_buf(), attempted: b.to_path_buf() },
        );
        assert!(e.to_string().contains("refusing to re-pin"), "{e}");
    }

    /// `NORNIR_WORKSPACE_ROOT` is process-global; serialize the tests that
    /// mutate it so they can't observe each other under the parallel runner.
    static WS_ROOT_LOCK: Mutex<()> = Mutex::new(());

    /// Task #74 — the worktree escape hatch. A cwd whose ancestry does NOT
    /// contain the constellation (the shape of a git *worktree* checked out
    /// somewhere else) fails plain `discover`; setting `NORNIR_WORKSPACE_ROOT`
    /// to the real git root makes resolution succeed from that non-canonical
    /// cwd — proving the hardcoded sibling layout is no longer required. RED if
    /// the override is dropped: the "worktree" branch reverts to the Err path.
    #[test]
    fn discover_honors_workspace_root_env_from_a_worktree_cwd() {
        let _g = WS_ROOT_LOCK.lock().unwrap_or_else(|e| e.into_inner());

        // A unique sandbox under the OS temp dir — no ancestor holds a config.
        let base = std::env::temp_dir().join(format!(
            "nornir-cfg-t74-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_nanos())
                .unwrap_or(0),
        ));
        // The canonical constellation git root: <base>/git/workspace_probe/release/nornir.toml
        let git_root = base.join("git");
        let cfg = git_root.join("workspace_probe").join("release").join("nornir.toml");
        std::fs::create_dir_all(cfg.parent().unwrap()).unwrap();
        std::fs::write(&cfg, "# minimal nornir.toml for #74 test\n").unwrap();
        // The "worktree" cwd lives OUTSIDE the git root — its ancestry has no config.
        let worktree_cwd = base.join("elsewhere").join("edda-wt-xyz");
        std::fs::create_dir_all(&worktree_cwd).unwrap();

        // 1) Without the knob, discovery from the worktree cwd fails (walk-up
        //    finds nothing) — this is the collision the task describes.
        // TODO: Audit that the environment access only happens in single-threaded code.
        unsafe { std::env::remove_var(WORKSPACE_ROOT_ENV) };
        assert!(
            discover(&worktree_cwd).is_err(),
            "a worktree cwd with no constellation in its ancestry must NOT resolve without the knob",
        );

        // 2) With NORNIR_WORKSPACE_ROOT pinned at the real git root, the SAME
        //    non-canonical cwd resolves to the constellation config.
        // TODO: Audit that the environment access only happens in single-threaded code.
        unsafe { std::env::set_var(WORKSPACE_ROOT_ENV, &git_root) };
        let loaded = discover(&worktree_cwd)
            .expect("NORNIR_WORKSPACE_ROOT must resolve config from a non-canonical cwd");
        assert_eq!(loaded.config_path, cfg, "config_path must be the pinned-root config");
        assert_eq!(
            loaded.workspace_root, git_root,
            "workspace_root must be the pinned git root, not a hardcoded sibling",
        );
        assert_eq!(loaded.workspace_name(), "probe");

        // 3) A stale/empty override falls through to the walk-up (no hard fail),
        //    so it can't break a run that would otherwise resolve from cwd.
        // TODO: Audit that the environment access only happens in single-threaded code.
        unsafe { std::env::set_var(WORKSPACE_ROOT_ENV, base.join("does-not-exist")) };
        assert!(
            discover(&worktree_cwd).is_err(),
            "a non-config override must fall through to the (here failing) walk-up, not resolve spuriously",
        );

        // Cleanup: drop the env var and the sandbox.
        // TODO: Audit that the environment access only happens in single-threaded code.
        unsafe { std::env::remove_var(WORKSPACE_ROOT_ENV) };
        let _ = std::fs::remove_dir_all(&base);
    }
}

/// ?41 defect 1 — the warehouse derivation is ONE function, and the branch that
/// could not promise the same answer to two verbs is closed by name.
#[cfg(test)]
mod one_warehouse_derivation_tests {
    use std::path::PathBuf;

    use super::{registry_root, Loaded, WarehouseOrigin};

    fn pinned_absolute(dir: &str) -> Loaded {
        let mut l = Loaded::default();
        l.workspace_root = PathBuf::from("/home/alice/git/workspace_nordisk");
        l.config_path = PathBuf::from("/home/alice/git/workspace_nordisk/release/nornir.toml");
        l.nornir.storage.local_path = dir.to_string();
        l
    }

    fn unpinned() -> Loaded {
        let mut l = Loaded::default();
        l.workspace_root = PathBuf::from("/home/alice/git/workspace_nordisk");
        l.config_path = PathBuf::from("/home/alice/git/workspace_nordisk/release/nornir.toml");
        l
    }

    /// `warehouse_root()` may never be a SECOND derivation: it is the path half
    /// of the resolved answer, always, for every origin.
    #[test]
    fn warehouse_root_is_the_resolved_path_and_nothing_else() {
        for l in [
            pinned_absolute("/home/alice/fast/nordisk"),
            pinned_absolute("builds"),
            unpinned(),
        ] {
            assert_eq!(l.warehouse_root(), l.warehouse_root_resolved().path);
        }
    }

    /// An absolute `local_path` still wins verbatim, and it is labelled as such.
    #[test]
    fn an_absolute_local_path_is_honored_verbatim() {
        let l = pinned_absolute("/home/alice/fast/nordisk");
        let r = l.warehouse_root_resolved();
        assert_eq!(r.origin, WarehouseOrigin::PinnedAbsolute);
        assert_eq!(r.path, PathBuf::from("/home/alice/fast/nordisk/warehouse"));
        assert_eq!(l.warehouse_root_checked().unwrap(), r.path);
    }

    /// A relative one is joined under the workspace root, and it is a PIN, so it
    /// is answered rather than refused.
    #[test]
    fn a_relative_local_path_is_joined_under_the_workspace_root() {
        let l = pinned_absolute(".nornir");
        let r = l.warehouse_root_resolved();
        assert_eq!(r.origin, WarehouseOrigin::PinnedRelative);
        assert_eq!(
            r.path,
            PathBuf::from("/home/alice/git/workspace_nordisk/.nornir/warehouse")
        );
        assert!(l.warehouse_root_checked().is_ok());
    }

    /// THE DEFECT: with no `[storage]` the only input left is the config PATH.
    /// The checked resolver refuses, and the refusal NAMES the config, the name
    /// it would have guessed, and the path it would have invented.
    #[test]
    fn a_config_without_storage_is_refused_by_name() {
        let l = unpinned();
        let r = l.warehouse_root_resolved();
        assert_eq!(
            r.origin,
            WarehouseOrigin::Unpinned { workspace_name: "nordisk".to_string() }
        );
        let err = l.warehouse_root_checked().expect_err("an unpinned config must be refused");
        assert_eq!(err.config_path, l.config_path);
        assert_eq!(err.workspace_name, "nordisk");
        assert_eq!(err.would_be, registry_root().join("nordisk").join("builds"));
        let said = err.to_string();
        assert!(said.contains("release/nornir.toml"), "{said}");
        assert!(said.contains("[storage].local_path"), "{said}");
    }

    /// And the guess it refuses to make really does move with the config path —
    /// same directory, same storage, two config paths, two warehouses. That is
    /// the whole reason the branch is closed.
    #[test]
    fn the_unpinned_guess_moves_with_the_config_path() {
        let a = unpinned();
        let mut b = unpinned();
        b.config_path = PathBuf::from("/home/alice/git/nornir/nornir.toml");
        assert_ne!(a.warehouse_root(), b.warehouse_root());
        assert!(a.warehouse_root_checked().is_err());
        assert!(b.warehouse_root_checked().is_err());
    }
}

/// `[storage].root_path` — the per-workspace root said OUTRIGHT, which is what
/// makes closing the unpinned branch possible at all (?41 defect 1).
#[cfg(test)]
mod root_path_tests {
    use std::path::PathBuf;

    use super::{Loaded, WarehouseOrigin};

    fn at(root_path: &str, local_path: &str) -> Loaded {
        let mut l = Loaded::default();
        l.workspace_root = PathBuf::from("/home/alice/git");
        l.config_path = PathBuf::from("/home/alice/git/workspace_nordisk/release/nornir.toml");
        l.nornir.storage.root_path = root_path.to_string();
        l.nornir.storage.local_path = local_path.to_string();
        l
    }

    /// The root the SERVER serves — `<home>/.nornir/workspaces/<name>/builds`,
    /// which HOLDS `catalog.redb` beside a nested `warehouse/` — is unreachable
    /// through `local_path`, because the resolver appends `warehouse/` to that.
    /// `root_path` names it verbatim, so the workspace no longer has to leave
    /// `[storage]` out to get it.
    #[test]
    fn root_path_names_the_root_verbatim_with_no_warehouse_segment() {
        let l = at("/home/alice/.nornir/workspaces/nordisk/builds", "");
        let r = l.warehouse_root_resolved();
        assert_eq!(r.origin, WarehouseOrigin::PinnedRoot);
        assert_eq!(
            r.path,
            PathBuf::from("/home/alice/.nornir/workspaces/nordisk/builds"),
            "no `warehouse/` may be appended to a root_path",
        );
        assert_eq!(l.warehouse_root_checked().unwrap(), r.path);
    }

    /// And it is byte-for-byte what the guess used to invent — so adopting it
    /// moves no existing warehouse, it only makes the same answer sayable.
    #[test]
    fn root_path_can_reproduce_the_guess_it_replaces() {
        let mut guessing = Loaded::default();
        guessing.workspace_root = PathBuf::from("/home/alice/git");
        guessing.config_path =
            PathBuf::from("/home/alice/git/workspace_nordisk/release/nornir.toml");
        let guessed = guessing.warehouse_root();
        assert!(guessing.warehouse_root_checked().is_err(), "the guess is refused");

        let pinned = at(&guessed.display().to_string(), "");
        assert_eq!(pinned.warehouse_root_checked().unwrap(), guessed);
    }

    /// A relative `root_path` is joined under the workspace root, still with no
    /// `warehouse/` appended.
    #[test]
    fn a_relative_root_path_is_joined_under_the_workspace_root() {
        let l = at("builds", "");
        assert_eq!(
            l.warehouse_root_checked().unwrap(),
            PathBuf::from("/home/alice/git/builds")
        );
    }

    /// Both set is not ambiguous: `root_path` is the more specific statement and
    /// wins, deterministically, for every verb.
    #[test]
    fn root_path_wins_over_local_path() {
        let l = at("/home/alice/.nornir/workspaces/nordisk/builds", "/somewhere/else");
        assert_eq!(l.warehouse_root_resolved().origin, WarehouseOrigin::PinnedRoot);
        assert_eq!(
            l.warehouse_root_checked().unwrap(),
            PathBuf::from("/home/alice/.nornir/workspaces/nordisk/builds")
        );
    }
}