use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct BrainVerifyConfig {
#[serde(default)]
required: Vec<RequiredRule>,
#[serde(default)]
contradictions: Vec<ContradictionRule>,
}
#[derive(Debug, Deserialize)]
struct RequiredRule {
file: String,
pattern: String,
#[allow(dead_code)]
why: String,
}
#[derive(Debug, Deserialize)]
struct ContradictionRule {
pattern_a: String,
pattern_b: String,
message: String,
}
fn brain_verify_config() -> Option<BrainVerifyConfig> {
let home = crate::config::profile::resolve_profile_home();
let path = home.join("safety").join("brain_verify.toml");
if !path.exists() {
tracing::debug!("No brain verify TOML at {}", path.display());
return None;
}
match std::fs::read_to_string(&path) {
Ok(content) => match toml::from_str::<BrainVerifyConfig>(&content) {
Ok(cfg) => {
tracing::debug!(
"Loaded brain verify config from {}: {} required rules, {} contradiction checks",
path.display(),
cfg.required.len(),
cfg.contradictions.len()
);
Some(cfg)
}
Err(e) => {
tracing::warn!("Brain verify TOML parse error at {}: {}", path.display(), e);
None
}
},
Err(e) => {
tracing::warn!("Brain verify TOML read error at {}: {}", path.display(), e);
None
}
}
}
fn pattern_matches(pattern: &str, content: &str) -> bool {
let clean = if let Some(rest) = pattern.strip_prefix("(?i)") {
rest
} else {
pattern
};
let content_lower = content.to_lowercase();
let parts: Vec<&str> = clean.split(".*").collect();
if parts.len() == 1 {
return content_lower.contains(&parts[0].to_lowercase());
}
let mut search_from = 0usize;
for part in &parts {
let part_lower = part.to_lowercase();
match content_lower[search_from..].find(&part_lower) {
Some(pos) => search_from += pos + part_lower.len(),
None => return false,
}
}
true
}
pub fn verify_brain_file(file_name: &str, content: &str) -> Vec<String> {
let Some(config) = brain_verify_config() else {
return vec![]; };
verify_brain_file_with_config(file_name, content, &config)
}
fn verify_brain_file_with_config(
file_name: &str,
content: &str,
config: &BrainVerifyConfig,
) -> Vec<String> {
let mut violations = Vec::new();
for rule in &config.required {
if rule.file == file_name && !pattern_matches(&rule.pattern, content) {
violations.push(format!(
"Required rule missing in {}: \"{}\" ({})",
file_name, rule.pattern, rule.why
));
}
}
let entries: Vec<&str> = content.split("\n\n").collect();
for contra in &config.contradictions {
let contradiction_in_entry = entries.iter().any(|entry| {
pattern_matches(&contra.pattern_a, entry) && pattern_matches(&contra.pattern_b, entry)
});
if contradiction_in_entry {
violations.push(format!(
"Contradiction detected in {}: {}",
file_name, contra.message
));
}
}
violations
}
#[derive(Debug)]
pub enum GateDecision {
Allow,
Reject(String),
}
fn orient_gate_decision(
file_name: &str,
proposed_content: &str,
config: Option<&BrainVerifyConfig>,
hard_fail_on_no_config: bool,
) -> GateDecision {
match config {
None => {
if hard_fail_on_no_config {
GateDecision::Reject(
"Brain verification belief base (brain_verify.toml) is not loaded. \
Autonomous writes are blocked until it exists — a gate with no rules \
looks enforced but checks nothing."
.to_string(),
)
} else {
GateDecision::Allow
}
}
Some(cfg) => {
let violations = verify_brain_file_with_config(file_name, proposed_content, cfg);
if violations.is_empty() {
GateDecision::Allow
} else {
GateDecision::Reject(format!(
"Brain file verification failed for {}: {}",
file_name,
violations.join("; ")
))
}
}
}
}
pub fn orient_gate_decision_active(file_name: &str, proposed_content: &str) -> GateDecision {
orient_gate_decision(
file_name,
proposed_content,
brain_verify_config().as_ref(),
true,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config(toml_str: &str) -> BrainVerifyConfig {
toml::from_str(toml_str).expect("test TOML must parse")
}
const AGENTS_TOML: &str = r#"
[[required]]
file = "AGENTS.md"
pattern = "Reports use rich markdown tables"
why = "Report Format Hard Rule"
[[required]]
file = "AGENTS.md"
pattern = "NEVER add.*Co-authored-by"
why = "Commit attribution: user is sole author"
[[required]]
file = "AGENTS.md"
pattern = "NEVER push to main without explicit"
why = "Git push safety"
[[required]]
file = "AGENTS.md"
pattern = "NEVER use.*git revert"
why = "Git revert creates new commits"
[[required]]
file = "AGENTS.md"
pattern = "NEVER start.*draft.*redo a release"
why = "Release safety"
[[required]]
file = "AGENTS.md"
pattern = "NEVER delete.*disable.*cron"
why = "Cron safety"
[[contradictions]]
pattern_a = "(?i)no.*markdown.*table"
pattern_b = "(?i)rich markdown table"
message = "'no markdown tables' vs 'use rich markdown tables'"
[[contradictions]]
pattern_a = "(?i)never.*push"
pattern_b = "(?i)always.*push"
message = "'never push' vs 'always push'"
"#;
#[test]
fn test_simple_substring_match() {
assert!(pattern_matches(
"BAN em-dashes",
"## BAN em-dashes. ZERO TOLERANCE."
));
assert!(!pattern_matches("BAN em-dashes", "No issues here"));
}
#[test]
fn test_ordered_part_match() {
let text = "NEVER start, draft, or redo a release unless explicitly asked.";
assert!(pattern_matches("NEVER start.*draft.*redo a release", text));
assert!(!pattern_matches("draft.*NEVER start", text));
}
#[test]
fn test_case_insensitive() {
let text = "Reports use rich markdown tables for tabular data.";
assert!(pattern_matches("(?i)reports use rich markdown table", text));
assert!(pattern_matches("reports use rich", text));
}
#[test]
fn test_pattern_with_glob() {
let text = "NEVER push to main without explicit user approval.";
assert!(pattern_matches("NEVER push.*without explicit", text));
assert!(!pattern_matches(
"NEVER push.*without approval.*extra",
text
));
}
#[test]
fn test_verify_brain_file_required() {
let config = test_config(AGENTS_TOML);
let content_no_rule = "Some content without the required rule.";
let violations = verify_brain_file_with_config("AGENTS.md", content_no_rule, &config);
assert!(
violations.iter().any(|v| v.contains("Reports use rich")),
"Should detect missing required rule. Violations: {:?}",
violations
);
}
#[test]
fn test_verify_brain_file_no_violations() {
let config = test_config(AGENTS_TOML);
let content = r#"
Reports use rich markdown tables for structured data.
NEVER add Co-authored-by trailers to commits.
NEVER push to main without explicit user approval.
NEVER use git revert.
NEVER start, draft, or redo a release unless explicitly asked.
NEVER delete or disable cron jobs without approval.
"#;
let violations = verify_brain_file_with_config("AGENTS.md", content, &config);
let required_violations: Vec<_> = violations
.iter()
.filter(|v| v.contains("Required rule missing"))
.collect();
assert!(
required_violations.is_empty(),
"Expected no required violations, got: {:?}",
required_violations
);
}
#[test]
fn test_contradiction_detection() {
let config = test_config(AGENTS_TOML);
let content = "No markdown tables. Use rich markdown tables for reports.";
let violations = verify_brain_file_with_config("AGENTS.md", content, &config);
assert!(
violations.iter().any(|v| v.contains("Contradiction")),
"Should detect contradiction in same entry. Violations: {:?}",
violations
);
}
#[test]
fn test_contradiction_scoped_per_entry() {
let config = test_config(AGENTS_TOML);
let content = "NEVER push to main without explicit user approval.\n\nAlways push after tests pass and the user says go.";
let violations = verify_brain_file_with_config("MEMORY.md", content, &config);
let contradictions: Vec<_> = violations
.iter()
.filter(|v| v.contains("Contradiction"))
.collect();
assert!(
contradictions.is_empty(),
"Patterns in separate entries must NOT trigger contradiction. Got: {:?}",
contradictions
);
}
#[test]
fn test_contradiction_same_entry_still_fires() {
let config = test_config(AGENTS_TOML);
let content = "Never push anything. Always push everything.";
let violations = verify_brain_file_with_config("MEMORY.md", content, &config);
assert!(
violations.iter().any(|v| v.contains("Contradiction")),
"Same-entry contradiction must still fire. Violations: {:?}",
violations
);
}
#[test]
fn test_wrong_file_ignored() {
let config = test_config(AGENTS_TOML);
let content = "Some content without any rules.";
let violations = verify_brain_file_with_config("MEMORY.md", content, &config);
let agents_violations: Vec<_> = violations
.iter()
.filter(|v| v.contains("AGENTS.md"))
.collect();
assert!(
agents_violations.is_empty(),
"AGENTS.md rules should not apply to MEMORY.md"
);
}
#[test]
fn orient_gate_clean_content_is_allowed() {
let config = test_config(AGENTS_TOML);
let content = r#"
Reports use rich markdown tables for structured data.
NEVER add Co-authored-by trailers to commits.
NEVER push to main without explicit user approval.
NEVER use git revert.
NEVER start, draft, or redo a release unless explicitly asked.
NEVER delete or disable cron jobs without approval.
"#;
match orient_gate_decision("AGENTS.md", content, Some(&config), true) {
GateDecision::Allow => {}
other => panic!("expected Allow, got {other:?}"),
}
}
#[test]
fn orient_gate_strips_required_rule_is_rejected() {
let config = test_config(AGENTS_TOML);
let content =
"NEVER add Co-authored-by. NEVER push to main without explicit user approval.";
match orient_gate_decision("AGENTS.md", content, Some(&config), true) {
GateDecision::Reject(msg) => assert!(
msg.contains("Required rule missing"),
"should name the missing required rule, got: {msg}"
),
other => panic!("expected Reject, got {other:?}"),
}
}
#[test]
fn orient_gate_introduces_contradiction_is_rejected() {
let config = test_config(AGENTS_TOML);
let content = "Never push anything. Always push everything.";
match orient_gate_decision("MEMORY.md", content, Some(&config), true) {
GateDecision::Reject(msg) => assert!(
msg.contains("Contradiction"),
"should flag the contradiction, got: {msg}"
),
other => panic!("expected Reject, got {other:?}"),
}
}
#[test]
fn orient_gate_hard_fails_when_no_belief_base() {
match orient_gate_decision("AGENTS.md", "anything", None, true) {
GateDecision::Reject(msg) => assert!(
msg.contains("not loaded"),
"should explain the missing belief base, got: {msg}"
),
other => panic!("expected Reject on missing belief base, got {other:?}"),
}
}
#[test]
fn orient_gate_user_path_allows_when_no_belief_base() {
match orient_gate_decision("AGENTS.md", "anything", None, false) {
GateDecision::Allow => {}
other => panic!("expected Allow on user path with no config, got {other:?}"),
}
}
}