Skip to main content

agentic_time/
query_engine.rs

1//! Query engine — temporal queries over the `.atime` graph.
2
3use 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
15/// Query engine for temporal data.
16pub struct QueryEngine<'a> {
17    file: &'a TimeFile,
18}
19
20impl<'a> QueryEngine<'a> {
21    /// Create a new query engine over a TimeFile.
22    pub fn new(file: &'a TimeFile) -> Self {
23        Self { file }
24    }
25
26    // --- Deadline queries ---
27
28    /// Get all overdue deadlines.
29    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    /// Get deadlines due within a given duration from now.
49    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    /// Get deadlines by status.
69    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    // --- Schedule queries ---
83
84    /// Get schedules in a time range.
85    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    /// Get conflicting schedules for a proposed schedule.
106    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    /// Get available time slots in a range.
120    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    // --- Sequence queries ---
159
160    /// Get a sequence by ID.
161    pub fn sequence(&self, id: &TemporalId) -> TimeResult<Option<Sequence>> {
162        self.file.get(id)
163    }
164
165    /// Get all active sequences.
166    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    /// Get blocked sequences (steps waiting on dependencies).
180    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    // --- Decay queries ---
210
211    /// Get decay models below a threshold.
212    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    /// Get decays that will reach threshold within a given duration.
228    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    // --- Duration queries ---
249
250    /// Estimate total duration for a set of tasks.
251    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    // --- Stats ---
281
282    /// Get temporal statistics.
283    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/// A free time slot.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct TimeSlot {
322    /// Slot start.
323    pub start: DateTime<Utc>,
324    /// Slot end.
325    pub end: DateTime<Utc>,
326    /// Duration in seconds.
327    pub duration_secs: i64,
328}
329
330/// Temporal statistics.
331#[derive(Debug, Default, Serialize, Deserialize)]
332pub struct TimeStats {
333    /// Number of duration estimates.
334    pub duration_count: usize,
335    /// Number of deadlines.
336    pub deadline_count: usize,
337    /// Number of schedules.
338    pub schedule_count: usize,
339    /// Number of sequences.
340    pub sequence_count: usize,
341    /// Number of decay models.
342    pub decay_count: usize,
343    /// Number of overdue deadlines.
344    pub overdue_count: usize,
345    /// Number of warning-state deadlines.
346    pub warning_count: usize,
347    /// Number of pending deadlines.
348    pub pending_count: usize,
349    /// Number of completed deadlines.
350    pub completed_count: usize,
351}