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::Description => {
97 preview_task.description = if trimmed.is_empty() {
98 None
99 } else {
100 Some(trimmed.to_string())
101 };
102 }
103 TaskEditKey::Status => {
104 let next_status = if trimmed.is_empty() {
105 cycle_status(preview_task.status)
106 } else {
107 parse_status(trimmed).with_context(|| {
108 format!(
109 "Queue edit preview failed (task_id={}, field=status)",
110 needle
111 )
112 })?
113 };
114 preview_task.status = next_status;
115 if next_status == TaskStatus::Done || next_status == TaskStatus::Rejected {
116 preview_task.completed_at = Some(now_rfc3339.to_string());
117 } else {
118 preview_task.completed_at = None;
119 }
120 }
121 TaskEditKey::Priority => {
122 preview_task.priority = if trimmed.is_empty() {
123 preview_task.priority.cycle()
124 } else {
125 trimmed.parse::<TaskPriority>().with_context(|| {
126 format!(
127 "Queue edit preview failed (task_id={}, field=priority)",
128 needle
129 )
130 })?
131 };
132 }
133 TaskEditKey::Tags => {
134 preview_task.tags = parse_list(trimmed);
135 }
136 TaskEditKey::Scope => {
137 preview_task.scope = parse_list(trimmed);
138 }
139 TaskEditKey::Evidence => {
140 preview_task.evidence = parse_list(trimmed);
141 }
142 TaskEditKey::Plan => {
143 preview_task.plan = parse_list(trimmed);
144 }
145 TaskEditKey::Notes => {
146 preview_task.notes = parse_list(trimmed);
147 }
148 TaskEditKey::Request => {
149 preview_task.request = if trimmed.is_empty() {
150 None
151 } else {
152 Some(trimmed.to_string())
153 };
154 }
155 TaskEditKey::DependsOn => {
156 preview_task.depends_on = parse_list(trimmed);
157 }
158 TaskEditKey::Blocks => {
159 preview_task.blocks = parse_list(trimmed);
160 }
161 TaskEditKey::RelatesTo => {
162 preview_task.relates_to = parse_list(trimmed);
163 }
164 TaskEditKey::Duplicates => {
165 preview_task.duplicates = if trimmed.is_empty() {
166 None
167 } else {
168 Some(trimmed.to_string())
169 };
170 }
171 TaskEditKey::CustomFields => {
172 preview_task.custom_fields =
173 parse_custom_fields_with_context(needle, trimmed, operation)?;
174 }
175 TaskEditKey::Agent => {
176 preview_task.agent = parse_task_agent_override(trimmed).with_context(|| {
177 format!(
178 "Queue edit preview failed (task_id={}, field=agent)",
179 needle
180 )
181 })?;
182 }
183 TaskEditKey::CreatedAt => {
184 let normalized = normalize_rfc3339_input("created_at", trimmed).with_context(|| {
185 format!(
186 "Queue edit preview failed (task_id={}, field=created_at)",
187 needle
188 )
189 })?;
190 preview_task.created_at = normalized;
191 }
192 TaskEditKey::UpdatedAt => {
193 let normalized = normalize_rfc3339_input("updated_at", trimmed).with_context(|| {
194 format!(
195 "Queue edit preview failed (task_id={}, field=updated_at)",
196 needle
197 )
198 })?;
199 preview_task.updated_at = normalized;
200 }
201 TaskEditKey::CompletedAt => {
202 let normalized =
203 normalize_rfc3339_input("completed_at", trimmed).with_context(|| {
204 format!(
205 "Queue edit preview failed (task_id={}, field=completed_at)",
206 needle
207 )
208 })?;
209 preview_task.completed_at = normalized;
210 }
211 TaskEditKey::StartedAt => {
212 let normalized = normalize_rfc3339_input("started_at", trimmed).with_context(|| {
213 format!(
214 "Queue edit preview failed (task_id={}, field=started_at)",
215 needle
216 )
217 })?;
218 preview_task.started_at = normalized;
219 }
220 TaskEditKey::ScheduledStart => {
221 let normalized =
222 normalize_rfc3339_input("scheduled_start", trimmed).with_context(|| {
223 format!(
224 "Queue edit preview failed (task_id={}, field=scheduled_start)",
225 needle
226 )
227 })?;
228 preview_task.scheduled_start = normalized;
229 }
230 TaskEditKey::EstimatedMinutes => {
231 let minutes = if trimmed.is_empty() {
232 None
233 } else {
234 Some(trimmed.parse::<u32>().with_context(|| {
235 format!(
236 "Queue edit preview failed (task_id={}, field=estimated_minutes): must be a non-negative integer",
237 needle
238 )
239 })?)
240 };
241 preview_task.estimated_minutes = minutes;
242 }
243 TaskEditKey::ActualMinutes => {
244 let minutes = if trimmed.is_empty() {
245 None
246 } else {
247 Some(trimmed.parse::<u32>().with_context(|| {
248 format!(
249 "Queue edit preview failed (task_id={}, field=actual_minutes): must be a non-negative integer",
250 needle
251 )
252 })?)
253 };
254 preview_task.actual_minutes = minutes;
255 }
256 }
257
258 if !matches!(key, TaskEditKey::UpdatedAt) {
260 preview_task.updated_at = Some(now_rfc3339.to_string());
261 }
262
263 let mut preview_queue = queue.clone();
265 if let Some(index) = preview_queue
266 .tasks
267 .iter()
268 .position(|t| t.id.trim() == needle)
269 {
270 preview_queue.tasks[index] = preview_task.clone();
271 }
272
273 let warnings = match queue::validate_queue_set(
274 &preview_queue,
275 done,
276 id_prefix,
277 id_width,
278 max_dependency_depth,
279 ) {
280 Ok(warnings) => warnings,
281 Err(err) => {
282 bail!(
283 "Queue edit preview failed (task_id={}): validation error - {}",
284 needle,
285 err
286 );
287 }
288 };
289
290 let old_value = format_field_value(task, key);
292 let new_value = format_field_value(&preview_task, key);
293
294 Ok(TaskEditPreview {
295 task_id: needle.to_string(),
296 field: key.as_str().to_string(),
297 old_value,
298 new_value,
299 warnings,
300 })
301}
302
303pub(crate) fn format_field_value(task: &Task, key: TaskEditKey) -> String {
308 let sep = match key {
309 TaskEditKey::Evidence | TaskEditKey::Plan | TaskEditKey::Notes => "; ",
310 _ => ", ",
311 };
312 key.format_value(task, sep)
313}