use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub(crate) 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
}
}
}
pub(crate) 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)
}
pub(crate) 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),
}
pub(crate) 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,
)
}