lean_ctx/core/ib/
intent.rs1use std::fmt;
7
8use crate::core::session::SessionState;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12pub enum TaskIntent {
13 Debug,
15 Refactor,
17 Implement,
19 Review,
21 Explore,
23 #[default]
25 Unknown,
26}
27
28const INTENT_KEYWORDS: &[(TaskIntent, &[&str])] = &[
29 (
30 TaskIntent::Debug,
31 &["fix", "bug", "error", "debug", "crash", "panic"],
32 ),
33 (
34 TaskIntent::Refactor,
35 &[
36 "refactor",
37 "rename",
38 "move",
39 "extract",
40 "restructure",
41 "clean",
42 ],
43 ),
44 (
45 TaskIntent::Implement,
46 &["implement", "add", "create", "build", "feature", "new"],
47 ),
48 (
49 TaskIntent::Review,
50 &["review", "check", "audit", "verify", "inspect"],
51 ),
52 (
53 TaskIntent::Explore,
54 &["understand", "explore", "analyze", "investigate", "find"],
55 ),
56];
57
58const ERROR_KEYWORDS: &[&str] = &["error", "panic", "crash", "failed", "failure", "bug"];
59
60pub(crate) fn classify_intent(session: &SessionState) -> TaskIntent {
65 if let Some(task) = &session.task {
66 if let Some(intent) = classify_text(task.intent.as_deref().unwrap_or_default()) {
67 return intent;
68 }
69 if let Some(intent) = classify_text(&task.description) {
70 return intent;
71 }
72 }
73
74 if session
75 .findings
76 .iter()
77 .rev()
78 .any(|finding| contains_keyword(&finding.summary, ERROR_KEYWORDS))
79 {
80 return TaskIntent::Debug;
81 }
82
83 TaskIntent::Unknown
84}
85
86fn classify_text(text: &str) -> Option<TaskIntent> {
87 INTENT_KEYWORDS
88 .iter()
89 .find_map(|(intent, keywords)| contains_keyword(text, keywords).then_some(*intent))
90}
91
92fn contains_keyword(text: &str, keywords: &[&str]) -> bool {
93 text.split(|character: char| !character.is_alphanumeric())
94 .filter(|word| !word.is_empty())
95 .any(|word| {
96 keywords
97 .iter()
98 .any(|keyword| word.eq_ignore_ascii_case(keyword))
99 })
100}
101
102impl fmt::Display for TaskIntent {
103 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104 let name = match self {
105 Self::Debug => "debug",
106 Self::Refactor => "refactor",
107 Self::Implement => "implement",
108 Self::Review => "review",
109 Self::Explore => "explore",
110 Self::Unknown => "unknown",
111 };
112 formatter.write_str(name)
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use crate::core::session::{SessionState, TaskInfo};
119
120 use super::{TaskIntent, classify_intent};
121
122 fn session_with_task(description: &str) -> SessionState {
123 let mut session = SessionState::new();
124 session.task = Some(TaskInfo {
125 description: description.to_owned(),
126 intent: None,
127 progress_pct: None,
128 });
129 session
130 }
131
132 #[test]
133 fn classify_debug_from_task() {
134 let session = session_with_task("Fix the parser crash");
135 assert_eq!(classify_intent(&session), TaskIntent::Debug);
136 }
137
138 #[test]
139 fn classify_refactor_from_task() {
140 let session = session_with_task("Refactor the session store");
141 assert_eq!(classify_intent(&session), TaskIntent::Refactor);
142 }
143
144 #[test]
145 fn classify_unknown_when_empty() {
146 assert_eq!(classify_intent(&SessionState::new()), TaskIntent::Unknown);
147 }
148
149 #[test]
150 fn display_format() {
151 assert_eq!(TaskIntent::Implement.to_string(), "implement");
152 }
153}