Skip to main content

chant/spec/
frontmatter.rs

1//! Frontmatter types and defaults for specs.
2
3use serde::{Deserialize, Deserializer, Serialize};
4use std::str::FromStr;
5
6/// Deserialize depends_on as either a string or array of strings
7fn 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/// Spec type enum
60#[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/// Verification status enum
90#[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/// Approval status for a spec
115#[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/// Approval information for a spec
125#[derive(Debug, Clone, Serialize, Deserialize, Default)]
126pub struct Approval {
127    /// Whether approval is required for this spec
128    #[serde(default)]
129    pub required: bool,
130    /// Current approval status
131    #[serde(default)]
132    pub status: ApprovalStatus,
133    /// Name of the person who approved/rejected
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub by: Option<String>,
136    /// Timestamp of approval/rejection
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub at: Option<String>,
139}
140
141/// Represents a dependency that is blocking a spec from being ready.
142#[derive(Debug, Clone)]
143pub struct BlockingDependency {
144    /// The spec ID of the blocking dependency.
145    pub spec_id: String,
146    /// The title of the blocking dependency, if available.
147    pub title: Option<String>,
148    /// The current status of the blocking dependency.
149    pub status: SpecStatus,
150    /// When the dependency was completed, if applicable.
151    pub completed_at: Option<String>,
152    /// Whether this is a sibling dependency (from group ordering).
153    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    // Documentation-specific fields
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub tracks: Option<Vec<String>>,
187    // Research-specific fields
188    #[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    // Conflict-specific fields
195    #[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    // Verification-specific fields
206    #[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    // Replay tracking fields
213    #[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    // Derivation tracking - which fields were automatically derived
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub derived_fields: Option<Vec<String>>,
222    // Approval workflow fields
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub approval: Option<Approval>,
225    // Driver/group member tracking
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub members: Option<Vec<String>>,
228    // Output schema validation
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub output_schema: Option<String>,
231    // Site generation control - set to false to exclude from site
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub public: Option<bool>,
234    // Retry state for failed specs (watch mode)
235    #[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}