use crate::contracts::{TaskAgent, TaskStatus};
use crate::timeutil;
use anyhow::{Context, Result, bail};
use time::UtcOffset;
pub(crate) fn parse_status(value: &str) -> Result<TaskStatus> {
match value.trim().to_lowercase().as_str() {
"draft" => Ok(TaskStatus::Draft),
"todo" => Ok(TaskStatus::Todo),
"doing" => Ok(TaskStatus::Doing),
"done" => Ok(TaskStatus::Done),
"rejected" => Ok(TaskStatus::Rejected),
_ => bail!(
"Invalid status: '{}'. Expected one of: draft, todo, doing, done, rejected.",
value
),
}
}
pub(crate) fn parse_list(input: &str) -> Vec<String> {
input
.split([',', '\n'])
.map(|item| item.trim().to_string())
.filter(|item| !item.is_empty())
.collect()
}
pub(crate) fn normalize_rfc3339_input(label: &str, value: &str) -> Result<Option<String>> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(None);
}
let dt = timeutil::parse_rfc3339(trimmed)
.with_context(|| format!("{} must be a valid RFC3339 timestamp", label))?;
if dt.offset() != UtcOffset::UTC {
bail!("{} must be a valid RFC3339 UTC timestamp", label);
}
let formatted = timeutil::format_rfc3339(dt)
.with_context(|| format!("{} must be a valid RFC3339 timestamp", label))?;
Ok(Some(formatted))
}
pub(crate) fn parse_task_agent_override(input: &str) -> Result<Option<TaskAgent>> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Ok(None);
}
let parsed: TaskAgent = serde_json::from_str(trimmed)
.context("agent must be a valid JSON object matching the task.agent contract")?;
if let Some(iterations) = parsed.iterations
&& iterations == 0
{
bail!("agent.iterations must be >= 1");
}
if let Some(phases) = parsed.phases
&& !(1..=3).contains(&phases)
{
bail!("agent.phases must be one of: 1, 2, 3");
}
Ok(Some(parsed))
}
pub(crate) fn cycle_status(status: TaskStatus) -> TaskStatus {
match status {
TaskStatus::Draft => TaskStatus::Todo,
TaskStatus::Todo => TaskStatus::Doing,
TaskStatus::Doing => TaskStatus::Done,
TaskStatus::Done => TaskStatus::Rejected,
TaskStatus::Rejected => TaskStatus::Draft,
}
}