1use serde::{Deserialize, Deserializer, Serialize};
4use std::str::FromStr;
5
6fn deserialize_depends_on<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
8where
9 D: Deserializer<'de>,
10{
11 #[derive(Deserialize)]
12 #[serde(untagged)]
13 enum StringOrVec {
14 String(String),
15 Vec(Vec<String>),
16 }
17
18 let value = Option::<StringOrVec>::deserialize(deserializer)?;
19 Ok(value.map(|v| match v {
20 StringOrVec::String(s) => vec![s],
21 StringOrVec::Vec(v) => v,
22 }))
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
26#[serde(rename_all = "snake_case")]
27pub enum SpecStatus {
28 #[default]
29 Pending,
30 InProgress,
31 Paused,
32 Completed,
33 Failed,
34 NeedsAttention,
35 Ready,
36 Blocked,
37 Cancelled,
38}
39
40impl FromStr for SpecStatus {
41 type Err = anyhow::Error;
42
43 fn from_str(s: &str) -> Result<Self, Self::Err> {
44 match s {
45 "pending" => Ok(Self::Pending),
46 "in_progress" => Ok(Self::InProgress),
47 "paused" => Ok(Self::Paused),
48 "completed" => Ok(Self::Completed),
49 "failed" => Ok(Self::Failed),
50 "needs_attention" => Ok(Self::NeedsAttention),
51 "ready" => Ok(Self::Ready),
52 "blocked" => Ok(Self::Blocked),
53 "cancelled" => Ok(Self::Cancelled),
54 _ => anyhow::bail!("Invalid status: {}. Must be one of: pending, in_progress, paused, completed, failed, needs_attention, ready, blocked, cancelled", s),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
61#[serde(rename_all = "snake_case")]
62pub enum SpecType {
63 #[default]
64 Code,
65 Task,
66 Driver,
67 Documentation,
68 Research,
69 Group,
70}
71
72impl std::fmt::Display for SpecType {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 Self::Code => write!(f, "code"),
76 Self::Task => write!(f, "task"),
77 Self::Driver => write!(f, "driver"),
78 Self::Documentation => write!(f, "documentation"),
79 Self::Research => write!(f, "research"),
80 Self::Group => write!(f, "group"),
81 }
82 }
83}
84
85pub(crate) fn default_type_enum() -> SpecType {
86 SpecType::Code
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91#[serde(rename_all = "snake_case")]
92pub enum VerificationStatus {
93 Passed,
94 Failed,
95 Partial,
96}
97
98impl FromStr for VerificationStatus {
99 type Err = anyhow::Error;
100
101 fn from_str(s: &str) -> Result<Self, Self::Err> {
102 match s {
103 "passed" => Ok(Self::Passed),
104 "failed" => Ok(Self::Failed),
105 "partial" => Ok(Self::Partial),
106 _ => anyhow::bail!(
107 "Invalid verification status: {}. Must be one of: passed, failed, partial",
108 s
109 ),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
116#[serde(rename_all = "snake_case")]
117pub enum ApprovalStatus {
118 #[default]
119 Pending,
120 Approved,
121 Rejected,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, Default)]
126pub struct Approval {
127 #[serde(default)]
129 pub required: bool,
130 #[serde(default)]
132 pub status: ApprovalStatus,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub by: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub at: Option<String>,
139}
140
141#[derive(Debug, Clone)]
143pub struct BlockingDependency {
144 pub spec_id: String,
146 pub title: Option<String>,
148 pub status: SpecStatus,
150 pub completed_at: Option<String>,
152 pub is_sibling: bool,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SpecFrontmatter {
158 #[serde(default = "default_type_enum")]
159 pub r#type: SpecType,
160 #[serde(default)]
161 pub status: SpecStatus,
162 #[serde(
163 default,
164 skip_serializing_if = "Option::is_none",
165 deserialize_with = "deserialize_depends_on"
166 )]
167 pub depends_on: Option<Vec<String>>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub labels: Option<Vec<String>>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub target_files: Option<Vec<String>>,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub context: Option<Vec<String>>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub prompt: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub branch: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub commits: Option<Vec<String>>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub completed_at: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub model: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub tracks: Option<Vec<String>>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub informed_by: Option<Vec<String>>,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub origin: Option<Vec<String>>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub schedule: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub source_branch: Option<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub target_branch: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub conflicting_files: Option<Vec<String>>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub blocked_specs: Option<Vec<String>>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub original_spec: Option<String>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub last_verified: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub verification_status: Option<VerificationStatus>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub verification_failures: Option<Vec<String>>,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub replayed_at: Option<String>,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub replay_count: Option<u32>,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub original_completed_at: Option<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub derived_fields: Option<Vec<String>>,
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub approval: Option<Approval>,
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub members: Option<Vec<String>>,
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub output_schema: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub public: Option<bool>,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub retry_state: Option<crate::retry::RetryState>,
237}
238
239impl Default for SpecFrontmatter {
240 fn default() -> Self {
241 Self {
242 r#type: default_type_enum(),
243 status: SpecStatus::Pending,
244 depends_on: None,
245 labels: None,
246 target_files: None,
247 context: None,
248 prompt: None,
249 branch: None,
250 commits: None,
251 completed_at: None,
252 model: None,
253 tracks: None,
254 informed_by: None,
255 origin: None,
256 schedule: None,
257 source_branch: None,
258 target_branch: None,
259 conflicting_files: None,
260 blocked_specs: None,
261 original_spec: None,
262 last_verified: None,
263 verification_status: None,
264 verification_failures: None,
265 replayed_at: None,
266 replay_count: None,
267 original_completed_at: None,
268 derived_fields: None,
269 approval: None,
270 members: None,
271 output_schema: None,
272 public: None,
273 retry_state: None,
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_depends_on_string_format() {
284 let yaml = r#"
285type: code
286status: pending
287depends_on: "spec-id"
288"#;
289 let fm: SpecFrontmatter = serde_yaml::from_str(yaml).unwrap();
290 assert_eq!(fm.depends_on, Some(vec!["spec-id".to_string()]));
291 }
292
293 #[test]
294 fn test_depends_on_array_format() {
295 let yaml = r#"
296type: code
297status: pending
298depends_on: ["a", "b"]
299"#;
300 let fm: SpecFrontmatter = serde_yaml::from_str(yaml).unwrap();
301 assert_eq!(fm.depends_on, Some(vec!["a".to_string(), "b".to_string()]));
302 }
303}