Skip to main content

chant/spec/
frontmatter.rs

1//! Frontmatter types and defaults for specs.
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5/// Deserialize depends_on as either a string or array of strings
6fn 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/// Approval status for a spec
40#[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/// Approval information for a spec
50#[derive(Debug, Clone, Serialize, Deserialize, Default)]
51pub struct Approval {
52    /// Whether approval is required for this spec
53    #[serde(default)]
54    pub required: bool,
55    /// Current approval status
56    #[serde(default)]
57    pub status: ApprovalStatus,
58    /// Name of the person who approved/rejected
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub by: Option<String>,
61    /// Timestamp of approval/rejection
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub at: Option<String>,
64}
65
66/// Represents a dependency that is blocking a spec from being ready.
67#[derive(Debug, Clone)]
68pub struct BlockingDependency {
69    /// The spec ID of the blocking dependency.
70    pub spec_id: String,
71    /// The title of the blocking dependency, if available.
72    pub title: Option<String>,
73    /// The current status of the blocking dependency.
74    pub status: SpecStatus,
75    /// When the dependency was completed, if applicable.
76    pub completed_at: Option<String>,
77    /// Whether this is a sibling dependency (from group ordering).
78    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    // Documentation-specific fields
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub tracks: Option<Vec<String>>,
112    // Research-specific fields
113    #[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    // Conflict-specific fields
120    #[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    // Verification-specific fields
131    #[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    // Replay tracking fields
138    #[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    // Derivation tracking - which fields were automatically derived
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub derived_fields: Option<Vec<String>>,
147    // Approval workflow fields
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub approval: Option<Approval>,
150    // Driver/group member tracking
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub members: Option<Vec<String>>,
153    // Output schema validation
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub output_schema: Option<String>,
156    // Site generation control - set to false to exclude from site
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub public: Option<bool>,
159    // Retry state for failed specs (watch mode)
160    #[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}