1use serde::{Deserialize, Deserializer, Serialize};
4
5fn deserialize_depends_on<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
7where
8 D: Deserializer<'de>,
9{
10 #[derive(Deserialize)]
11 #[serde(untagged)]
12 enum StringOrVec {
13 String(String),
14 Vec(Vec<String>),
15 }
16
17 let value = Option::<StringOrVec>::deserialize(deserializer)?;
18 Ok(value.map(|v| match v {
19 StringOrVec::String(s) => vec![s],
20 StringOrVec::Vec(v) => v,
21 }))
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
25#[serde(rename_all = "snake_case")]
26pub enum SpecStatus {
27 #[default]
28 Pending,
29 InProgress,
30 Paused,
31 Completed,
32 Failed,
33 NeedsAttention,
34 Ready,
35 Blocked,
36 Cancelled,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
41#[serde(rename_all = "snake_case")]
42pub enum ApprovalStatus {
43 #[default]
44 Pending,
45 Approved,
46 Rejected,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, Default)]
51pub struct Approval {
52 #[serde(default)]
54 pub required: bool,
55 #[serde(default)]
57 pub status: ApprovalStatus,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub by: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub at: Option<String>,
64}
65
66#[derive(Debug, Clone)]
68pub struct BlockingDependency {
69 pub spec_id: String,
71 pub title: Option<String>,
73 pub status: SpecStatus,
75 pub completed_at: Option<String>,
77 pub is_sibling: bool,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct SpecFrontmatter {
83 #[serde(default = "default_type")]
84 pub r#type: String,
85 #[serde(default)]
86 pub status: SpecStatus,
87 #[serde(
88 default,
89 skip_serializing_if = "Option::is_none",
90 deserialize_with = "deserialize_depends_on"
91 )]
92 pub depends_on: Option<Vec<String>>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub labels: Option<Vec<String>>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub target_files: Option<Vec<String>>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub context: Option<Vec<String>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub prompt: Option<String>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub branch: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub commits: Option<Vec<String>>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub completed_at: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub model: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub tracks: Option<Vec<String>>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub informed_by: Option<Vec<String>>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub origin: Option<Vec<String>>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub schedule: Option<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub source_branch: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub target_branch: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub conflicting_files: Option<Vec<String>>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub blocked_specs: Option<Vec<String>>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub original_spec: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub last_verified: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub verification_status: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub verification_failures: Option<Vec<String>>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub replayed_at: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub replay_count: Option<u32>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub original_completed_at: Option<String>,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub derived_fields: Option<Vec<String>>,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub approval: Option<Approval>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub members: Option<Vec<String>>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub output_schema: Option<String>,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub public: Option<bool>,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub retry_state: Option<crate::retry::RetryState>,
162}
163
164pub(crate) fn default_type() -> String {
165 "code".to_string()
166}
167
168impl Default for SpecFrontmatter {
169 fn default() -> Self {
170 Self {
171 r#type: default_type(),
172 status: SpecStatus::Pending,
173 depends_on: None,
174 labels: None,
175 target_files: None,
176 context: None,
177 prompt: None,
178 branch: None,
179 commits: None,
180 completed_at: None,
181 model: None,
182 tracks: None,
183 informed_by: None,
184 origin: None,
185 schedule: None,
186 source_branch: None,
187 target_branch: None,
188 conflicting_files: None,
189 blocked_specs: None,
190 original_spec: None,
191 last_verified: None,
192 verification_status: None,
193 verification_failures: None,
194 replayed_at: None,
195 replay_count: None,
196 original_completed_at: None,
197 derived_fields: None,
198 approval: None,
199 members: None,
200 output_schema: None,
201 public: None,
202 retry_state: None,
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_depends_on_string_format() {
213 let yaml = r#"
214type: code
215status: pending
216depends_on: "spec-id"
217"#;
218 let fm: SpecFrontmatter = serde_yaml::from_str(yaml).unwrap();
219 assert_eq!(fm.depends_on, Some(vec!["spec-id".to_string()]));
220 }
221
222 #[test]
223 fn test_depends_on_array_format() {
224 let yaml = r#"
225type: code
226status: pending
227depends_on: ["a", "b"]
228"#;
229 let fm: SpecFrontmatter = serde_yaml::from_str(yaml).unwrap();
230 assert_eq!(fm.depends_on, Some(vec!["a".to_string(), "b".to_string()]));
231 }
232}