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 ")
);
}