ralph/queue/operations/mutation/
helpers.rs1use crate::contracts::{QueueFile, TaskStatus};
18use std::collections::HashSet;
19
20pub fn suggest_new_task_insert_index(queue: &QueueFile) -> usize {
25 match queue.tasks.first() {
26 Some(first_task) if matches!(first_task.status, TaskStatus::Doing) => 1,
27 _ => 0,
28 }
29}
30
31pub fn reposition_new_tasks(queue: &mut QueueFile, new_task_ids: &[String], insert_at: usize) {
39 if new_task_ids.is_empty() || queue.tasks.is_empty() {
40 return;
41 }
42
43 let insert_at = insert_at.min(queue.tasks.len());
44 let new_task_set: HashSet<String> = new_task_ids.iter().cloned().collect();
45
46 let mut new_tasks = Vec::new();
47 let mut retained_tasks = Vec::new();
48
49 for task in queue.tasks.drain(..) {
50 if new_task_set.contains(&task.id) {
51 new_tasks.push(task);
52 } else {
53 retained_tasks.push(task);
54 }
55 }
56
57 let split_index = insert_at.min(retained_tasks.len());
58 let mut before_split = Vec::new();
59 let mut after_split = retained_tasks;
60 for task in after_split.drain(..split_index) {
61 before_split.push(task);
62 }
63
64 queue.tasks = before_split
65 .into_iter()
66 .chain(new_tasks)
67 .chain(after_split)
68 .collect();
69}
70
71pub fn added_tasks(before: &HashSet<String>, after: &QueueFile) -> Vec<(String, String)> {
72 let mut added = Vec::new();
73 for task in &after.tasks {
74 let id = task.id.trim();
75 if id.is_empty() || before.contains(id) {
76 continue;
77 }
78 added.push((id.to_string(), task.title.trim().to_string()));
79 }
80 added
81}
82
83pub fn backfill_missing_fields(
84 queue: &mut QueueFile,
85 new_task_ids: &[String],
86 default_request: &str,
87 now_utc: &str,
88) {
89 let now = now_utc.trim();
90 if now.is_empty() || new_task_ids.is_empty() || queue.tasks.is_empty() {
91 return;
92 }
93
94 let new_task_set: HashSet<&str> = new_task_ids.iter().map(|id| id.as_str()).collect();
95 for task in queue.tasks.iter_mut() {
96 if !new_task_set.contains(task.id.trim()) {
97 continue;
98 }
99
100 if task.request.as_ref().is_none_or(|r| r.trim().is_empty()) {
101 let req = default_request.trim();
102 if !req.is_empty() {
103 task.request = Some(req.to_string());
104 }
105 }
106
107 if task.created_at.as_ref().is_none_or(|t| t.trim().is_empty()) {
108 task.created_at = Some(now.to_string());
109 }
110
111 if task.updated_at.as_ref().is_none_or(|t| t.trim().is_empty()) {
112 task.updated_at = Some(now.to_string());
113 }
114 }
115}
116
117pub fn backfill_terminal_completed_at(queue: &mut QueueFile, now_utc: &str) -> usize {
121 let now = now_utc.trim();
122 if now.is_empty() {
123 return 0;
124 }
125
126 let mut updated = 0;
127 for task in queue.tasks.iter_mut() {
128 if !matches!(task.status, TaskStatus::Done | TaskStatus::Rejected) {
129 continue;
130 }
131
132 if task
133 .completed_at
134 .as_ref()
135 .is_none_or(|t| t.trim().is_empty())
136 {
137 task.completed_at = Some(now.to_string());
138 updated += 1;
139 }
140 }
141
142 updated
143}
144
145pub fn sort_tasks_by_priority(queue: &mut QueueFile, descending: bool) {
146 queue.tasks.sort_by(|a, b| {
147 let ord = if descending {
148 a.priority.cmp(&b.priority).reverse()
149 } else {
150 a.priority.cmp(&b.priority)
151 };
152 match ord {
153 std::cmp::Ordering::Equal => a.id.cmp(&b.id),
154 other => other,
155 }
156 });
157}
158
159pub fn task_id_set(queue: &QueueFile) -> HashSet<String> {
160 let mut set = HashSet::new();
161 for task in &queue.tasks {
162 let id = task.id.trim();
163 if id.is_empty() {
164 continue;
165 }
166 set.insert(id.to_string());
167 }
168 set
169}
170
171pub(crate) fn distribute_plan_items(plan: &[String], num_children: usize) -> Vec<Vec<String>> {
173 let mut distribution: Vec<Vec<String>> = vec![Vec::new(); num_children];
174
175 for (i, item) in plan.iter().enumerate() {
176 distribution[i % num_children].push(item.clone());
177 }
178
179 distribution
180}