use std::collections::BTreeSet;
use anyhow::Result;
use chrono::Utc;
use crate::model::{
ReadyTask, StateSnapshot, StateTask, StoreIndex, TaskContinuation, TaskDetail, TaskEvent,
TaskStatus, TaskSummary,
};
use super::{
ListFilter, TaskStore,
helpers::{
compare_summary_for_list, is_due_scheduled_summary, is_pending_dependency_summary,
is_ready_summary, match_label_filter, normalize_labels, normalize_query,
task_matches_query, unmet_dependency_ids,
},
};
struct QueryContext<'a> {
index: &'a StoreIndex,
now: chrono::DateTime<Utc>,
ready_top_level: Vec<TaskSummary>,
}
impl<'a> QueryContext<'a> {
fn new(index: &'a StoreIndex, now: chrono::DateTime<Utc>) -> Self {
let mut ready_top_level = Vec::new();
for task in index.tasks.values() {
if is_ready_summary(task, index, now) {
ready_top_level.push(task.clone());
}
}
ready_top_level.sort_by(compare_summary_for_list);
Self {
index,
now,
ready_top_level,
}
}
fn resolve_continuation(&self, task: &TaskSummary) -> TaskContinuation {
let mut continuation = task.continuation.clone();
if task.status == TaskStatus::Done {
return continuation;
}
if continuation.next_task.is_none() {
let mut ready_dependencies = task
.depends_on
.iter()
.filter_map(|dependency_id| self.index.tasks.get(dependency_id))
.filter(|dependency| is_ready_summary(dependency, self.index, self.now))
.cloned()
.collect::<Vec<_>>();
ready_dependencies.sort_by(compare_summary_for_list);
if let Some(dependency) = ready_dependencies.first() {
continuation.next_task = Some(dependency.id.clone());
} else if let Some(next_task) = self
.ready_top_level
.iter()
.find(|candidate| candidate.id != task.id)
{
continuation.next_task = Some(next_task.id.clone());
}
}
continuation
}
fn build_state_task(&self, task: TaskSummary) -> StateTask {
StateTask {
ready: is_ready_summary(&task, self.index, self.now),
dependency_count: task.depends_on.len(),
next: self.resolve_continuation(&task),
task,
}
}
fn build_state_section(&self, mut tasks: Vec<TaskSummary>, limit: usize) -> Vec<StateTask> {
tasks.sort_by(compare_summary_for_list);
tasks
.into_iter()
.take(limit)
.map(|task| self.build_state_task(task))
.collect()
}
fn resolve_done_handoff_target(
&self,
task: &TaskSummary,
visited: &mut BTreeSet<String>,
) -> Option<TaskSummary> {
if !visited.insert(task.id.clone()) {
return None;
}
let continuation = self.resolve_continuation(task);
for next_id in [continuation.next_task.as_deref()].into_iter().flatten() {
if let Some(candidate) = self.index.tasks.get(next_id) {
if candidate.status != TaskStatus::Done {
return Some(candidate.clone());
}
if let Some(target) = self.resolve_done_handoff_target(candidate, visited) {
return Some(target);
}
}
}
let mut graph_targets = Vec::new();
graph_targets.extend(
self.index
.tasks
.values()
.filter(|candidate| {
candidate.status != TaskStatus::Done
&& candidate.depends_on.iter().any(|id| id == &task.id)
})
.cloned(),
);
graph_targets.sort_by(compare_summary_for_list);
graph_targets.dedup_by(|left, right| left.id == right.id);
graph_targets.into_iter().next()
}
fn build_counts(&self, tasks: &[TaskSummary]) -> crate::model::StateCounts {
let mut counts = crate::model::StateCounts::default();
for task in tasks {
match task.status {
TaskStatus::Todo => counts.todo += 1,
TaskStatus::Active => counts.active += 1,
TaskStatus::Checkpoint => counts.checkpoint += 1,
TaskStatus::Blocked => counts.blocked += 1,
TaskStatus::Review => counts.review += 1,
TaskStatus::Done => counts.done += 1,
}
if is_ready_summary(task, self.index, self.now) {
counts.ready += 1;
}
if is_pending_dependency_summary(task, self.index, self.now) {
counts.pending_dependencies += 1;
}
if task.status == TaskStatus::Done && !self.resolve_continuation(task).is_empty() {
counts.handoff += 1;
}
}
counts
}
}
impl TaskStore {
pub fn list_tasks(&self, filter: &ListFilter) -> Result<Vec<crate::model::TaskSummary>> {
if !self.root.exists() {
return Ok(Vec::new());
}
let index = self.read_index()?;
let now = Utc::now();
let labels = normalize_labels(filter.labels.clone());
let query = normalize_query(filter.query.as_deref());
let mut items = index.tasks.values().cloned().collect::<Vec<_>>();
items.retain(|task| super::helpers::match_status_filter(task, filter));
if filter.ready_only {
items.retain(|task| is_ready_summary(task, &index, now));
}
if !labels.is_empty() {
items.retain(|task| match_label_filter(task, &labels));
}
if let Some(query) = query.as_deref() {
items.retain(|task| task_matches_query(task, query));
}
items.sort_by(compare_summary_for_list);
if let Some(limit) = filter.limit {
items.truncate(limit);
}
Ok(items)
}
pub fn ready_tasks(
&self,
query: Option<String>,
limit: Option<usize>,
) -> Result<Vec<ReadyTask>> {
if !self.root.exists() {
return Ok(Vec::new());
}
let index = self.read_index()?;
let now = Utc::now();
let context = QueryContext::new(&index, now);
let query = normalize_query(query.as_deref());
let mut items = index
.tasks
.values()
.filter(|task| {
is_ready_summary(task, &index, now)
|| (is_due_scheduled_summary(task, now)
&& !unmet_dependency_ids(task, &index).is_empty())
})
.cloned()
.collect::<Vec<_>>();
if let Some(query) = query.as_deref() {
items.retain(|task| task_matches_query(task, query));
}
items.sort_by(compare_summary_for_list);
let mut ready = items
.into_iter()
.map(|task| {
let missing_dependencies = unmet_dependency_ids(&task, &index);
ReadyTask {
ready: missing_dependencies.is_empty(),
dependency_count: task.depends_on.len(),
missing_dependencies,
next: context.resolve_continuation(&task),
task,
}
})
.collect::<Vec<_>>();
if let Some(limit) = limit {
ready.truncate(limit);
}
Ok(ready)
}
pub fn state_snapshot(&self, query: Option<String>, limit: usize) -> Result<StateSnapshot> {
if !self.root.exists() {
return Ok(StateSnapshot::default());
}
let index = self.read_index()?;
let now = Utc::now();
let context = QueryContext::new(&index, now);
let query = normalize_query(query.as_deref());
let tasks = index
.tasks
.values()
.filter(|task| {
query
.as_deref()
.is_none_or(|query| task_matches_query(task, query))
})
.cloned()
.collect::<Vec<_>>();
let counts = context.build_counts(&tasks);
let ready = context.build_state_section(
tasks
.iter()
.filter(|task| is_ready_summary(task, &index, now))
.cloned()
.collect(),
limit,
);
let pending_dependencies = context.build_state_section(
tasks
.iter()
.filter(|task| is_pending_dependency_summary(task, &index, now))
.cloned()
.collect(),
limit,
);
let active = context.build_state_section(
tasks
.iter()
.filter(|task| task.status == TaskStatus::Active)
.cloned()
.collect(),
limit,
);
let blocked = context.build_state_section(
tasks
.iter()
.filter(|task| task.status == TaskStatus::Blocked)
.cloned()
.collect(),
limit,
);
let checkpoint = context.build_state_section(
tasks
.iter()
.filter(|task| task.status == TaskStatus::Checkpoint)
.cloned()
.collect(),
limit,
);
let review = context.build_state_section(
tasks
.iter()
.filter(|task| task.status == TaskStatus::Review)
.cloned()
.collect(),
limit,
);
let handoff = context.build_state_section(
tasks
.iter()
.filter(|task| {
task.status == TaskStatus::Done
&& !context.resolve_continuation(task).is_empty()
})
.cloned()
.collect(),
limit,
);
Ok(StateSnapshot {
counts,
ready,
pending_dependencies,
active,
blocked,
checkpoint,
review,
handoff,
})
}
pub fn next_task(&self, id: &str) -> Result<StateTask> {
let index = self.read_index()?;
let resolved_id = self.resolve_task_reference_in_index(&index, id)?;
let task = index
.tasks
.get(&resolved_id)
.cloned()
.expect("resolved task must exist");
let context = QueryContext::new(&index, Utc::now());
Ok(context.build_state_task(task))
}
pub fn continuation_tasks(&self, limit: usize) -> Result<Vec<StateTask>> {
if !self.root.exists() {
return Ok(Vec::new());
}
let index = self.read_index()?;
let now = Utc::now();
let context = QueryContext::new(&index, now);
let mut items = index
.tasks
.values()
.filter_map(|task| {
if context.resolve_continuation(task).is_empty() {
return None;
}
match task.status {
TaskStatus::Checkpoint => Some(task.clone()),
TaskStatus::Done => {
context.resolve_done_handoff_target(task, &mut BTreeSet::new())
}
_ => None,
}
})
.collect::<Vec<_>>();
items.sort_by(compare_summary_for_list);
items.dedup_by(|left, right| left.id == right.id);
let mut tasks = items
.into_iter()
.map(|task| context.build_state_task(task))
.collect::<Vec<_>>();
tasks.truncate(limit);
Ok(tasks)
}
pub fn task_detail(&self, id: &str) -> Result<TaskDetail> {
let index = self.read_index()?;
let context = QueryContext::new(&index, Utc::now());
let resolved_id = self.resolve_task_reference_in_index(&index, id)?;
let task = self.read_task_by_id(&resolved_id)?;
let mut dependencies = Vec::new();
let mut blocked_by = Vec::new();
for dependency_id in &task.summary.depends_on {
if let Some(summary) = index.tasks.get(dependency_id) {
dependencies.push(summary.clone());
if summary.status != TaskStatus::Done {
blocked_by.push(summary.clone());
}
}
}
dependencies.sort_by(compare_summary_for_list);
blocked_by.sort_by(compare_summary_for_list);
let missing_dependencies = unmet_dependency_ids(&task.summary, &index)
.into_iter()
.filter(|dependency_id| {
!dependencies
.iter()
.any(|summary| &summary.id == dependency_id)
})
.collect();
Ok(TaskDetail {
ready: is_ready_summary(&task.summary, &index, context.now),
next: context.resolve_continuation(&task.summary),
task,
dependencies,
missing_dependencies,
blocked_by,
})
}
pub fn read_events(&self, id: Option<&str>, limit: Option<usize>) -> Result<Vec<TaskEvent>> {
if !self.events_path().exists() {
return Ok(Vec::new());
}
let resolved_id = match id {
Some(task_id) => Some(self.resolve_task_reference(task_id)?),
None => None,
};
let mut events = if let Some(task_id) = resolved_id.as_deref() {
self.ensure_task_event_log(task_id)?;
self.read_events_file(self.task_events_path(task_id))?
} else {
self.read_events_file(self.events_path())?
};
events.sort_by_key(|e| std::cmp::Reverse(e.at));
if let Some(limit) = limit {
events.truncate(limit);
}
Ok(events)
}
}