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

nordisk/knut

crates/knut-graphdb/tests/no_korp_dependency.rs

//! **GATE 6 — the dependency runs one way: korp → knut. `knut → korp` is ZERO.**
//!
//! Order item 8 moved korp's `GraphStore` INTO this crate. That direction is only
//! safe while knut knows nothing about korp: the moment a knut crate names korp, the
//! two repos are a cycle and neither can be published, built or reasoned about alone.
//!
//! So this scans every `Cargo.toml` in the knut workspace for a korp dependency, and
//! every `.rs` file for a `korp` / `korp_ontology` import. It reports the file count
//! and byte total it read, so a scan that reached nothing cannot report clean.
//!
//! **The RED:** add `korp = { path = "../../../korp" }` to any knut manifest, or
//! `use korp_ontology::…` to any knut source, and the verdict names the file.

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

/// The knut workspace root (this crate is `crates/knut-graphdb`).
fn knut_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .expect("crates/<crate> has two ancestors")
        .to_path_buf()
}

fn files_with(root: &Path, ext: &str) -> Vec<PathBuf> {
    let mut out = Vec::new();
    let mut stack = vec![root.to_path_buf()];
    while let Some(d) = stack.pop() {
        let name = d.file_name().and_then(|n| n.to_str()).unwrap_or("");
        // Never walk build output or the vcs dir.
        if matches!(name, "target" | ".git") {
            continue;
        }
        let Ok(rd) = std::fs::read_dir(&d) else { continue };
        for e in rd.flatten() {
            let p = e.path();
            if p.is_dir() {
                stack.push(p);
            } else if p.file_name().and_then(|n| n.to_str()) == Some(ext)
                || p.extension().and_then(|x| x.to_str()) == Some(ext)
            {
                out.push(p);
            }
        }
    }
    out.sort();
    out
}

/// The ONE matcher, shared by the scan and its self-check.
fn korp_reference(line: &str) -> bool {
    let t = line.trim_start();
    if t.starts_with("//") || t.starts_with("#") || t.starts_with("*") {
        return false;
    }
    // A manifest dependency on korp, or a Rust import of a korp crate.
    t.starts_with("korp")
        || t.contains("use korp")
        || t.contains("korp_ontology::")
        || t.contains("extern crate korp")
        || t.contains("path = \"../korp")
        || t.contains("path = \"../../korp")
        || t.contains("path = \"../../../korp")
}

#[test]
fn knut_does_not_depend_on_korp() {
    let root = knut_root();
    let manifests = files_with(&root, "Cargo.toml");
    let sources = files_with(&root, "rs");

    // ── Reach: real files, real bytes, and the known members among them. ──
    assert!(
        manifests.len() >= 10,
        "the scan found only {} manifests under {} — that is not the knut workspace",
        manifests.len(),
        root.display()
    );
    assert!(sources.len() >= 50, "the scan read only {} .rs files", sources.len());

    let mut bytes = 0usize;
    let mut read: Vec<(PathBuf, String)> = Vec::new();
    for p in manifests.iter().chain(sources.iter()) {
        let Ok(t) = std::fs::read_to_string(p) else { continue };
        bytes += t.len();
        read.push((p.clone(), t));
    }
    assert!(bytes > 1_000_000, "the scan read only {bytes} bytes — that is not the real tree");
    let names: Vec<String> = read
        .iter()
        .map(|(p, _)| p.strip_prefix(&root).unwrap_or(p).to_string_lossy().into_owned())
        .collect();
    for must in ["Cargo.toml", "crates/knut-graphdb/src/lib.rs", "crates/knut-graphdb/Cargo.toml"] {
        assert!(names.iter().any(|n| n == must), "the scan did not reach {must}");
    }

    // ── The matcher FIRES. A "no hits" verdict is a real negative. ──
    for planted in [
        "korp = { path = \"../korp\", version = \"0.1.0\" }",
        "use korp_ontology::resolve_for_type;",
        "    korp_ontology::resolve_for_type(label, key)",
    ] {
        assert!(korp_reference(planted), "the matcher is blind to {planted:?}");
    }
    // ...and does NOT fire on prose, which knut's docs are full of.
    for prose in [
        "//! korp is the consumer of this crate.",
        "// korp used to carry a byte-identical copy.",
        "/// `korp` reads this through the seam.",
    ] {
        assert!(!korp_reference(prose), "the matcher would report a false hit on {prose:?}");
    }

    // ── The scan. ──
    // This file is the one legitimate exception: it CONTAINS the vocabulary it hunts
    // for, in the matcher itself. Excluding it by path is honest — the reach checks
    // above already prove the matcher fires, and every other file is still scanned.
    let me = PathBuf::from(file!());
    let mut bad: Vec<String> = Vec::new();
    let mut skipped = 0usize;
    for (p, text) in &read {
        let rel = p.strip_prefix(&root).unwrap_or(p);
        if rel == me {
            skipped += 1;
            continue;
        }
        for (i, line) in text.lines().enumerate() {
            if korp_reference(line) {
                bad.push(format!("{}:{}: {}", rel.display(), i + 1, line.trim()));
            }
        }
    }

    assert_eq!(skipped, 1, "the self-exclusion must skip exactly this file, nothing else");
    eprintln!(
        "knut→korp cycle scan: {} manifests + {} sources, {bytes} bytes, {} hit(s) \
         (1 file self-excluded: the matcher's own source)",
        manifests.len(),
        sources.len(),
        bad.len()
    );

    assert!(
        bad.is_empty(),
        "knut must not depend on korp — the fold moved code korp-ward-to-knut and the \
         direction must stay one-way. {} hit(s):\n  {}",
        bad.len(),
        bad.join("\n  ")
    );
}