ralph/queue/operations/edit/
preview.rs1use super::key::TaskEditKey;
15use super::parsing::{
16 cycle_status, normalize_rfc3339_input, parse_list, parse_status, parse_task_agent_override,
17};
18use crate::contracts::{QueueFile, Task, TaskPriority, TaskStatus};
19use crate::queue;
20use crate::queue::ValidationWarning;
21use crate::queue::operations::validate::{ensure_task_id, parse_custom_fields_with_context};
22use anyhow::{Context, Result, anyhow, bail};
23
24#[derive(Debug, Clone)]
28pub struct TaskEditPreview {
29 pub task_id: String,
30 pub field: String,
31 pub old_value: String,
32 pub new_value: String,
33 pub warnings: Vec<ValidationWarning>,
34}
35
36#[allow(clippy::too_many_arguments)]
55pub fn preview_task_edit(
56 queue: &QueueFile,
57 done: Option<&QueueFile>,
58 task_id: &str,
59 key: TaskEditKey,
60 input: &str,
61 now_rfc3339: &str,
62 id_prefix: &str,
63 id_width: usize,
64 max_dependency_depth: u8,
65) -> Result<TaskEditPreview> {
66 let operation = "preview edit";
67 let needle = ensure_task_id(task_id, operation)?;
68
69 let task = queue
71 .tasks
72 .iter()
73 .find(|t| t.id.trim() == needle)
74 .ok_or_else(|| {
75 anyhow!(
76 "{}",
77 crate::error_messages::task_not_found_for_edit("edit preview", needle)
78 )
79 })?;
80
81 let mut preview_task = task.clone();
83 let trimmed = input.trim();
84
85 match key {
87 TaskEditKey::Title => {
88 if trimmed.is_empty() {
89 bail!(
90 "Queue edit preview failed (task_id={}, field=title): title cannot be empty. Provide a non-empty title (e.g., 'Fix login bug').",
91 needle
92 );
93 }
94 preview_task.title = trimmed.to_string();
95 }
96 TaskEditKey::Status => {
97 let next_status = if trimmed.is_empty() {
98 cycle_status(preview_task.status)
99 } else {
100 parse_status(trimmed).with_context(|| {
101 format!(
102 "Queue edit preview failed (task_id={}, field=status)",
103 needle
104 )
105 })?
106 };
107 preview_task.status = next_status;
108 if next_status == TaskStatus::Done || next_status == TaskStatus::Rejected {
109 preview_task.completed_at = Some(now_rfc3339.to_string());
110 } else {
111 preview_task.completed_at = None;
112 }
113 }
114 TaskEditKey::Priority => {
115 preview_task.priority = if trimmed.is_empty() {
116 preview_task.priority.cycle()
117 } else {
118 trimmed.parse::<TaskPriority>().with_context(|| {
119 format!(
120 "Queue edit preview failed (task_id={}, field=priority)",
121 needle
122 )
123 })?
124 };
125 }
126 TaskEditKey::Tags => {
127 preview_task.tags = parse_list(trimmed);
128 }
129 TaskEditKey::Scope => {
130 preview_task.scope = parse_list(trimmed);
131 }
132 TaskEditKey::Evidence => {
133 preview_task.evidence = parse_list(trimmed);
134 }
135 TaskEditKey::Plan => {
136 preview_task.plan = parse_list(trimmed);
137 }
138 TaskEditKey::Notes => {
139 preview_task.notes = parse_list(trimmed);
140 }
141 TaskEditKey::Request => {
142 preview_task.request = if trimmed.is_empty() {
143 None
144 } else {
145 Some(trimmed.to_string())
146 };
147 }
148 TaskEditKey::DependsOn => {
149 preview_task.depends_on = parse_list(trimmed);
150 }
151 TaskEditKey::Blocks => {
152 preview_task.blocks = parse_list(trimmed);
153 }
154 TaskEditKey::RelatesTo => {
155 preview_task.relates_to = parse_list(trimmed);
156 }
157 TaskEditKey::Duplicates => {
158 preview_task.duplicates = if trimmed.is_empty() {
159 None
160 } else {
161 Some(trimmed.to_string())
162 };
163 }
164 TaskEditKey::CustomFields => {
165 preview_task.custom_fields =
166 parse_custom_fields_with_context(needle, trimmed, operation)?;
167 }
168 TaskEditKey::Agent => {
169 preview_task.agent = parse_task_agent_override(trimmed).with_context(|| {
170 format!(
171 "Queue edit preview failed (task_id={}, field=agent)",
172 needle
173 )
174 })?;
175 }
176 TaskEditKey::CreatedAt => {
177 let normalized = normalize_rfc3339_input("created_at", trimmed).with_context(|| {
178 format!(
179 "Queue edit preview failed (task_id={}, field=created_at)",
180 needle
181 )
182 })?;
183 preview_task.created_at = normalized;
184 }
185 TaskEditKey::UpdatedAt => {
186 let normalized = normalize_rfc3339_input("updated_at", trimmed).with_context(|| {
187 format!(
188 "Queue edit preview failed (task_id={}, field=updated_at)",
189 needle
190 )
191 })?;
192 preview_task.updated_at = normalized;
193 }
194 TaskEditKey::CompletedAt => {
195 let normalized =
196 normalize_rfc3339_input("completed_at", trimmed).with_context(|| {
197 format!(
198 "Queue edit preview failed (task_id={}, field=completed_at)",
199 needle
200 )
201 })?;
202 preview_task.completed_at = normalized;
203 }
204 TaskEditKey::StartedAt => {
205 let normalized = normalize_rfc3339_input("started_at", trimmed).with_context(|| {
206 format!(
207 "Queue edit preview failed (task_id={}, field=started_at)",
208 needle
209 )
210 })?;
211 preview_task.started_at = normalized;
212 }
213 TaskEditKey::ScheduledStart => {
214 let normalized =
215 normalize_rfc3339_input("scheduled_start", trimmed).with_context(|| {
216 format!(
217 "Queue edit preview failed (task_id={}, field=scheduled_start)",
218 needle
219 )
220 })?;
221 preview_task.scheduled_start = normalized;
222 }
223 TaskEditKey::EstimatedMinutes => {
224 let minutes = if trimmed.is_empty() {
225 None
226 } else {
227 Some(trimmed.parse::<u32>().with_context(|| {
228 format!(
229 "Queue edit preview failed (task_id={}, field=estimated_minutes): must be a non-negative integer",
230 needle
231 )
232 })?)
233 };
234 preview_task.estimated_minutes = minutes;
235 }
236 TaskEditKey::ActualMinutes => {
237 let minutes = if trimmed.is_empty() {
238 None
239 } else {
240 Some(trimmed.parse::<u32>().with_context(|| {
241 format!(
242 "Queue edit preview failed (task_id={}, field=actual_minutes): must be a non-negative integer",
243 needle
244 )
245 })?)
246 };
247 preview_task.actual_minutes = minutes;
248 }
249 }
250
251 if !matches!(key, TaskEditKey::UpdatedAt) {
253 preview_task.updated_at = Some(now_rfc3339.to_string());
254 }
255
256 let mut preview_queue = queue.clone();
258 if let Some(index) = preview_queue
259 .tasks
260 .iter()
261 .position(|t| t.id.trim() == needle)
262 {
263 preview_queue.tasks[index] = preview_task.clone();
264 }
265
266 let warnings = match queue::validate_queue_set(
267 &preview_queue,
268 done,
269 id_prefix,
270 id_width,
271 max_dependency_depth,
272 ) {
273 Ok(warnings) => warnings,
274 Err(err) => {
275 bail!(
276 "Queue edit preview failed (task_id={}): validation error - {}",
277 needle,
278 err
279 );
280 }
281 };
282
283 let old_value = format_field_value(task, key);
285 let new_value = format_field_value(&preview_task, key);
286
287 Ok(TaskEditPreview {
288 task_id: needle.to_string(),
289 field: key.as_str().to_string(),
290 old_value,
291 new_value,
292 warnings,
293 })
294}
295
296pub(crate) fn format_field_value(task: &Task, key: TaskEditKey) -> String {
301 let sep = match key {
302 TaskEditKey::Evidence | TaskEditKey::Plan | TaskEditKey::Notes => "; ",
303 _ => ", ",
304 };
305 key.format_value(task, sep)
306}