1use super::{
2 ProfileHypothesis, TaskAnalysisInput, TaskAnalyzer, TriageBackendLocal,
3 confidence::{RULES_FALLBACK_MILLI, clamp_milli},
4 profile::{TaskProfileLocal, TaskScopeLocal},
5};
6use crate::core::{
7 adaptive::TaskComplexity,
8 intent_engine::{self, IntentScope, StructuredIntent, TaskType},
9};
10
11#[derive(Debug, Clone, Copy, Default)]
12pub struct RuleTriageBackend;
13
14impl TaskAnalyzer for RuleTriageBackend {
15 fn analyze(&self, input: &TaskAnalysisInput) -> Result<ProfileHypothesis, super::TriageError> {
16 if input.query.trim().is_empty() {
17 return Ok(fallback());
18 }
19 let classification = intent_engine::classify(&input.query);
20 let structured =
21 StructuredIntent::from_query_with_session(&input.query, &input.files_touched);
22 let (complexity, reasoning) = complexity_values(intent_engine::classify_complexity(
23 &input.query,
24 &classification,
25 ));
26 let confidence = milli(structured.confidence);
27 let (scope, scope_base) = scope_values(structured.scope);
28 let profile = TaskProfileLocal {
29 task_class: "coding".into(),
30 intent: intent_name(structured.task_type).into(),
31 complexity: complexity.into(),
32 scope,
33 context_need_milli: clamp_milli(
34 scope_base
35 + input.files_touched.len().min(6) as u16 * 25
36 + u16::from(input.session_context.is_some()) * 50,
37 ),
38 reasoning_need_milli: reasoning,
39 risk_signal_milli: clamp_milli(
40 input.active_diagnostics.saturating_mul(100).min(600) as u16
41 + u16::from(matches!(
42 structured.task_type,
43 TaskType::Config | TaskType::Deploy
44 )) * 200,
45 ),
46 confidence_milli: confidence,
47 };
48 Ok(ProfileHypothesis {
49 profile,
50 confidence_milli: confidence,
51 backend: TriageBackendLocal::Rules,
52 })
53 }
54 fn name(&self) -> &'static str {
55 "rules"
56 }
57}
58
59fn fallback() -> ProfileHypothesis {
60 let profile = TaskProfileLocal {
61 task_class: "coding".into(),
62 intent: "explore".into(),
63 confidence_milli: RULES_FALLBACK_MILLI,
64 ..Default::default()
65 };
66 ProfileHypothesis {
67 profile,
68 confidence_milli: RULES_FALLBACK_MILLI,
69 backend: TriageBackendLocal::Rules,
70 }
71}
72fn milli(value: f64) -> u16 {
73 if value.is_finite() {
74 clamp_milli((value.clamp(0.0, 1.0) * 1000.0).round() as u16)
75 } else {
76 RULES_FALLBACK_MILLI
80 }
81}
82fn intent_name(t: TaskType) -> &'static str {
83 match t {
84 TaskType::FixBug => "coding_fix",
85 _ => t.as_str(),
86 }
87}
88fn complexity_values(c: TaskComplexity) -> (&'static str, u16) {
89 match c {
90 TaskComplexity::Mechanical => ("low", 250),
91 TaskComplexity::Standard => ("medium", 550),
92 TaskComplexity::Architectural => ("high", 850),
93 }
94}
95fn scope_values(s: IntentScope) -> (TaskScopeLocal, u16) {
96 match s {
97 IntentScope::SingleFile => (TaskScopeLocal::SingleFile, 250),
98 IntentScope::MultiFile => (TaskScopeLocal::MultiFile, 500),
99 IntentScope::CrossModule => (TaskScopeLocal::CrossModule, 750),
100 IntentScope::ProjectWide => (TaskScopeLocal::CrossProject, 900),
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 fn intent(query: &str) -> String {
108 RuleTriageBackend
109 .analyze(&TaskAnalysisInput {
110 query: query.into(),
111 ..Default::default()
112 })
113 .unwrap()
114 .profile
115 .intent
116 }
117 #[test]
118 fn maps_all_intents() {
119 for (query, expected) in [
120 ("generate code", "generate"),
121 ("fix bug", "coding_fix"),
122 ("refactor", "refactor"),
123 ("explain", "explore"),
124 ("test", "test"),
125 ("debug", "debug"),
126 ("configure", "config"),
127 ("deploy", "deploy"),
128 ("review", "review"),
129 ] {
130 assert_eq!(intent(query), expected);
131 }
132 }
133 #[test]
134 fn fallback_has_low_confidence() {
135 assert_eq!(
136 RuleTriageBackend
137 .analyze(&TaskAnalysisInput::default())
138 .unwrap()
139 .confidence_milli,
140 RULES_FALLBACK_MILLI
141 );
142 }
143 #[test]
144 fn non_finite_confidence_stays_below_the_actionable_floor() {
145 use crate::core::triage::confidence::ACTIONABLE_FLOOR_MILLI;
146
147 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
148 assert!(
149 milli(value) < ACTIONABLE_FLOOR_MILLI,
150 "a non-finite confidence is an absent signal, not an actionable one"
151 );
152 }
153 }
154}