use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AntipatternRule {
pub id: String,
pub label: String,
pub rule: String,
pub seed_count: usize,
pub graduated_at: i64,
}
#[must_use]
pub fn active_rules_path() -> PathBuf {
if let Ok(p) = std::env::var("CLAUDETTE_ANTIPATTERNS_FILE") {
if !p.is_empty() {
return PathBuf::from(p);
}
}
let home = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.unwrap_or_else(|_| ".".to_string());
PathBuf::from(home)
.join(".claudette")
.join("antipatterns")
.join("active.toml")
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ActiveRulesFile {
#[serde(default)]
pub rules: Vec<AntipatternRule>,
}
#[must_use]
pub fn load_active_rules() -> Vec<AntipatternRule> {
let path = active_rules_path();
let Ok(text) = std::fs::read_to_string(&path) else {
return Vec::new();
};
match toml::from_str::<ActiveRulesFile>(&text) {
Ok(file) => file.rules,
Err(_) => Vec::new(),
}
}
#[must_use]
pub fn rules_prompt_overlay(rules: &[AntipatternRule]) -> String {
if rules.is_empty() {
return String::new();
}
let mut s = String::from("\n\nLearned rules (do not repeat past failures):\n");
for rule in rules {
s.push_str("- ");
s.push_str(&rule.rule);
s.push('\n');
}
s
}
#[cfg(test)]
mod tests {
use super::*;
fn rule(id: &str, label: &str, body: &str) -> AntipatternRule {
AntipatternRule {
id: id.to_string(),
label: label.to_string(),
rule: body.to_string(),
seed_count: 3,
graduated_at: 0,
}
}
#[test]
fn rules_overlay_empty_when_no_rules() {
let overlay = rules_prompt_overlay(&[]);
assert!(overlay.is_empty());
}
#[test]
fn rules_overlay_lists_each_rule_as_bullet() {
let rules = vec![
rule(
"r1",
"off-by-one",
"Always use `range(1, n+1)` for inclusive sums.",
),
rule(
"r2",
"sql-injection",
"Never concatenate user input into SQL.",
),
];
let overlay = rules_prompt_overlay(&rules);
assert!(overlay.contains("Learned rules"));
assert!(overlay.contains("- Always use `range(1, n+1)`"));
assert!(overlay.contains("- Never concatenate user input"));
}
#[test]
fn active_rules_path_honors_env() {
let _lock = crate::test_env_lock();
let prev = std::env::var("CLAUDETTE_ANTIPATTERNS_FILE").ok();
std::env::set_var("CLAUDETTE_ANTIPATTERNS_FILE", "/tmp/test-antipatterns.toml");
let p = active_rules_path();
match prev {
Some(v) => std::env::set_var("CLAUDETTE_ANTIPATTERNS_FILE", v),
None => std::env::remove_var("CLAUDETTE_ANTIPATTERNS_FILE"),
}
assert!(p.to_string_lossy().contains("test-antipatterns"));
}
#[test]
fn rule_round_trips_through_toml() {
let r = rule("r1", "off-by-one", "use range(1, n+1)");
let s = toml::to_string(&r).unwrap();
let back: AntipatternRule = toml::from_str(&s).unwrap();
assert_eq!(r, back);
}
}