use super::{DriftScore, ForgeBrain, KnowledgeCategory};
use crate::core::task::{AgentType, Task};
pub struct RuleBasedBrain;
const NON_USER_FACING_KEYWORDS: &[&str] = &[
"refactor",
"ci",
"pipeline",
"migration",
"schema",
"infrastructure",
"devops",
"dockerfile",
"linter",
"formatter",
"dependency",
"upgrade",
"internal",
"scaffolding",
"boilerplate",
"config",
"tooling",
];
pub fn is_user_facing(title: &str, description: &str) -> bool {
let text = format!("{} {}", title.to_lowercase(), description.to_lowercase());
!NON_USER_FACING_KEYWORDS
.iter()
.any(|kw| contains_word(&text, kw))
}
fn contains_word(text: &str, word: &str) -> bool {
for (i, _) in text.match_indices(word) {
let before_ok = i == 0 || !text.as_bytes()[i - 1].is_ascii_alphanumeric();
let after = i + word.len();
let after_ok = after >= text.len() || !text.as_bytes()[after].is_ascii_alphanumeric();
if before_ok && after_ok {
return true;
}
}
false
}
pub fn generate_uat_criteria(title: &str, acceptance_criteria: &[String]) -> Vec<String> {
let title_lower = title.to_lowercase();
let mut criteria = Vec::new();
if contains_word(&title_lower, "navigation") || contains_word(&title_lower, "menu") {
criteria.push("Can reach target in 3 clicks or fewer".to_string());
}
if contains_word(&title_lower, "button") || contains_word(&title_lower, "form") {
criteria.push("Interactive elements respond within 200ms".to_string());
}
if contains_word(&title_lower, "layout") || contains_word(&title_lower, "responsive") {
criteria.push("Layout correct at 1920x1080 and 1366x768".to_string());
}
if contains_word(&title_lower, "terminal") {
criteria.push("Terminal output readable with no rendering glitches".to_string());
}
if contains_word(&title_lower, "accessibility") {
criteria.push("All interactive elements are keyboard-navigable".to_string());
}
if contains_word(&title_lower, "animation") {
criteria.push("Animations are smooth with no visual jank".to_string());
}
if criteria.is_empty() {
criteria.push("Feature works as expected from user perspective".to_string());
}
for ac in acceptance_criteria {
let lower = ac.to_lowercase();
let is_technical = lower.contains("compile")
|| lower.contains("tests pass")
|| lower.contains("test pass")
|| lower.contains("coverage")
|| lower.contains("lint")
|| lower.contains("no error");
if !is_technical {
criteria.push(format!("Verify: {ac}"));
}
}
criteria
}
pub fn classify_task_type(title: &str, description: &str) -> Option<String> {
let text = format!("{} {}", title.to_lowercase(), description.to_lowercase());
if text.contains("design")
|| text.contains("architect")
|| text.contains("plan")
|| text.contains("schema")
|| text.contains("strategy")
{
return Some("design".into());
}
if text.contains("review")
|| text.contains("audit")
|| text.contains("analyze")
|| text.contains("evaluate")
|| text.contains("inspect")
{
return Some("review".into());
}
if text.contains("test")
|| text.contains("spec")
|| text.contains("coverage")
|| text.contains("assert")
{
return Some("test".into());
}
if text.contains("document")
|| text.contains("readme")
|| text.contains("docs")
|| text.contains("api doc")
|| text.contains("comment")
{
return Some("document".into());
}
if text.contains("implement")
|| text.contains("build")
|| text.contains("create")
|| text.contains("add")
|| text.contains("fix")
|| text.contains("refactor")
|| text.contains("code")
|| text.contains("develop")
{
return Some("implement".into());
}
None
}
impl ForgeBrain for RuleBasedBrain {
fn decompose_plan(
&self,
spec: &str,
_available_tools: &[AgentType],
start_id: u32,
) -> anyhow::Result<Vec<Task>> {
let inventory_files = extract_inventory_files(spec);
let mut tasks = Vec::new();
let mut task_num = start_id;
for line in spec.lines() {
if let Some(header) = line.strip_prefix("## ") {
let title = header.trim();
if title == "Source Files" || title == "Test Files" {
continue;
}
if !inventory_files.is_empty() {
let title_lower = title.to_lowercase().replace([' ', '-'], "_");
let already_exists = inventory_files.iter().any(|f| {
let f_lower = f.to_lowercase();
f_lower.contains(&title_lower)
|| title_lower
.split('_')
.any(|word| word.len() > 3 && f_lower.contains(word))
});
if already_exists {
let id = format!("T-{task_num:03}");
let desc = format!("Review existing implementation of: {title}");
let mut task = Task::new(&id, title, &desc);
task.task_type = Some("review".into());
tasks.push(task);
task_num += 1;
continue;
}
}
let id = format!("T-{task_num:03}");
let desc = format!("Implement: {title}");
let mut task = Task::new(&id, title, &desc);
task.task_type = classify_task_type(title, &desc);
tasks.push(task);
task_num += 1;
}
}
if tasks.is_empty() {
tasks.push(Task::new(
format!("T-{start_id:03}"),
"Implement specification",
spec.chars().take(500).collect::<String>(),
));
}
Ok(tasks)
}
fn assign_task(&self, task: &Task, available_tools: &[AgentType]) -> anyhow::Result<AgentType> {
if available_tools.is_empty() {
return Ok(AgentType::Any);
}
let title_lower = task.title.to_lowercase();
let desc_lower = task.description.to_lowercase();
let text = format!("{title_lower} {desc_lower}");
if text.contains("architect")
|| text.contains("design")
|| text.contains("plan")
|| text.contains("review")
|| text.contains("evaluate")
|| text.contains("decide")
|| text.contains("strategy")
{
if available_tools.contains(&AgentType::Claude) {
return Ok(AgentType::Claude);
}
}
if text.contains("implement")
|| text.contains("code")
|| text.contains("build")
|| text.contains("refactor")
|| text.contains("create")
|| text.contains("add")
|| text.contains("fix")
|| text.contains("develop")
|| text.contains("write code")
{
if available_tools.contains(&AgentType::Codex) {
return Ok(AgentType::Codex);
}
}
if text.contains("test")
|| text.contains("doc")
|| text.contains("document")
|| text.contains("documentation")
|| text.contains("report")
|| text.contains("summary")
|| text.contains("validate")
|| text.contains("verify")
|| text.contains("quality")
|| text.contains("coverage")
{
if available_tools.contains(&AgentType::Gemini) {
return Ok(AgentType::Gemini);
}
}
let task_num: usize = task.id.trim_start_matches("T-").parse().unwrap_or(0);
let idx = task_num % available_tools.len();
Ok(available_tools[idx].clone())
}
fn evaluate_drift(&self, _work_summary: &str, _vision: &str) -> anyhow::Result<DriftScore> {
Ok(DriftScore {
score: 0.0,
explanation: "Drift evaluation requires an LLM brain. Set brain to claude or gemini for intelligent drift detection.".into(),
})
}
fn route_knowledge(&self, content: &str) -> anyhow::Result<KnowledgeCategory> {
let lower = content.to_lowercase();
if lower.contains("research")
|| lower.contains("finding")
|| lower.contains("discovered")
|| lower.contains("learned that")
{
return Ok(KnowledgeCategory::Research);
}
if lower.contains("decided")
|| lower.contains("decision")
|| lower.contains("chose")
|| lower.contains("adr")
{
return Ok(KnowledgeCategory::Decision);
}
if lower.contains("lesson")
|| lower.contains("mistake")
|| lower.contains("bug")
|| lower.contains("fix")
{
return Ok(KnowledgeCategory::Learning);
}
if lower.contains("pattern")
|| lower.contains("convention")
|| lower.contains("best practice")
{
return Ok(KnowledgeCategory::Pattern);
}
Ok(KnowledgeCategory::Unknown)
}
}
fn extract_inventory_files(spec: &str) -> Vec<String> {
let marker = "EXISTING CODEBASE INVENTORY:";
let Some(idx) = spec.find(marker) else {
return Vec::new();
};
let inventory_section = &spec[idx..];
inventory_section
.lines()
.filter_map(|line| {
let trimmed = line.trim().strip_prefix("- ")?;
let path = trimmed.split(' ').next()?;
if path.contains('.') {
Some(path.to_string())
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_assign_architecture_to_claude() {
let brain = RuleBasedBrain;
let task = Task::new("T-001", "Design the API architecture", "...");
let tools = vec![AgentType::Claude, AgentType::Codex];
let result = brain.assign_task(&task, &tools).unwrap();
assert_eq!(result, AgentType::Claude);
}
#[test]
fn test_assign_implementation_to_codex() {
let brain = RuleBasedBrain;
let task = Task::new("T-002", "Implement the login endpoint", "...");
let tools = vec![AgentType::Claude, AgentType::Codex];
let result = brain.assign_task(&task, &tools).unwrap();
assert_eq!(result, AgentType::Codex);
}
#[test]
fn test_route_knowledge_research() {
let brain = RuleBasedBrain;
let result = brain
.route_knowledge("Research finding: Codex CLI supports MCP server mode")
.unwrap();
assert_eq!(result, KnowledgeCategory::Research);
}
#[test]
fn test_route_knowledge_decision() {
let brain = RuleBasedBrain;
let result = brain
.route_knowledge("We decided to use Rust for the orchestrator")
.unwrap();
assert_eq!(result, KnowledgeCategory::Decision);
}
#[test]
fn test_assign_testing_to_gemini() {
let brain = RuleBasedBrain;
let task = Task::new("T-003", "Write unit tests for auth module", "...");
let tools = vec![AgentType::Claude, AgentType::Codex, AgentType::Gemini];
let result = brain.assign_task(&task, &tools).unwrap();
assert_eq!(result, AgentType::Gemini);
}
#[test]
fn test_assign_docs_to_gemini() {
let brain = RuleBasedBrain;
let task = Task::new("T-004", "API documentation update", "Update the API docs");
let tools = vec![AgentType::Claude, AgentType::Codex, AgentType::Gemini];
let result = brain.assign_task(&task, &tools).unwrap();
assert_eq!(result, AgentType::Gemini);
}
#[test]
fn test_fallback_round_robin() {
let brain = RuleBasedBrain;
let task1 = Task::new("T-003", "Setup environment", "Configure dev setup");
let task2 = Task::new("T-004", "Setup environment", "Configure dev setup");
let tools = vec![AgentType::Claude, AgentType::Codex, AgentType::Gemini];
let r1 = brain.assign_task(&task1, &tools).unwrap(); let r2 = brain.assign_task(&task2, &tools).unwrap(); assert_eq!(r1, AgentType::Claude);
assert_eq!(r2, AgentType::Codex);
}
#[test]
fn test_decompose_plan_from_headers() {
let brain = RuleBasedBrain;
let spec = "# My Project\n## Authentication\n## Database\n## API endpoints\n";
let tasks = brain.decompose_plan(spec, &[AgentType::Claude], 1).unwrap();
assert_eq!(tasks.len(), 3);
assert_eq!(tasks[0].title, "Authentication");
assert_eq!(tasks[1].title, "Database");
}
#[test]
fn test_decompose_plan_with_start_id_offset() {
let brain = RuleBasedBrain;
let spec = "## Caching\n## Monitoring\n";
let tasks = brain.decompose_plan(spec, &[AgentType::Claude], 5).unwrap();
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0].id, "T-005");
assert_eq!(tasks[1].id, "T-006");
}
#[test]
fn test_decompose_plan_fallback_uses_start_id() {
let brain = RuleBasedBrain;
let spec = "Just a plain spec with no headers.";
let tasks = brain
.decompose_plan(spec, &[AgentType::Claude], 10)
.unwrap();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].id, "T-010");
}
#[test]
fn test_is_user_facing_positive() {
assert!(is_user_facing(
"Add dashboard sidebar",
"Create navigation component"
));
assert!(is_user_facing("Fix button styles", "Update CSS"));
assert!(is_user_facing(
"Terminal improvements",
"Improve terminal output"
));
assert!(is_user_facing("Create login form", "Authentication form"));
assert!(is_user_facing(
"Layout restructure",
"Update the page layout"
));
}
#[test]
fn test_is_user_facing_negative() {
assert!(!is_user_facing(
"Refactor auth module",
"Clean up internal code"
));
assert!(!is_user_facing("Update CI pipeline", "Fix GitHub Actions"));
assert!(!is_user_facing(
"Dockerfile optimization",
"Reduce image size"
));
assert!(!is_user_facing(
"Dependency upgrade",
"Bump all packages to latest"
));
assert!(!is_user_facing(
"Database migration",
"Add new schema columns"
));
}
#[test]
fn test_is_user_facing_games_and_features() {
assert!(is_user_facing(
"Design High Score Feature",
"Add high score tracking"
));
assert!(is_user_facing(
"Implement Combo Multiplier",
"Combo system for scoring"
));
assert!(is_user_facing(
"Add caching layer",
"Redis integration for faster responses"
));
assert!(is_user_facing(
"Optimize database queries",
"Improve SQL performance"
));
}
#[test]
fn test_uat_criteria_skips_technical() {
let criteria = generate_uat_criteria(
"Add navigation menu",
&[
"Menu items are visible".to_string(),
"All tests pass".to_string(), "Code compiles without errors".to_string(), "Links work correctly".to_string(),
],
);
assert!(criteria.iter().any(|c| c.contains("3 clicks")));
assert!(criteria.iter().any(|c| c.starts_with("Verify: Menu items")));
assert!(criteria.iter().any(|c| c.starts_with("Verify: Links work")));
assert!(!criteria.iter().any(|c| c.contains("tests pass")));
assert!(!criteria.iter().any(|c| c.contains("compiles")));
}
}