Skip to main content

ralph/queue/operations/batch/
plan.rs

1//! Batch plan manipulation operations.
2//!
3//! Responsibilities:
4//! - Batch append plan items to multiple tasks.
5//! - Batch prepend plan items to multiple tasks.
6//!
7//! Does not handle:
8//! - Task creation or deletion.
9//! - Task field updates other than plan and updated_at.
10//! - Plan item distribution during task split (see generate.rs).
11//!
12//! Assumptions/invariants:
13//! - Both append and prepend update the task's updated_at timestamp.
14//! - Empty plan items lists are rejected before processing.
15
16use crate::contracts::QueueFile;
17use anyhow::{Result, bail};
18
19use super::{
20    BatchOperationResult, BatchResultCollector, preprocess_batch_ids, validate_task_ids_exist,
21};
22
23/// Batch append plan items to multiple tasks.
24///
25/// # Arguments
26/// * `queue` - The queue file to modify
27/// * `task_ids` - List of task IDs to update
28/// * `plan_items` - Plan items to append
29/// * `now_rfc3339` - Current timestamp for updated_at
30/// * `continue_on_error` - If true, continue processing on individual failures
31///
32/// # Returns
33/// A `BatchOperationResult` with details of successes and failures.
34pub fn batch_plan_append(
35    queue: &mut QueueFile,
36    task_ids: &[String],
37    plan_items: &[String],
38    now_rfc3339: &str,
39    continue_on_error: bool,
40) -> Result<BatchOperationResult> {
41    let unique_ids = preprocess_batch_ids(task_ids, "plan append")?;
42
43    if plan_items.is_empty() {
44        bail!("No plan items provided for batch plan append");
45    }
46
47    // In atomic mode, validate all IDs exist first
48    if !continue_on_error {
49        validate_task_ids_exist(queue, &unique_ids)?;
50    }
51
52    let mut collector =
53        BatchResultCollector::new(unique_ids.len(), continue_on_error, "plan append");
54
55    for task_id in &unique_ids {
56        match queue.tasks.iter_mut().find(|t| t.id == *task_id) {
57            Some(task) => {
58                task.plan.extend(plan_items.iter().cloned());
59                task.updated_at = Some(now_rfc3339.to_string());
60                collector.record_success(task_id.clone(), Vec::new());
61            }
62            None => {
63                collector.record_failure(
64                    task_id.clone(),
65                    crate::error_messages::task_not_found_batch_failure(task_id),
66                )?;
67            }
68        }
69    }
70
71    Ok(collector.finish())
72}
73
74/// Batch prepend plan items to multiple tasks.
75///
76/// # Arguments
77/// * `queue` - The queue file to modify
78/// * `task_ids` - List of task IDs to update
79/// * `plan_items` - Plan items to prepend
80/// * `now_rfc3339` - Current timestamp for updated_at
81/// * `continue_on_error` - If true, continue processing on individual failures
82///
83/// # Returns
84/// A `BatchOperationResult` with details of successes and failures.
85pub fn batch_plan_prepend(
86    queue: &mut QueueFile,
87    task_ids: &[String],
88    plan_items: &[String],
89    now_rfc3339: &str,
90    continue_on_error: bool,
91) -> Result<BatchOperationResult> {
92    let unique_ids = preprocess_batch_ids(task_ids, "plan prepend")?;
93
94    if plan_items.is_empty() {
95        bail!("No plan items provided for batch plan prepend");
96    }
97
98    // In atomic mode, validate all IDs exist first
99    if !continue_on_error {
100        validate_task_ids_exist(queue, &unique_ids)?;
101    }
102
103    let mut collector =
104        BatchResultCollector::new(unique_ids.len(), continue_on_error, "plan prepend");
105
106    for task_id in &unique_ids {
107        match queue.tasks.iter_mut().find(|t| t.id == *task_id) {
108            Some(task) => {
109                // Prepend items: new items first, then existing
110                let mut new_plan = plan_items.to_vec();
111                new_plan.append(&mut task.plan);
112                task.plan = new_plan;
113                task.updated_at = Some(now_rfc3339.to_string());
114
115                collector.record_success(task_id.clone(), Vec::new());
116            }
117            None => {
118                collector.record_failure(
119                    task_id.clone(),
120                    crate::error_messages::task_not_found_batch_failure(task_id),
121                )?;
122            }
123        }
124    }
125
126    Ok(collector.finish())
127}