intent_engine/db/
models.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4
5#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
6pub struct Task {
7    pub id: i64,
8    pub parent_id: Option<i64>,
9    pub name: String,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub spec: Option<String>,
12    pub status: String,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub complexity: Option<i32>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub priority: Option<i32>,
17    pub first_todo_at: Option<DateTime<Utc>>,
18    pub first_doing_at: Option<DateTime<Utc>>,
19    pub first_done_at: Option<DateTime<Utc>>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct TaskWithEvents {
24    #[serde(flatten)]
25    pub task: Task,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub events_summary: Option<EventsSummary>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct EventsSummary {
32    pub total_count: i64,
33    pub recent_events: Vec<Event>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
37pub struct Event {
38    pub id: i64,
39    pub task_id: i64,
40    pub timestamp: DateTime<Utc>,
41    pub log_type: String,
42    pub discussion_data: String,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
46pub struct WorkspaceState {
47    pub key: String,
48    pub value: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Report {
53    pub summary: ReportSummary,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub tasks: Option<Vec<Task>>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub events: Option<Vec<Event>>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ReportSummary {
62    pub total_tasks: i64,
63    pub tasks_by_status: StatusBreakdown,
64    pub total_events: i64,
65    pub date_range: Option<DateRange>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct StatusBreakdown {
70    pub todo: i64,
71    pub doing: i64,
72    pub done: i64,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct DateRange {
77    pub from: DateTime<Utc>,
78    pub to: DateTime<Utc>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct DoneTaskResponse {
83    pub completed_task: Task,
84    pub workspace_status: WorkspaceStatus,
85    pub next_step_suggestion: NextStepSuggestion,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct WorkspaceStatus {
90    pub current_task_id: Option<i64>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(tag = "type")]
95pub enum NextStepSuggestion {
96    #[serde(rename = "PARENT_IS_READY")]
97    ParentIsReady {
98        message: String,
99        parent_task_id: i64,
100        parent_task_name: String,
101    },
102    #[serde(rename = "SIBLING_TASKS_REMAIN")]
103    SiblingTasksRemain {
104        message: String,
105        parent_task_id: i64,
106        parent_task_name: String,
107        remaining_siblings_count: i64,
108    },
109    #[serde(rename = "TOP_LEVEL_TASK_COMPLETED")]
110    TopLevelTaskCompleted {
111        message: String,
112        completed_task_id: i64,
113        completed_task_name: String,
114    },
115    #[serde(rename = "NO_PARENT_CONTEXT")]
116    NoParentContext {
117        message: String,
118        completed_task_id: i64,
119        completed_task_name: String,
120    },
121    #[serde(rename = "WORKSPACE_IS_CLEAR")]
122    WorkspaceIsClear {
123        message: String,
124        completed_task_id: i64,
125    },
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct TaskSearchResult {
130    #[serde(flatten)]
131    pub task: Task,
132    pub match_snippet: String,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct PickNextResponse {
137    pub suggestion_type: String,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub task: Option<Task>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub reason_code: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub message: Option<String>,
144}
145
146impl PickNextResponse {
147    /// Create a response for focused subtask suggestion
148    pub fn focused_subtask(task: Task) -> Self {
149        Self {
150            suggestion_type: "FOCUSED_SUB_TASK".to_string(),
151            task: Some(task),
152            reason_code: None,
153            message: None,
154        }
155    }
156
157    /// Create a response for top-level task suggestion
158    pub fn top_level_task(task: Task) -> Self {
159        Self {
160            suggestion_type: "TOP_LEVEL_TASK".to_string(),
161            task: Some(task),
162            reason_code: None,
163            message: None,
164        }
165    }
166
167    /// Create a response for no tasks in project
168    pub fn no_tasks_in_project() -> Self {
169        Self {
170            suggestion_type: "NONE".to_string(),
171            task: None,
172            reason_code: Some("NO_TASKS_IN_PROJECT".to_string()),
173            message: Some(
174                "No tasks found in this project. Your intent backlog is empty.".to_string(),
175            ),
176        }
177    }
178
179    /// Create a response for all tasks completed
180    pub fn all_tasks_completed() -> Self {
181        Self {
182            suggestion_type: "NONE".to_string(),
183            task: None,
184            reason_code: Some("ALL_TASKS_COMPLETED".to_string()),
185            message: Some("Project Complete! All intents have been realized.".to_string()),
186        }
187    }
188
189    /// Create a response for no available todos
190    pub fn no_available_todos() -> Self {
191        Self {
192            suggestion_type: "NONE".to_string(),
193            task: None,
194            reason_code: Some("NO_AVAILABLE_TODOS".to_string()),
195            message: Some("No immediate next task found based on the current context.".to_string()),
196        }
197    }
198
199    /// Format response as human-readable text
200    pub fn format_as_text(&self) -> String {
201        match self.suggestion_type.as_str() {
202            "FOCUSED_SUB_TASK" | "TOP_LEVEL_TASK" => {
203                if let Some(task) = &self.task {
204                    format!(
205                        "Based on your current focus, the recommended next task is:\n\n\
206                        [ID: {}] [Priority: {}] [Status: {}]\n\
207                        Name: {}\n\n\
208                        To start working on it, run:\n  ie task start {}",
209                        task.id,
210                        task.priority.unwrap_or(0),
211                        task.status,
212                        task.name,
213                        task.id
214                    )
215                } else {
216                    "[ERROR] Invalid response: task is missing".to_string()
217                }
218            }
219            "NONE" => {
220                let reason_code = self.reason_code.as_deref().unwrap_or("UNKNOWN");
221                let message = self.message.as_deref().unwrap_or("No tasks found");
222
223                match reason_code {
224                    "NO_TASKS_IN_PROJECT" => {
225                        format!(
226                            "[INFO] {}\n\n\
227                            To get started, capture your first high-level intent:\n  \
228                            ie task add --name \"Setup initial project structure\" --priority 1",
229                            message
230                        )
231                    }
232                    "ALL_TASKS_COMPLETED" => {
233                        format!(
234                            "[SUCCESS] {}\n\n\
235                            You can review the accomplishments of the last 30 days with:\n  \
236                            ie report --since 30d",
237                            message
238                        )
239                    }
240                    "NO_AVAILABLE_TODOS" => {
241                        format!(
242                            "[INFO] {}\n\n\
243                            Possible reasons:\n\
244                            - All available 'todo' tasks are part of larger epics that have not been started yet.\n\
245                            - You are not currently focused on a task that has 'todo' sub-tasks.\n\n\
246                            To see all available top-level tasks you can start, run:\n  \
247                            ie task find --parent NULL --status todo",
248                            message
249                        )
250                    }
251                    _ => format!("[INFO] {}", message),
252                }
253            }
254            _ => "[ERROR] Unknown suggestion type".to_string(),
255        }
256    }
257}