pe-tasks 0.1.0

Task management for Potential Expectations — structured work items, dependency DAG, lifecycle hooks, and agent tools
Documentation
//! # TaskRegistry trait — async CRUD + queries + dependencies.
//!
//! The registry is the storage backend for tasks. The library provides
//! [`InMemoryTaskRegistry`](crate::in_memory::InMemoryTaskRegistry);
//! users implement this trait for persistent backends (SurrealDB, Postgres, etc.).

use pe_core::PeError;

use crate::dependency::TaskDependency;
use crate::task::{Task, TaskId, TaskPriority, TaskStatus, TaskType};

/// Async task storage with CRUD, queries, and dependency management.
///
/// All operations are async and return `Result<T, PeError>`.
/// Implementations must be `Send + Sync` for use across async tasks.
#[async_trait::async_trait]
pub trait TaskRegistry: Send + Sync {
    // === CRUD ===

    /// Create a new task. Returns the created task with generated ID.
    async fn create(&self, task: Task) -> Result<Task, PeError>;

    /// Get a task by ID. Returns None if not found or deleted.
    async fn get(&self, id: &TaskId) -> Result<Option<Task>, PeError>;

    /// Update an existing task. Returns the updated task.
    async fn update(&self, task: &Task) -> Result<Task, PeError>;

    /// Soft-delete a task (sets deleted_at). Returns true if found.
    async fn delete(&self, id: &TaskId) -> Result<bool, PeError>;

    /// Restore a soft-deleted task. Returns true if found in trash.
    async fn restore(&self, id: &TaskId) -> Result<bool, PeError>;

    // === Status ===

    /// Update task status (with lifecycle validation).
    async fn update_status(
        &self,
        id: &TaskId,
        status: TaskStatus,
        result: Option<serde_json::Value>,
        error: Option<String>,
    ) -> Result<Task, PeError>;

    // === Queries ===

    /// List tasks matching a filter.
    async fn list(&self, filter: &TaskFilter) -> Result<Vec<Task>, PeError>;

    /// Get direct subtasks of a parent.
    async fn get_subtasks(&self, parent_id: &TaskId) -> Result<Vec<Task>, PeError>;

    /// Get the full subtask tree (recursive) rooted at a task.
    async fn get_tree(&self, root_id: &TaskId) -> Result<Vec<Task>, PeError>;

    // === Dependencies ===

    /// Add a dependency edge. Rejects if it would create a cycle.
    async fn add_dependency(&self, dep: TaskDependency) -> Result<(), PeError>;

    /// Remove a dependency edge.
    async fn remove_dependency(
        &self,
        task_id: &TaskId,
        depends_on_id: &TaskId,
    ) -> Result<(), PeError>;

    /// Get all dependencies of a task (what it depends on).
    async fn get_dependencies(&self, task_id: &TaskId) -> Result<Vec<TaskDependency>, PeError>;

    /// Get all dependents of a task (what depends on it).
    async fn get_dependents(&self, task_id: &TaskId) -> Result<Vec<TaskDependency>, PeError>;

    /// Get all tasks that are ready to execute (no incomplete Blocks deps).
    async fn get_ready_tasks(&self) -> Result<Vec<Task>, PeError>;
}

/// Filter for listing tasks.
///
/// All fields are optional — `None` means "don't filter on this field".
///
/// # Example
///
/// ```
/// use pe_tasks::registry::TaskFilter;
/// use pe_tasks::TaskStatus;
///
/// let filter = TaskFilter::default().with_status(TaskStatus::Pending).with_limit(10);
/// ```
#[derive(Clone, Debug, Default)]
pub struct TaskFilter {
    pub status: Option<TaskStatus>,
    pub task_type: Option<TaskType>,
    pub priority: Option<TaskPriority>,
    pub agent_id: Option<String>,
    pub assignee: Option<String>,
    pub parent_id: Option<TaskId>,
    pub tag: Option<String>,
    pub include_deleted: bool,
    pub limit: usize,
}

impl TaskFilter {
    /// Builder: filter by status.
    #[must_use]
    pub fn with_status(mut self, status: TaskStatus) -> Self {
        self.status = Some(status);
        self
    }

    /// Builder: filter by task type.
    #[must_use]
    pub fn with_task_type(mut self, tt: TaskType) -> Self {
        self.task_type = Some(tt);
        self
    }

    /// Builder: filter by priority.
    #[must_use]
    pub fn with_priority(mut self, priority: TaskPriority) -> Self {
        self.priority = Some(priority);
        self
    }

    /// Builder: filter by agent.
    #[must_use]
    pub fn with_agent(mut self, agent_id: impl Into<String>) -> Self {
        self.agent_id = Some(agent_id.into());
        self
    }

    /// Builder: filter by assignee.
    #[must_use]
    pub fn with_assignee(mut self, assignee: impl Into<String>) -> Self {
        self.assignee = Some(assignee.into());
        self
    }

    /// Builder: filter by parent.
    #[must_use]
    pub fn with_parent(mut self, parent_id: impl Into<TaskId>) -> Self {
        self.parent_id = Some(parent_id.into());
        self
    }

    /// Builder: filter by tag.
    #[must_use]
    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
        self.tag = Some(tag.into());
        self
    }

    /// Builder: set result limit.
    #[must_use]
    pub fn with_limit(mut self, limit: usize) -> Self {
        self.limit = limit;
        self
    }

    /// Builder: include deleted tasks.
    #[must_use]
    pub fn including_deleted(mut self) -> Self {
        self.include_deleted = true;
        self
    }
}