nyx/tests/dynamic_layering.rs
2026-06-05 10:16:30 -05:00

110 lines
3.9 KiB
Rust

//! Layering boundary test: ensures the dynamic module is only referenced from
//! the allowed crossing points in the static codebase.
//!
//! The dynamic module is feature-gated (`--features dynamic`). Call sites
//! outside the allowed set create an implicit dependency on the feature flag
//! that the static-analysis path must never have. This test fails fast when
//! new code accidentally reaches into `crate::dynamic` from a module that
//! should remain feature-agnostic.
//!
//! # Allowed crossings
//!
//! | File | Reason |
//! |------------------------------|-------------------------------------------|
//! | `src/main.rs` | binary entry point; wires --features dynamic|
//! | `src/lib.rs` | crate root; `#[cfg(feature="dynamic")]` mod|
//! | `src/commands/scan.rs` | enrichment loop lives here |
//! | `src/commands/mod.rs` | `verify-feedback` subcommand |
//! | `src/server/` (any file) | server start_scan verify wiring |
//! | `src/rank.rs` | dynamic-verdict rank scoring |
//! | `src/chain/reverify.rs` | composite chain re-verification |
use std::fs;
use std::path::{Path, PathBuf};
/// Files/prefixes that are allowed to reference `crate::dynamic` (or
/// `dynamic::`) directly. Paths are relative to `src/` (no leading `src/`).
const ALLOWED: &[&str] = &[
"main.rs",
"lib.rs",
"commands/scan.rs",
"commands/mod.rs",
"server/",
"rank.rs",
// Composite chain re-verification is the public bridge between the chain
// composer and the dynamic verifier.
"chain/reverify.rs",
// The dynamic module itself is obviously allowed.
"dynamic/",
];
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_rs_files(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
}
fn is_allowed(path: &Path, src_root: &Path) -> bool {
let rel = path
.strip_prefix(src_root)
.unwrap_or(path)
.to_string_lossy();
ALLOWED
.iter()
.any(|allowed| rel.starts_with(allowed) || rel.as_ref() == *allowed)
}
#[test]
fn dynamic_module_only_referenced_from_allowed_files() {
let src_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
collect_rs_files(&src_root, &mut files);
let mut violations: Vec<String> = Vec::new();
for path in &files {
if is_allowed(path, &src_root) {
continue;
}
let content = fs::read_to_string(path).unwrap_or_default();
// Look for any reference to the dynamic module.
// Exclude `// dynamic` style comments and doc strings.
for (lineno, line) in content.lines().enumerate() {
let trimmed = line.trim();
// Skip comment lines.
if trimmed.starts_with("//") || trimmed.starts_with("*") {
continue;
}
if trimmed.contains("crate::dynamic")
|| trimmed.contains("dynamic::")
|| trimmed.contains("use crate::dynamic")
{
let rel = path
.strip_prefix(&src_root)
.unwrap_or(path)
.display()
.to_string();
violations.push(format!("{}:{}: {}", rel, lineno + 1, trimmed));
}
}
}
if !violations.is_empty() {
panic!(
"Files outside allowed crossings reference `crate::dynamic`:\n{}\n\
Add the file to ALLOWED in tests/dynamic_layering.rs if the \
reference is intentional.",
violations.join("\n")
);
}
}