lean-ctx 3.9.19

Context Runtime for AI Agents with CCP. 79 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
use super::{
    ProfileHypothesis, TaskAnalysisInput, TaskAnalyzer, TriageBackendLocal,
    confidence::clamp_milli,
    profile::{TaskProfileLocal, TaskScopeLocal},
};
use crate::core::{
    adaptive::TaskComplexity,
    intent_engine::{self, IntentScope, StructuredIntent, TaskType},
};

#[derive(Debug, Clone, Copy, Default)]
pub struct RuleTriageBackend;

impl TaskAnalyzer for RuleTriageBackend {
    fn analyze(&self, input: &TaskAnalysisInput) -> Result<ProfileHypothesis, super::TriageError> {
        if input.query.trim().is_empty() {
            return Ok(fallback());
        }
        let classification = intent_engine::classify(&input.query);
        let structured =
            StructuredIntent::from_query_with_session(&input.query, &input.files_touched);
        let (complexity, reasoning) = complexity_values(intent_engine::classify_complexity(
            &input.query,
            &classification,
        ));
        let confidence = milli(structured.confidence);
        let (scope, scope_base) = scope_values(structured.scope);
        let profile = TaskProfileLocal {
            task_class: "coding".into(),
            intent: intent_name(structured.task_type).into(),
            complexity: complexity.into(),
            scope,
            context_need_milli: clamp_milli(
                scope_base
                    + input.files_touched.len().min(6) as u16 * 25
                    + u16::from(input.session_context.is_some()) * 50,
            ),
            reasoning_need_milli: reasoning,
            risk_signal_milli: clamp_milli(
                input.active_diagnostics.saturating_mul(100).min(600) as u16
                    + u16::from(matches!(
                        structured.task_type,
                        TaskType::Config | TaskType::Deploy
                    )) * 200,
            ),
            confidence_milli: confidence,
        };
        Ok(ProfileHypothesis {
            profile,
            confidence_milli: confidence,
            backend: TriageBackendLocal::Rules,
        })
    }
    fn name(&self) -> &'static str {
        "rules"
    }
}

fn fallback() -> ProfileHypothesis {
    let profile = TaskProfileLocal {
        task_class: "coding".into(),
        intent: "explore".into(),
        confidence_milli: 300,
        ..Default::default()
    };
    ProfileHypothesis {
        profile,
        confidence_milli: 300,
        backend: TriageBackendLocal::Rules,
    }
}
fn milli(value: f64) -> u16 {
    if value.is_finite() {
        clamp_milli((value.clamp(0.0, 1.0) * 1000.0).round() as u16)
    } else {
        300
    }
}
fn intent_name(t: TaskType) -> &'static str {
    match t {
        TaskType::FixBug => "coding_fix",
        _ => t.as_str(),
    }
}
fn complexity_values(c: TaskComplexity) -> (&'static str, u16) {
    match c {
        TaskComplexity::Mechanical => ("low", 250),
        TaskComplexity::Standard => ("medium", 550),
        TaskComplexity::Architectural => ("high", 850),
    }
}
fn scope_values(s: IntentScope) -> (TaskScopeLocal, u16) {
    match s {
        IntentScope::SingleFile => (TaskScopeLocal::SingleFile, 250),
        IntentScope::MultiFile => (TaskScopeLocal::MultiFile, 500),
        IntentScope::CrossModule => (TaskScopeLocal::CrossModule, 750),
        IntentScope::ProjectWide => (TaskScopeLocal::CrossProject, 900),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    fn intent(query: &str) -> String {
        RuleTriageBackend
            .analyze(&TaskAnalysisInput {
                query: query.into(),
                ..Default::default()
            })
            .unwrap()
            .profile
            .intent
    }
    #[test]
    fn maps_all_intents() {
        for (query, expected) in [
            ("generate code", "generate"),
            ("fix bug", "coding_fix"),
            ("refactor", "refactor"),
            ("explain", "explore"),
            ("test", "test"),
            ("debug", "debug"),
            ("configure", "config"),
            ("deploy", "deploy"),
            ("review", "review"),
        ] {
            assert_eq!(intent(query), expected);
        }
    }
    #[test]
    fn fallback_has_low_confidence() {
        assert_eq!(
            RuleTriageBackend
                .analyze(&TaskAnalysisInput::default())
                .unwrap()
                .confidence_milli,
            300
        );
    }
}