agentic_time/
query_engine.rs1use chrono::{DateTime, Duration as ChronoDuration, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::deadline::{Deadline, DeadlineStatus};
7use crate::decay::DecayModel;
8use crate::duration::{DurationEstimate, DurationSource};
9use crate::error::TimeResult;
10use crate::file_format::{EntityType, TimeFile};
11use crate::schedule::Schedule;
12use crate::sequence::{Sequence, SequenceStatus, StepStatus};
13use crate::TemporalId;
14
15pub struct QueryEngine<'a> {
17 file: &'a TimeFile,
18}
19
20impl<'a> QueryEngine<'a> {
21 pub fn new(file: &'a TimeFile) -> Self {
23 Self { file }
24 }
25
26 pub fn overdue_deadlines(&self) -> TimeResult<Vec<Deadline>> {
30 let now = Utc::now();
31 let mut results = Vec::new();
32
33 for block in self.file.list_by_type(EntityType::Deadline) {
34 let deadline: Deadline = block.deserialize()?;
35 if deadline.due_at < now
36 && deadline.status != DeadlineStatus::Completed
37 && deadline.status != DeadlineStatus::CompletedLate
38 && deadline.status != DeadlineStatus::Cancelled
39 {
40 results.push(deadline);
41 }
42 }
43
44 results.sort_by_key(|d| d.due_at);
45 Ok(results)
46 }
47
48 pub fn deadlines_due_within(&self, duration: ChronoDuration) -> TimeResult<Vec<Deadline>> {
50 let now = Utc::now();
51 let cutoff = now + duration;
52 let mut results = Vec::new();
53
54 for block in self.file.list_by_type(EntityType::Deadline) {
55 let deadline: Deadline = block.deserialize()?;
56 if deadline.due_at > now
57 && deadline.due_at <= cutoff
58 && deadline.status == DeadlineStatus::Pending
59 {
60 results.push(deadline);
61 }
62 }
63
64 results.sort_by_key(|d| d.due_at);
65 Ok(results)
66 }
67
68 pub fn deadlines_by_status(&self, status: DeadlineStatus) -> TimeResult<Vec<Deadline>> {
70 let mut results = Vec::new();
71
72 for block in self.file.list_by_type(EntityType::Deadline) {
73 let deadline: Deadline = block.deserialize()?;
74 if deadline.status == status {
75 results.push(deadline);
76 }
77 }
78
79 Ok(results)
80 }
81
82 pub fn schedules_in_range(
86 &self,
87 start: DateTime<Utc>,
88 end: DateTime<Utc>,
89 ) -> TimeResult<Vec<Schedule>> {
90 let mut results = Vec::new();
91
92 for block in self.file.list_by_type(EntityType::Schedule) {
93 let schedule: Schedule = block.deserialize()?;
94 let schedule_end = schedule.end_at();
95
96 if schedule.start_at < end && schedule_end > start {
97 results.push(schedule);
98 }
99 }
100
101 results.sort_by_key(|s| s.start_at);
102 Ok(results)
103 }
104
105 pub fn schedule_conflicts(&self, schedule: &Schedule) -> TimeResult<Vec<Schedule>> {
107 let mut conflicts = Vec::new();
108
109 for block in self.file.list_by_type(EntityType::Schedule) {
110 let existing: Schedule = block.deserialize()?;
111 if existing.id != schedule.id && schedule.conflicts_with(&existing) {
112 conflicts.push(existing);
113 }
114 }
115
116 Ok(conflicts)
117 }
118
119 pub fn available_slots(
121 &self,
122 start: DateTime<Utc>,
123 end: DateTime<Utc>,
124 min_duration: ChronoDuration,
125 ) -> TimeResult<Vec<TimeSlot>> {
126 let scheduled = self.schedules_in_range(start, end)?;
127 let mut slots = Vec::new();
128 let mut current = start;
129
130 for schedule in scheduled {
131 if schedule.start_at > current {
132 let gap = schedule.start_at - current;
133 if gap >= min_duration {
134 slots.push(TimeSlot {
135 start: current,
136 end: schedule.start_at,
137 duration_secs: gap.num_seconds(),
138 });
139 }
140 }
141 current = schedule.end_at().max(current);
142 }
143
144 if current < end {
145 let gap = end - current;
146 if gap >= min_duration {
147 slots.push(TimeSlot {
148 start: current,
149 end,
150 duration_secs: gap.num_seconds(),
151 });
152 }
153 }
154
155 Ok(slots)
156 }
157
158 pub fn sequence(&self, id: &TemporalId) -> TimeResult<Option<Sequence>> {
162 self.file.get(id)
163 }
164
165 pub fn active_sequences(&self) -> TimeResult<Vec<Sequence>> {
167 let mut results = Vec::new();
168
169 for block in self.file.list_by_type(EntityType::Sequence) {
170 let sequence: Sequence = block.deserialize()?;
171 if sequence.status == SequenceStatus::InProgress {
172 results.push(sequence);
173 }
174 }
175
176 Ok(results)
177 }
178
179 pub fn blocked_sequences(&self) -> TimeResult<Vec<(Sequence, Vec<TemporalId>)>> {
181 let mut results = Vec::new();
182
183 for block in self.file.list_by_type(EntityType::Sequence) {
184 let sequence: Sequence = block.deserialize()?;
185 let blocked: Vec<_> = sequence
186 .steps
187 .iter()
188 .filter(|s| s.status == StepStatus::Pending)
189 .flat_map(|s| &s.depends_on)
190 .filter(|&dep_order| {
191 sequence
192 .steps
193 .iter()
194 .find(|s| s.order == *dep_order)
195 .map(|s| s.status != StepStatus::Completed)
196 .unwrap_or(false)
197 })
198 .map(|_| sequence.id)
199 .collect();
200
201 if !blocked.is_empty() {
202 results.push((sequence, blocked));
203 }
204 }
205
206 Ok(results)
207 }
208
209 pub fn decays_below_threshold(&self, threshold: f64) -> TimeResult<Vec<DecayModel>> {
213 let mut results = Vec::new();
214 let now = Utc::now();
215
216 for block in self.file.list_by_type(EntityType::Decay) {
217 let decay: DecayModel = block.deserialize()?;
218 let current_value = decay.calculate_value(now);
219 if current_value < threshold {
220 results.push(decay);
221 }
222 }
223
224 Ok(results)
225 }
226
227 pub fn decays_reaching_threshold(
229 &self,
230 threshold: f64,
231 within: ChronoDuration,
232 ) -> TimeResult<Vec<(DecayModel, ChronoDuration)>> {
233 let mut results = Vec::new();
234
235 for block in self.file.list_by_type(EntityType::Decay) {
236 let decay: DecayModel = block.deserialize()?;
237 if let Some(time_until) = decay.time_until(threshold) {
238 if time_until <= within {
239 results.push((decay, time_until));
240 }
241 }
242 }
243
244 results.sort_by_key(|(_, d)| *d);
245 Ok(results)
246 }
247
248 pub fn estimate_total(&self, ids: &[TemporalId]) -> TimeResult<DurationEstimate> {
252 let mut optimistic = 0i64;
253 let mut expected = 0i64;
254 let mut pessimistic = 0i64;
255
256 for id in ids {
257 if let Some(duration) = self.file.get::<DurationEstimate>(id)? {
258 optimistic += duration.optimistic_secs;
259 expected += duration.expected_secs;
260 pessimistic += duration.pessimistic_secs;
261 }
262 }
263
264 Ok(DurationEstimate {
265 id: TemporalId::new(),
266 label: "Combined estimate".to_string(),
267 optimistic_secs: optimistic,
268 expected_secs: expected,
269 pessimistic_secs: pessimistic,
270 confidence: 0.5,
271 source: DurationSource::Predicted {
272 model: "aggregation".to_string(),
273 confidence: 0.5,
274 },
275 created_at: Utc::now(),
276 tags: vec![],
277 })
278 }
279
280 pub fn stats(&self) -> TimeResult<TimeStats> {
284 let mut stats = TimeStats::default();
285
286 for _block in self.file.list_by_type(EntityType::Duration) {
287 stats.duration_count += 1;
288 }
289
290 for block in self.file.list_by_type(EntityType::Deadline) {
291 let deadline: Deadline = block.deserialize()?;
292 stats.deadline_count += 1;
293 match deadline.status {
294 DeadlineStatus::Overdue => stats.overdue_count += 1,
295 DeadlineStatus::Completed | DeadlineStatus::CompletedLate => {
296 stats.completed_count += 1
297 }
298 DeadlineStatus::Warning => stats.warning_count += 1,
299 _ => stats.pending_count += 1,
300 }
301 }
302
303 for _block in self.file.list_by_type(EntityType::Schedule) {
304 stats.schedule_count += 1;
305 }
306
307 for _block in self.file.list_by_type(EntityType::Sequence) {
308 stats.sequence_count += 1;
309 }
310
311 for _block in self.file.list_by_type(EntityType::Decay) {
312 stats.decay_count += 1;
313 }
314
315 Ok(stats)
316 }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct TimeSlot {
322 pub start: DateTime<Utc>,
324 pub end: DateTime<Utc>,
326 pub duration_secs: i64,
328}
329
330#[derive(Debug, Default, Serialize, Deserialize)]
332pub struct TimeStats {
333 pub duration_count: usize,
335 pub deadline_count: usize,
337 pub schedule_count: usize,
339 pub sequence_count: usize,
341 pub decay_count: usize,
343 pub overdue_count: usize,
345 pub warning_count: usize,
347 pub pending_count: usize,
349 pub completed_count: usize,
351}