- 1
//! Banned-token gate (docs/design/68-context-engine.md §9): production - 2
//! source under `crates/*/src` must never let a vendor name or a topic word - 3
//! act as a routing key. A `#[cfg(test)]` fixture is allowed to call itself - 4
//! whatever it likes — the scan stops at the first `#[cfg(test)]` marker in - 5
//! each file — and a comment does not count as a hard-coding, so `//` and - 6
//! `///` text is stripped before the remaining lines are checked. - 7
//! - 8
//! The banned set is deliberately just the words this codebase's own history - 9
//! produced: a search vendor's name that once drove tool-name sniffing, and - 10
//! a topic word from the bug report that motivated deleting it. - 11
- 12
use std::path::{Path, PathBuf}; - 13
- 14
const BANNED_TOKENS: &[&str] = &["tavily", "weather", "noida"]; - 15
- 16
/// One line the gate refused, already resolved to a workspace-relative path - 17
/// so the report reads the same regardless of where the suite runs from. - 18
#[derive(Debug, Clone)] - 19
pub struct Offense { - 20
pub path: String, - 21
pub line: usize, - 22
pub text: String, - 23
} - 24
- 25
impl std::fmt::Display for Offense { - 26
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - 27
write!(f, "{}:{}: {}", self.path, self.line, self.text.trim()) - 28
} - 29
} - 30
- 31
fn workspace_root() -> Result<PathBuf, String> { - 32
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - 33
manifest_dir - 34
.parent() - 35
.and_then(Path::parent) - 36
.map(Path::to_path_buf) - 37
.ok_or_else(|| { - 38
format!( - 39
"{} has no workspace root two levels up", - 40
manifest_dir.display() - 41
) - 42
}) - 43
} - 44
- 45
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> { - 46
let entries = std::fs::read_dir(dir).map_err(|error| format!("{}: {error}", dir.display()))?; - 47
for entry in entries { - 48
let entry = entry.map_err(|error| format!("{}: {error}", dir.display()))?; - 49
let path = entry.path(); - 50
let file_type = entry - 51
.file_type() - 52
.map_err(|error| format!("{}: {error}", path.display()))?; - 53
if file_type.is_dir() { - 54
collect_rs_files(&path, out)?; - 55
} else if path.extension().is_some_and(|ext| ext == "rs") { - 56
out.push(path); - 57
} - 58
} - 59
Ok(()) - 60
} - 61
- 62
/// Every `.rs` file under `crates/*/src`, in the workspace named by `root`. - 63
fn all_src_files(root: &Path) -> Result<Vec<PathBuf>, String> { - 64
let mut out = Vec::new(); - 65
let crates_dir = root.join("crates"); - 66
let entries = std::fs::read_dir(&crates_dir) - 67
.map_err(|error| format!("{}: {error}", crates_dir.display()))?; - 68
for entry in entries { - 69
let entry = entry.map_err(|error| format!("{}: {error}", crates_dir.display()))?; - 70
let src = entry.path().join("src"); - 71
if src.is_dir() { - 72
collect_rs_files(&src, &mut out)?; - 73
} - 74
} - 75
out.sort(); - 76
Ok(out) - 77
} - 78
- 79
/// Truncates a line at its first `//` that is not inside a string literal, - 80
/// which covers `///` and `//!` doc comments too (both start with `//`). - 81
/// Block comments (`/* */`) do not appear in this codebase's `.rs` sources - 82
/// today, so they are deliberately not handled here. - 83
fn strip_line_comment(line: &str) -> &str { - 84
let bytes = line.as_bytes(); - 85
let mut quote: Option<u8> = None; - 86
let mut i = 0; - 87
while i < bytes.len() { - 88
let byte = bytes[i]; - 89
match quote { - 90
Some(q) => { - 91
if byte == b'\\' { - 92
i += 1; - 93
} else if byte == q { - 94
quote = None; - 95
} - 96
} - 97
None => { - 98
if byte == b'"' || byte == b'\'' { - 99
quote = Some(byte); - 100
} else if byte == b'/' && bytes.get(i + 1) == Some(&b'/') { - 101
return &line[..i]; - 102
} - 103
} - 104
} - 105
i += 1; - 106
} - 107
line - 108
} - 109
- 110
/// `(1-based line number, comment-stripped text)` for every line before the - 111
/// file's first `#[cfg(test)]` marker. - 112
fn production_lines(text: &str) -> Vec<(usize, String)> { - 113
let mut out = Vec::new(); - 114
for (index, raw_line) in text.lines().enumerate() { - 115
if raw_line.contains("#[cfg(test)]") { - 116
break; - 117
} - 118
out.push((index + 1, strip_line_comment(raw_line).to_string())); - 119
} - 120
out - 121
} - 122
- 123
/// Small, explicit exceptions — vocabulary that is presentation vocabulary - 124
/// or provider-registry data, never a vendor/topic routing key: - 125
/// - 126
/// * `vak-llm/src/registry.rs` — verified to contain none of the banned - 127
/// words; allow-listed anyway per the invariant this gate implements. - 128
/// * `docs/` — out of the walked tree today (only `crates/*/src` is - 129
/// scanned), kept here so the rule still reads correctly if the walk ever - 130
/// widens. - 131
/// * `vak-core/src/presentation_tools.rs`'s `semantic_types` arrays — the - 132
/// `weather` semantic type is a card shape the runtime still emits - 133
/// (`emit_metric_card`), not a vendor name. - 134
/// * `vak-delivery/src/skills.rs`'s registry `provides` list and built-in - 135
/// recipe id mirror that same `weather` semantic type on the receiving - 136
/// side; the recipe id is consumed by `vak-agent`'s tests (outside this - 137
/// agent's scope to rename in the same change). - 138
fn is_allowed(rel_path: &str, line: &str) -> bool { - 139
if rel_path.starts_with("docs/") { - 140
return true; - 141
} - 142
if rel_path == "crates/vak-llm/src/registry.rs" { - 143
return true; - 144
} - 145
if rel_path == "crates/vak-core/src/presentation_tools.rs" && line.contains("semantic_types") { - 146
return true; - 147
} - 148
if rel_path == "crates/vak-delivery/src/skills.rs" { - 149
let trimmed = line.trim().trim_end_matches(','); - 150
if trimmed == "\"weather\"" || trimmed == "\"weather.forecast\"" { - 151
return true; - 152
} - 153
} - 154
false - 155
} - 156
- 157
pub fn scan_banned_tokens() -> Result<Vec<Offense>, String> { - 158
let root = workspace_root()?; - 159
let mut offenses = Vec::new(); - 160
for path in all_src_files(&root)? { - 161
let rel = path - 162
.strip_prefix(&root) - 163
.unwrap_or(&path) - 164
.to_string_lossy() - 165
.replace('\\', "/"); - 166
let text = std::fs::read_to_string(&path) - 167
.map_err(|error| format!("{}: {error}", path.display()))?; - 168
for (line, stripped) in production_lines(&text) { - 169
let lower = stripped.to_ascii_lowercase(); - 170
if BANNED_TOKENS.iter().any(|token| lower.contains(token)) - 171
&& !is_allowed(&rel, &stripped) - 172
{ - 173
offenses.push(Offense { - 174
path: rel.clone(), - 175
line, - 176
text: stripped, - 177
}); - 178
} - 179
} - 180
} - 181
Ok(offenses) - 182
} - 183
- 184
#[cfg(test)] - 185
mod tests { - 186
#![allow(clippy::unwrap_used, clippy::expect_used)] - 187
use super::*; - 188
- 189
#[test] - 190
fn no_vendor_or_topic_routing_keys_in_production_code() { - 191
let offenses = scan_banned_tokens().expect("scan crates/*/src"); - 192
assert!( - 193
offenses.is_empty(), - 194
"banned tokens found outside tests/comments/allow-list:\n{}", - 195
offenses - 196
.iter() - 197
.map(Offense::to_string) - 198
.collect::<Vec<_>>() - 199
.join("\n") - 200
); - 201
} - 202
} - 203
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.