Skip to main content

chant/domain/
spec_validation.rs

1//! Pure validation functions for spec readiness and blocking dependencies.
2
3use crate::spec::{Spec, SpecStatus};
4
5/// Check if a spec is ready to be worked on.
6///
7/// A spec is ready if:
8/// - It has status `Pending`
9/// - All dependencies in `depends_on` are completed
10/// - It doesn't require approval (or approval is granted)
11///
12/// # Arguments
13///
14/// * `spec` - The spec to check
15/// * `all_specs` - All available specs for dependency lookup
16///
17/// # Returns
18///
19/// `true` if the spec is ready to work, `false` otherwise
20pub fn is_spec_ready(spec: &Spec, all_specs: &[Spec]) -> bool {
21    // Must be pending
22    if spec.frontmatter.status != SpecStatus::Pending {
23        return false;
24    }
25
26    // Check if any dependencies are not completed
27    if let Some(deps) = &spec.frontmatter.depends_on {
28        for dep_id in deps {
29            let dep = all_specs.iter().find(|s| s.id == *dep_id);
30            match dep {
31                Some(d) if d.frontmatter.status == SpecStatus::Completed => continue,
32                _ => return false, // Dep not found or not completed
33            }
34        }
35    }
36
37    true
38}
39
40/// Get the list of spec IDs that are blocking this spec from being ready.
41///
42/// Returns IDs of specs in `depends_on` that are not yet completed.
43///
44/// # Arguments
45///
46/// * `spec` - The spec to check
47/// * `all_specs` - All available specs for dependency lookup
48///
49/// # Returns
50///
51/// A vector of spec IDs that are blocking this spec
52pub fn get_blockers(spec: &Spec, all_specs: &[Spec]) -> Vec<String> {
53    let mut blockers = Vec::new();
54
55    if let Some(deps) = &spec.frontmatter.depends_on {
56        for dep_id in deps {
57            let dep = all_specs.iter().find(|s| s.id == *dep_id);
58            match dep {
59                Some(d) if d.frontmatter.status == SpecStatus::Completed => continue,
60                _ => blockers.push(dep_id.clone()),
61            }
62        }
63    }
64
65    blockers
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_is_spec_ready_no_deps() {
74        let spec = Spec::parse(
75            "001",
76            r#"---
77status: pending
78---
79# Test
80"#,
81        )
82        .unwrap();
83
84        assert!(is_spec_ready(&spec, &[]));
85    }
86
87    #[test]
88    fn test_is_spec_ready_with_completed_dep() {
89        let dep_spec = Spec::parse(
90            "001",
91            r#"---
92status: completed
93---
94# Dependency
95"#,
96        )
97        .unwrap();
98
99        let spec = Spec::parse(
100            "002",
101            r#"---
102status: pending
103depends_on:
104  - "001"
105---
106# Test
107"#,
108        )
109        .unwrap();
110
111        assert!(is_spec_ready(&spec, &[dep_spec]));
112    }
113
114    #[test]
115    fn test_is_spec_ready_with_pending_dep() {
116        let dep_spec = Spec::parse(
117            "001",
118            r#"---
119status: pending
120---
121# Dependency
122"#,
123        )
124        .unwrap();
125
126        let spec = Spec::parse(
127            "002",
128            r#"---
129status: pending
130depends_on:
131  - "001"
132---
133# Test
134"#,
135        )
136        .unwrap();
137
138        assert!(!is_spec_ready(&spec, &[dep_spec]));
139    }
140
141    #[test]
142    fn test_get_blockers_returns_pending_deps() {
143        let dep_spec = Spec::parse(
144            "001",
145            r#"---
146status: pending
147---
148# Dependency
149"#,
150        )
151        .unwrap();
152
153        let spec = Spec::parse(
154            "002",
155            r#"---
156status: pending
157depends_on:
158  - "001"
159---
160# Test
161"#,
162        )
163        .unwrap();
164
165        let blockers = get_blockers(&spec, &[dep_spec]);
166        assert_eq!(blockers, vec!["001"]);
167    }
168
169    #[test]
170    fn test_get_blockers_empty_when_all_complete() {
171        let dep_spec = Spec::parse(
172            "001",
173            r#"---
174status: completed
175---
176# Dependency
177"#,
178        )
179        .unwrap();
180
181        let spec = Spec::parse(
182            "002",
183            r#"---
184status: pending
185depends_on:
186  - "001"
187---
188# Test
189"#,
190        )
191        .unwrap();
192
193        let blockers = get_blockers(&spec, &[dep_spec]);
194        assert!(blockers.is_empty());
195    }
196}