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

nordisk/nornir

tests/no_dwarves_edge.rs

//! **THE GUARD**: nornir must never regain a crate dependency on `dwarves`.
//!
//! ## The law
//!
//! `workspace_nordisk/prompt` line 66: *a member embeds only what is DOWNSTREAM of
//! it — dwarves embeds nornir+holger+skade; **nornir embeds holger+skade but
//! reaches dwarves ONLY via gRPC (cycle)***. `.nornir/code-placement.md:18` says the
//! same for the forge view specifically: *"nornir embeds the view via gRPC"*.
//!
//! The parenthetical `(cycle)` is the whole reason. dwarves sits DOWNSTREAM of
//! nornir, so dwarves is the member allowed to compile nornir IN for its fat build.
//! The moment nornir names `dwarves` as a dep, `nornir → dwarves → nornir` is a
//! cargo dependency cycle — which cargo rejects for normal deps *regardless of how
//! the features are arranged* — and the fat forge becomes unbuildable.
//!
//! ## What it cost last time
//!
//! nornir carried `dwarves = { path = "../dwarves/crates/dwarves", features =
//! ["release"] }` with **102 `dwarves::` call sites**. Of those:
//!
//! * **67 (66%) were `release_doctor`** — 26,746 lines containing zero
//!   `tonic`/`tokio`/`gix`/`redb`/`skidbladnir` imports. Pure semver and graph
//!   arithmetic. To reach that arithmetic nornir dependency-linked **506 crates**:
//!   the gRPC server, prost, the async runtime, redb, the SSH git transport — all
//!   of it, for semver math. That model is now the `nornir-release-core` edda leaf
//!   both repos consume.
//! * **17 were `brokkr`**, run in-process, where the placement doc says gRPC.
//! * **`dwarves::server::serve` ×3** — nornir was booting the forge's gRPC server
//!   inside its own process.
//!
//! ## Why a test and not a comment
//!
//! The old `Cargo.toml` already carried a "nornir must NEVER gain a crate dep on
//! dwarves" note directly above the dep line that violated it. A comment cannot
//! fail a build. This can.

use std::path::{Path, PathBuf};

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

/// RED when any `[dependencies]` / `[dev-dependencies]` / `[build-dependencies]` /
/// `[target.*.dependencies]` table in nornir's manifest declares `dwarves` (or any
/// crate whose own graph reaches it — `dwarves-embed`, `dwarves-web-client`).
///
/// Deliberately a TEXT scan of the manifest rather than a `cargo metadata` walk: it
/// is fast, needs no network/lock, and catches the dep the instant it is typed —
/// before anyone has to discover it as a cycle error at the far end of the graph.
#[test]
fn nornir_declares_no_dependency_on_the_forge() {
    let manifest =
        std::fs::read_to_string(repo_root().join("Cargo.toml")).expect("read nornir/Cargo.toml");

    // Crates whose presence would (re)create the `nornir → dwarves` edge. The
    // `*-embed` clients count: `dwarves-embed` path-deps `dwarves` itself, so
    // consuming it from here is the same cycle wearing a thin-client hat.
    const FORBIDDEN: [&str; 3] = ["dwarves", "dwarves-embed", "dwarves-web-client"];

    let mut in_dep_table = false;
    let mut offenders: Vec<String> = Vec::new();
    for (lineno, raw) in manifest.lines().enumerate() {
        let line = raw.trim();
        if line.starts_with('[') && !line.starts_with("[[") {
            // Any table whose name ends in `dependencies` declares deps.
            in_dep_table = line
                .trim_end_matches(']')
                .trim_end()
                .ends_with("dependencies");
            continue;
        }
        if !in_dep_table || line.starts_with('#') || line.is_empty() {
            continue;
        }
        // `key = …` / `"key" = …` — take the key only, so a path or comment that
        // merely MENTIONS dwarves (they are allowed to; the prose explains the law)
        // never trips this.
        let Some((key, _)) = line.split_once('=') else {
            continue;
        };
        let key = key.trim().trim_matches('"');
        if FORBIDDEN.contains(&key) {
            offenders.push(format!("Cargo.toml:{}: `{key}`", lineno + 1));
        }
    }

    assert!(
        offenders.is_empty(),
        "nornir has regained a crate dependency on the forge:\n  {}\n\n\
         The law (workspace_nordisk/prompt:66) is: nornir reaches dwarves ONLY via \
         gRPC, never as a crate — because dwarves is DOWNSTREAM and must be able to \
         compile nornir IN (`--features embed-nornir`). This dep makes \
         `nornir -> dwarves -> nornir` a cargo cycle and breaks the fat forge build.\n\n\
         If you need release model/analysis: it is in the `nornir-release-core` edda \
         leaf, which both repos consume.\n\
         If you need the forge to DO something (publish, build, run a pipeline): that \
         is the forge's operation. Let dwarves reach in, or call dwarves-server over \
         gRPC on :7900. Never link it.",
        offenders.join("\n  "),
    );
}

/// RED when any nornir source file names `dwarves::` in real code.
///
/// Prose is allowed — the modules that lost a call site explain *why* they now go
/// over gRPC, and that history is worth keeping — so comment lines are skipped. What
/// is not allowed is a `use dwarves::…` or a `dwarves::foo()` that would resurrect
/// the crate edge.
#[test]
fn no_nornir_source_file_calls_into_the_forge() {
    fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
        let Ok(entries) = std::fs::read_dir(dir) else {
            return;
        };
        for e in entries.flatten() {
            let p = e.path();
            if p.is_dir() {
                walk(&p, out);
            } else if p.extension().is_some_and(|x| x == "rs") {
                out.push(p);
            }
        }
    }
    let src = repo_root().join("src");
    let mut files = Vec::new();
    walk(&src, &mut files);
    assert!(
        !files.is_empty(),
        "found no .rs files under {} — the walk is broken",
        src.display()
    );

    let mut offenders: Vec<String> = Vec::new();
    for f in &files {
        let Ok(text) = std::fs::read_to_string(f) else {
            continue;
        };
        for (lineno, raw) in text.lines().enumerate() {
            let line = raw.trim_start();
            // Skip prose: `//`, `///`, `//!` and block-comment continuations.
            if line.starts_with("//") || line.starts_with('*') {
                continue;
            }
            if line.contains("dwarves::") || line.contains("::dwarves::") {
                offenders.push(format!(
                    "{}:{}: {}",
                    f.strip_prefix(repo_root()).unwrap_or(f).display(),
                    lineno + 1,
                    line.trim(),
                ));
            }
        }
    }

    assert!(
        offenders.is_empty(),
        "nornir source calls into the forge in real code (comments are fine, these are not):\n  {}\n\n\
         nornir reaches dwarves ONLY via gRPC (workspace_nordisk/prompt:66). The release \
         read model lives in the `nornir-release-core` edda leaf; anything constructive \
         is the forge's to perform.",
        offenders.join("\n  "),
    );
}