use std::collections::{BTreeSet, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
fn binding_src() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../shells")
.join("degenbot-python")
.join("src")
}
fn umbrella_lib() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("lib.rs")
}
const INTENTIONALLY_NOT_STANDALONE: &[(&str, &str)] = &[
(
"cli",
"ADR-051 console facade; standalone consumers depend on degenbot-cli directly.",
),
(
"cli_core",
"ADR-051 console-semantics crate; the degenbot-cli facade depends on it directly.",
),
];
fn rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for ent in entries.flatten() {
let p = ent.path();
if p.is_dir() {
rust_files(&p, out);
} else if p.extension().is_some_and(|e| e == "rs") {
out.push(p);
}
}
}
fn consumed_core_crates(src_dir: &Path) -> BTreeSet<String> {
let mut files = Vec::new();
rust_files(src_dir, &mut files);
let mut crates = BTreeSet::new();
for f in files {
let Ok(text) = fs::read_to_string(&f) else {
continue;
};
let stripped: String = text
.lines()
.map(|l| {
if let Some(idx) = l.find("//") {
&l[..idx]
} else {
l
}
})
.collect::<Vec<_>>()
.join("\n");
for m in stripped
.match_indices("degenbot_")
.map(|(i, _)| &stripped[i..])
{
let tail = &m["degenbot_".len()..];
let id: String = tail
.chars()
.take_while(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '_')
.collect();
if id.is_empty() || id == "python" {
continue; }
let after = &tail[id.len()..];
if after.starts_with("::") {
crates.insert(id);
}
}
}
crates
}
fn reexported_core_crates(lib_rs: &Path) -> BTreeSet<String> {
let Ok(text) = fs::read_to_string(lib_rs) else {
return BTreeSet::new();
};
let stripped: String = text
.lines()
.map(|l| {
if let Some(idx) = l.find("//") {
&l[..idx]
} else {
l
}
})
.collect::<Vec<_>>()
.join("\n");
let mut crates = BTreeSet::new();
for m in stripped
.match_indices("pub use degenbot_")
.map(|(i, _)| &stripped[i + "pub use ".len()..])
{
let tail = &m["degenbot_".len()..];
let id: String = tail
.chars()
.take_while(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '_')
.collect();
if !id.is_empty() {
crates.insert(id);
}
}
crates
}
#[test]
fn every_binding_core_crate_is_reachable_from_standalone() {
let consumed = consumed_core_crates(&binding_src());
let reexported = reexported_core_crates(&umbrella_lib());
let allowlist: HashSet<&str> = INTENTIONALLY_NOT_STANDALONE
.iter()
.map(|(c, _)| *c)
.collect();
let mut missing: BTreeSet<&str> = BTreeSet::new();
for c in &consumed {
if reexported.contains(c) {
continue;
}
if allowlist.contains(c.as_str()) {
continue;
}
missing.insert(c.as_str());
}
assert!(
missing.is_empty(),
"ADR-005 standalone reachability violated: the PyO3 binding reaches \
core crate(s) the `degenbot` umbrella does NOT `pub use`, so a \
`cargo add degenbot` standalone consumer cannot reach them. For \
each missing crate either:\n (a) add `pub use degenbot_<crate>;` \
to `rust/crates/facade/degenbot/src/lib.rs` (and the path dep to its \
Cargo.toml), OR\n (b) add it to INTENTIONALLY_NOT_STANDALONE in \
this test with the migration-task / rubric disposition that owns \
the eventual standalone port.\n\nMissing re-exports: {missing:?}\n\n\
Consumed (binding) : {consumed:?}\nRe-exported (umbrella): \
{reexported:?}\nAllowlisted : {:?}",
INTENTIONALLY_NOT_STANDALONE
.iter()
.map(|(c, _)| *c)
.collect::<Vec<_>>(),
);
}
#[test]
fn allowlist_entries_are_still_actually_consumed() {
let consumed = consumed_core_crates(&binding_src());
let reexported = reexported_core_crates(&umbrella_lib());
let mut stale = Vec::new();
let mut contradictory = Vec::new();
for (c, _reason) in INTENTIONALLY_NOT_STANDALONE {
if !consumed.contains(*c) {
stale.push(*c);
}
if reexported.contains(*c) {
contradictory.push(*c);
}
}
assert!(
stale.is_empty() && contradictory.is_empty(),
"stale/redundant allowlist entries (crate no longer consumed, or now \
re-exported — remove from INTENTIONALLY_NOT_STANDALONE):\n stale \
(not consumed): {stale:?}\n contradictory (now re-exported): \
{contradictory:?}",
);
}