Skip to main content

chant/
spec_group.rs

1//! Spec group/driver orchestration logic.
2//!
3//! This module manages spec membership and group completion tracking for driver specs.
4//! Driver specs can have member specs identified by numeric suffixes (e.g., `.1`, `.2`).
5//! This module handles relationships between drivers and their members.
6
7use crate::spec::{Spec, SpecStatus};
8use anyhow::Result;
9use std::path::Path;
10
11/// Check if `member_id` is a group member of `driver_id`.
12///
13/// Member IDs have format: `DRIVER_ID.N` or `DRIVER_ID.N.M` where N and M are numbers.
14/// For example: `2026-01-25-00y-abc.1` is a member of `2026-01-25-00y-abc`.
15///
16/// # Examples
17///
18/// ```ignore
19/// assert!(is_member_of("2026-01-25-00y-abc.1", "2026-01-25-00y-abc"));
20/// assert!(is_member_of("2026-01-25-00y-abc.2.1", "2026-01-25-00y-abc"));
21/// assert!(!is_member_of("2026-01-25-00y-abc", "2026-01-25-00y-abc")); // Not a member
22/// assert!(!is_member_of("2026-01-25-00x-xyz", "2026-01-25-00y-abc")); // Different driver
23/// ```
24pub fn is_member_of(member_id: &str, driver_id: &str) -> bool {
25    // Member IDs have format: DRIVER_ID.N or DRIVER_ID.N.M
26    if !member_id.starts_with(driver_id) {
27        return false;
28    }
29
30    let suffix = &member_id[driver_id.len()..];
31    suffix.starts_with('.') && suffix.len() > 1
32}
33
34/// Get all member specs of a driver spec.
35///
36/// Returns a vector of references to all spec members of the given driver.
37/// If the driver has no members, returns an empty vector.
38///
39/// # Arguments
40///
41/// * `driver_id` - The ID of the driver spec
42/// * `specs` - All available specs to search
43///
44/// # Examples
45///
46/// ```ignore
47/// let members = get_members("2026-01-25-00y-abc", &specs);
48/// ```
49pub fn get_members<'a>(driver_id: &str, specs: &'a [Spec]) -> Vec<&'a Spec> {
50    specs
51        .iter()
52        .filter(|s| is_member_of(&s.id, driver_id))
53        .collect()
54}
55
56/// Check if all members of a driver spec are completed.
57///
58/// Returns true if:
59/// - The driver has no members, or
60/// - All members have status `Completed`
61///
62/// # Arguments
63///
64/// * `driver_id` - The ID of the driver spec
65/// * `specs` - All available specs
66///
67/// # Examples
68///
69/// ```ignore
70/// if all_members_completed("2026-01-25-00y-abc", &specs) {
71///     println!("All members are done!");
72/// }
73/// ```
74pub fn all_members_completed(driver_id: &str, specs: &[Spec]) -> bool {
75    let members = get_members(driver_id, specs);
76    if members.is_empty() {
77        return true; // No members, so all are "completed"
78    }
79    members
80        .iter()
81        .all(|m| m.frontmatter.status == SpecStatus::Completed)
82}
83
84/// Get list of incomplete member spec IDs for a driver spec.
85///
86/// Returns a vector of IDs for all members that are not in `Completed` status.
87/// Returns an empty vector if the spec is not a driver or has no incomplete members.
88///
89/// # Arguments
90///
91/// * `driver_id` - The ID of the driver spec
92/// * `all_specs` - All available specs
93///
94/// # Examples
95///
96/// ```ignore
97/// let incomplete = get_incomplete_members("2026-01-25-00y-abc", &specs);
98/// for member_id in incomplete {
99///     println!("Incomplete member: {}", member_id);
100/// }
101/// ```
102pub fn get_incomplete_members(driver_id: &str, all_specs: &[Spec]) -> Vec<String> {
103    get_members(driver_id, all_specs)
104        .into_iter()
105        .filter(|m| m.frontmatter.status != SpecStatus::Completed)
106        .map(|m| m.id.clone())
107        .collect()
108}
109
110/// Extract the driver ID from a member ID.
111///
112/// For member specs with numeric suffixes, returns the base driver ID.
113/// For non-member specs, returns None.
114///
115/// # Examples
116///
117/// ```ignore
118/// assert_eq!(extract_driver_id("2026-01-25-00y-abc.1"), Some("2026-01-25-00y-abc".to_string()));
119/// assert_eq!(extract_driver_id("2026-01-25-00y-abc.3.2"), Some("2026-01-25-00y-abc".to_string()));
120/// assert_eq!(extract_driver_id("2026-01-25-00y-abc"), None);
121/// assert_eq!(extract_driver_id("2026-01-25-00y-abc.abc"), None);
122/// ```
123pub fn extract_driver_id(member_id: &str) -> Option<String> {
124    // Member IDs have format: DRIVER_ID.N or DRIVER_ID.N.M
125    if let Some(pos) = member_id.find('.') {
126        let (prefix, suffix) = member_id.split_at(pos);
127        // Check that what follows the dot is numeric (at least up to the first non-digit)
128        if suffix.len() > 1
129            && suffix[1..]
130                .chars()
131                .next()
132                .is_some_and(|c| c.is_ascii_digit())
133        {
134            return Some(prefix.to_string());
135        }
136    }
137    None
138}
139
140/// Extract the member number from a member ID.
141///
142/// For member specs with format `DRIVER_ID.N` or `DRIVER_ID.N.M`, extracts `N`.
143/// For non-member specs, returns None.
144///
145/// # Examples
146///
147/// ```ignore
148/// assert_eq!(extract_member_number("2026-01-25-00y-abc.1"), Some(1));
149/// assert_eq!(extract_member_number("2026-01-25-00y-abc.3"), Some(3));
150/// assert_eq!(extract_member_number("2026-01-25-00y-abc.10"), Some(10));
151/// assert_eq!(extract_member_number("2026-01-25-00y-abc.3.2"), Some(3));
152/// assert_eq!(extract_member_number("2026-01-25-00y-abc"), None);
153/// assert_eq!(extract_member_number("2026-01-25-00y-abc.abc"), None);
154/// ```
155pub fn extract_member_number(member_id: &str) -> Option<u32> {
156    if let Some(pos) = member_id.find('.') {
157        let suffix = &member_id[pos + 1..];
158        // Extract just the first numeric part after the dot
159        let num_str: String = suffix.chars().take_while(|c| c.is_ascii_digit()).collect();
160        if !num_str.is_empty() {
161            return num_str.parse::<u32>().ok();
162        }
163    }
164    None
165}
166
167/// Compare two spec IDs with numeric sorting for member specs and base36 sequences.
168///
169/// This function provides a natural sort order where member spec numbers and base36
170/// sequence portions are compared numerically rather than lexicographically. This ensures:
171/// - Specs like `2026-01-25-00y-abc.10` sort after `2026-01-25-00y-abc.2`
172/// - Specs like `2026-01-25-010-xxx` sort after `2026-01-25-00z-yyy`
173///
174/// # Sorting behavior
175///
176/// - For non-member specs, parses date/sequence/suffix and compares sequence numerically
177/// - For member specs (with `.N` suffix), compares the base ID using date/sequence/suffix
178///   parsing first, then compares member numbers numerically
179/// - Mixed member/non-member specs: non-members sort before members with the same base
180///
181/// # Examples
182///
183/// ```ignore
184/// use std::cmp::Ordering;
185/// assert_eq!(compare_spec_ids("2026-01-25-00y-abc.2", "2026-01-25-00y-abc.10"), Ordering::Less);
186/// assert_eq!(compare_spec_ids("2026-01-25-00y-abc.10", "2026-01-25-00y-abc.2"), Ordering::Greater);
187/// assert_eq!(compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-def"), Ordering::Less);
188/// assert_eq!(compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-abc.1"), Ordering::Less);
189/// assert_eq!(compare_spec_ids("2026-01-25-010-xxx", "2026-01-25-00z-yyy"), Ordering::Greater);
190/// ```
191pub fn compare_spec_ids(a: &str, b: &str) -> std::cmp::Ordering {
192    use std::cmp::Ordering;
193
194    // Try to extract driver IDs and member numbers
195    let a_driver = extract_driver_id(a);
196    let b_driver = extract_driver_id(b);
197
198    match (a_driver, b_driver) {
199        (Some(a_base), Some(b_base)) => {
200            // Both are member specs
201            // First compare the base IDs with sequence parsing
202            match compare_base_ids(&a_base, &b_base) {
203                Ordering::Equal => {
204                    // Same base ID, compare member numbers numerically
205                    let a_num = extract_member_number(a).unwrap_or(u32::MAX);
206                    let b_num = extract_member_number(b).unwrap_or(u32::MAX);
207                    a_num.cmp(&b_num)
208                }
209                other => other,
210            }
211        }
212        (Some(a_base), None) => {
213            // a is a member, b is not
214            // Compare a's base with b using sequence parsing
215            match compare_base_ids(&a_base, b) {
216                Ordering::Equal => {
217                    // b is the driver of a, so b comes first
218                    Ordering::Greater
219                }
220                other => other,
221            }
222        }
223        (None, Some(b_base)) => {
224            // a is not a member, b is
225            // Compare a with b's base using sequence parsing
226            match compare_base_ids(a, &b_base) {
227                Ordering::Equal => {
228                    // a is the driver of b, so a comes first
229                    Ordering::Less
230                }
231                other => other,
232            }
233        }
234        (None, None) => {
235            // Neither are member specs, use sequence parsing
236            compare_base_ids(a, b)
237        }
238    }
239}
240
241/// Compare two base spec IDs by parsing date, sequence, and suffix.
242///
243/// Spec IDs have format: YYYY-MM-DD-SSS-XXX where:
244/// - YYYY-MM-DD is the date (compared lexicographically)
245/// - SSS is a base36 sequence (compared numerically)
246/// - XXX is a random base36 suffix (compared lexicographically as tiebreaker)
247fn compare_base_ids(a: &str, b: &str) -> std::cmp::Ordering {
248    use std::cmp::Ordering;
249
250    // Parse both IDs into (date, sequence, suffix)
251    let a_parts = parse_spec_id_parts(a);
252    let b_parts = parse_spec_id_parts(b);
253
254    match (a_parts, b_parts) {
255        (Some((a_date, a_seq, a_suffix)), Some((b_date, b_seq, b_suffix))) => {
256            // Compare date lexicographically
257            match a_date.cmp(b_date) {
258                Ordering::Equal => {
259                    // Same date, compare sequence numerically
260                    match a_seq.cmp(&b_seq) {
261                        Ordering::Equal => {
262                            // Same sequence, compare suffix lexicographically
263                            a_suffix.cmp(b_suffix)
264                        }
265                        other => other,
266                    }
267                }
268                other => other,
269            }
270        }
271        // If parsing fails, fall back to lexicographic comparison
272        _ => a.cmp(b),
273    }
274}
275
276/// Parse a spec ID into (date, sequence_number, suffix).
277/// Returns None if the ID doesn't match the expected format.
278fn parse_spec_id_parts(id: &str) -> Option<(&str, u32, &str)> {
279    let parts: Vec<&str> = id.split('-').collect();
280
281    // Expected format: YYYY-MM-DD-SSS-XXX (5 parts minimum)
282    if parts.len() < 5 {
283        return None;
284    }
285
286    // Date is parts[0..3] joined: YYYY-MM-DD
287    let date = &id[..10]; // "YYYY-MM-DD" is always 10 chars
288
289    // Sequence is parts[3], parse from base36
290    let seq = crate::id::parse_base36(parts[3])?;
291
292    // Suffix is parts[4]
293    let suffix = parts[4];
294
295    Some((date, seq, suffix))
296}
297
298/// Check if all prior siblings of a member spec are completed.
299///
300/// For a member spec like `DRIVER_ID.3`, checks that `DRIVER_ID.1` and `DRIVER_ID.2`
301/// are both in `Completed` status. For `DRIVER_ID.1`, returns true (no prior siblings).
302/// For non-member specs, returns true (sibling check doesn't apply).
303///
304/// # Arguments
305///
306/// * `member_id` - The ID of the member spec to check
307/// * `all_specs` - All available specs
308///
309/// # Examples
310///
311/// ```ignore
312/// // For a spec DRIVER_ID.3, checks that DRIVER_ID.1 and DRIVER_ID.2 are completed
313/// assert!(all_prior_siblings_completed("2026-01-25-00y-abc.3", &specs));
314/// ```
315pub fn all_prior_siblings_completed(member_id: &str, all_specs: &[Spec]) -> bool {
316    // Find the current member spec
317    if let Some(member_spec) = all_specs.iter().find(|s| s.id == member_id) {
318        // If member has explicit depends_on, skip sequential check (use DAG dependencies instead)
319        if member_spec.frontmatter.depends_on.is_some() {
320            return true;
321        }
322    }
323
324    // Fall back to sequential ordering if no explicit dependencies
325    if let Some(driver_id) = extract_driver_id(member_id) {
326        if let Some(member_num) = extract_member_number(member_id) {
327            // Check all specs with numbers less than member_num
328            for i in 1..member_num {
329                let sibling_id = format!("{}.{}", driver_id, i);
330                let sibling = all_specs.iter().find(|s| s.id == sibling_id);
331                if let Some(s) = sibling {
332                    if s.frontmatter.status != SpecStatus::Completed {
333                        return false;
334                    }
335                } else {
336                    // Sibling doesn't exist, so it's not completed
337                    return false;
338                }
339            }
340            return true;
341        }
342    }
343    // Not a member spec, so this check doesn't apply
344    true
345}
346
347/// Mark the driver spec as in_progress if the current spec is a member.
348///
349/// When a member spec begins execution, its driver spec should transition from
350/// `Pending` to `InProgress` (if not already). This function handles that transition.
351///
352/// # Arguments
353///
354/// * `specs_dir` - Path to the specs directory
355/// * `member_id` - The ID of the member spec that is starting
356///
357/// # Returns
358///
359/// Returns `Ok(())` if successful or the driver doesn't exist.
360/// Returns `Err` if file I/O fails.
361///
362/// # Examples
363///
364/// ```ignore
365/// mark_driver_in_progress(&specs_dir, "2026-01-25-00y-abc.1")?;
366/// ```
367/// Mark a driver spec as in_progress when one of its members starts work.
368///
369/// This creates a "phantom" in_progress status for the driver that serves as a placeholder
370/// until all members complete. The driver will be auto-completed when the last member finishes.
371///
372/// Note: This means that during member execution, both the driver AND the active member
373/// will show as in_progress. This is by design but can be confusing in status displays.
374///
375/// Set `skip` to true to avoid marking the driver (useful in chain mode where we want
376/// only one spec to show as in_progress at a time).
377pub fn mark_driver_in_progress_conditional(
378    specs_dir: &Path,
379    member_id: &str,
380    skip: bool,
381) -> Result<()> {
382    use crate::spec::TransitionBuilder;
383
384    if skip {
385        return Ok(());
386    }
387
388    if let Some(driver_id) = extract_driver_id(member_id) {
389        // Try to load the driver spec
390        let driver_path = specs_dir.join(format!("{}.md", driver_id));
391        if driver_path.exists() {
392            let mut driver = Spec::load(&driver_path)?;
393            if driver.frontmatter.status == SpecStatus::Pending {
394                TransitionBuilder::new(&mut driver).to(SpecStatus::InProgress)?;
395                driver.save(&driver_path)?;
396            }
397        }
398    }
399    Ok(())
400}
401
402/// Mark a driver spec as in_progress when one of its members starts work.
403///
404/// Convenience wrapper that always marks the driver. For conditional marking,
405/// use `mark_driver_in_progress_conditional`.
406pub fn mark_driver_in_progress(specs_dir: &Path, member_id: &str) -> Result<()> {
407    mark_driver_in_progress_conditional(specs_dir, member_id, false)
408}
409
410/// Auto-complete a driver spec if all its members are now completed.
411///
412/// When a member spec completes, check if all other members are also completed.
413/// If so, and the driver is in `InProgress` status, automatically mark the driver
414/// as `Completed` with completion timestamp.
415///
416/// # Arguments
417///
418/// * `member_id` - The ID of the member spec that just completed
419/// * `all_specs` - All available specs
420/// * `specs_dir` - Path to the specs directory
421///
422/// # Returns
423///
424/// Returns `Ok(true)` if the driver was auto-completed.
425/// Returns `Ok(false)` if the driver was not ready for completion.
426/// Returns `Err` if file I/O fails.
427///
428/// # Examples
429///
430/// ```ignore
431/// if auto_complete_driver_if_ready("2026-01-25-00y-abc.2", &specs, &specs_dir)? {
432///     println!("Driver was auto-completed!");
433/// }
434/// ```
435pub fn auto_complete_driver_if_ready(
436    member_id: &str,
437    all_specs: &[Spec],
438    specs_dir: &Path,
439) -> Result<bool> {
440    // Only member specs can trigger driver auto-completion
441    let Some(driver_id) = extract_driver_id(member_id) else {
442        return Ok(false);
443    };
444
445    // Find the driver spec
446    let Some(driver_spec) = all_specs.iter().find(|s| s.id == driver_id) else {
447        return Ok(false);
448    };
449
450    // Only auto-complete if driver is in_progress or pending
451    // (Pending is allowed for chain mode where drivers aren't marked InProgress)
452    if driver_spec.frontmatter.status != SpecStatus::InProgress
453        && driver_spec.frontmatter.status != SpecStatus::Pending
454    {
455        return Ok(false);
456    }
457
458    // Check if all members are completed
459    if !all_members_completed(&driver_id, all_specs) {
460        return Ok(false);
461    }
462
463    // All members are completed, so auto-complete the driver
464    let driver_path = specs_dir.join(format!("{}.md", driver_id));
465    let mut driver = Spec::load(&driver_path)?;
466
467    use crate::spec::TransitionBuilder;
468    // Use force() to allow Pending→Completed transition in chain mode
469    TransitionBuilder::new(&mut driver)
470        .force()
471        .to(SpecStatus::Completed)?;
472    driver.frontmatter.completed_at = Some(crate::utc_now_iso());
473    driver.frontmatter.model = Some("auto-completed".to_string());
474
475    driver.save(&driver_path)?;
476
477    Ok(true)
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    #[test]
485    fn test_is_member_of() {
486        assert!(is_member_of("2026-01-22-001-x7m.1", "2026-01-22-001-x7m"));
487        assert!(is_member_of("2026-01-22-001-x7m.2.1", "2026-01-22-001-x7m"));
488        assert!(!is_member_of("2026-01-22-001-x7m", "2026-01-22-001-x7m"));
489        assert!(!is_member_of("2026-01-22-002-y8n", "2026-01-22-001-x7m"));
490    }
491
492    #[test]
493    fn test_extract_driver_id() {
494        assert_eq!(
495            extract_driver_id("2026-01-22-001-x7m.1"),
496            Some("2026-01-22-001-x7m".to_string())
497        );
498        assert_eq!(
499            extract_driver_id("2026-01-22-001-x7m.2.1"),
500            Some("2026-01-22-001-x7m".to_string())
501        );
502        assert_eq!(extract_driver_id("2026-01-22-001-x7m"), None);
503        assert_eq!(extract_driver_id("2026-01-22-001-x7m.abc"), None);
504    }
505
506    #[test]
507    fn test_extract_member_number() {
508        assert_eq!(extract_member_number("2026-01-24-001-abc.1"), Some(1));
509        assert_eq!(extract_member_number("2026-01-24-001-abc.3"), Some(3));
510        assert_eq!(extract_member_number("2026-01-24-001-abc.10"), Some(10));
511        assert_eq!(extract_member_number("2026-01-24-001-abc.3.2"), Some(3));
512        assert_eq!(extract_member_number("2026-01-24-001-abc"), None);
513        assert_eq!(extract_member_number("2026-01-24-001-abc.abc"), None);
514    }
515
516    #[test]
517    fn test_all_prior_siblings_completed() {
518        // Test spec for member .1 with no prior siblings
519        let spec1 = Spec::parse(
520            "2026-01-24-001-abc.1",
521            r#"---
522status: pending
523---
524# Test
525"#,
526        )
527        .unwrap();
528
529        // Should be ready since it has no prior siblings
530        assert!(all_prior_siblings_completed(&spec1.id, &[]));
531
532        // Test spec for member .3 with completed prior siblings
533        let spec_prior_1 = Spec::parse(
534            "2026-01-24-001-abc.1",
535            r#"---
536status: completed
537---
538# Test
539"#,
540        )
541        .unwrap();
542
543        let spec_prior_2 = Spec::parse(
544            "2026-01-24-001-abc.2",
545            r#"---
546status: completed
547---
548# Test
549"#,
550        )
551        .unwrap();
552
553        let spec3 = Spec::parse(
554            "2026-01-24-001-abc.3",
555            r#"---
556status: pending
557---
558# Test
559"#,
560        )
561        .unwrap();
562
563        let all_specs = vec![spec_prior_1, spec_prior_2, spec3.clone()];
564        assert!(all_prior_siblings_completed(&spec3.id, &all_specs));
565    }
566
567    #[test]
568    fn test_all_prior_siblings_completed_missing() {
569        // Test spec for member .3 with missing prior sibling
570        let spec_prior_1 = Spec::parse(
571            "2026-01-24-001-abc.1",
572            r#"---
573status: completed
574---
575# Test
576"#,
577        )
578        .unwrap();
579
580        let spec3 = Spec::parse(
581            "2026-01-24-001-abc.3",
582            r#"---
583status: pending
584---
585# Test
586"#,
587        )
588        .unwrap();
589
590        // Only spec .1 exists, .2 is missing
591        let all_specs = vec![spec_prior_1, spec3.clone()];
592        assert!(!all_prior_siblings_completed(&spec3.id, &all_specs));
593    }
594
595    #[test]
596    fn test_all_prior_siblings_completed_not_completed() {
597        // Test spec for member .2 with incomplete prior sibling
598        let spec_prior_1 = Spec::parse(
599            "2026-01-24-001-abc.1",
600            r#"---
601status: pending
602---
603# Test
604"#,
605        )
606        .unwrap();
607
608        let spec2 = Spec::parse(
609            "2026-01-24-001-abc.2",
610            r#"---
611status: pending
612---
613# Test
614"#,
615        )
616        .unwrap();
617
618        let all_specs = vec![spec_prior_1, spec2.clone()];
619        assert!(!all_prior_siblings_completed(&spec2.id, &all_specs));
620    }
621
622    #[test]
623    fn test_mark_driver_in_progress_when_member_starts() {
624        use tempfile::TempDir;
625
626        let temp_dir = TempDir::new().unwrap();
627        let specs_dir = temp_dir.path();
628
629        // Create a driver spec that is pending
630        let driver_spec = Spec {
631            id: "2026-01-24-001-abc".to_string(),
632            frontmatter: crate::spec::SpecFrontmatter {
633                status: SpecStatus::Pending,
634                ..Default::default()
635            },
636            title: Some("Driver spec".to_string()),
637            body: "# Driver spec\n\nBody content.".to_string(),
638        };
639
640        let driver_path = specs_dir.join("2026-01-24-001-abc.md");
641        driver_spec.save(&driver_path).unwrap();
642
643        // Mark driver as in_progress when member starts
644        mark_driver_in_progress(specs_dir, "2026-01-24-001-abc.1").unwrap();
645
646        // Verify driver status was updated to in_progress
647        let updated_driver = Spec::load(&driver_path).unwrap();
648        assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
649    }
650
651    #[test]
652    fn test_mark_driver_in_progress_skips_if_already_in_progress() {
653        use tempfile::TempDir;
654
655        let temp_dir = TempDir::new().unwrap();
656        let specs_dir = temp_dir.path();
657
658        // Create a driver spec that is already in_progress
659        let driver_spec = Spec {
660            id: "2026-01-24-002-def".to_string(),
661            frontmatter: crate::spec::SpecFrontmatter {
662                status: SpecStatus::InProgress,
663                ..Default::default()
664            },
665            title: Some("Driver spec".to_string()),
666            body: "# Driver spec\n\nBody content.".to_string(),
667        };
668
669        let driver_path = specs_dir.join("2026-01-24-002-def.md");
670        driver_spec.save(&driver_path).unwrap();
671
672        // Try to mark driver as in_progress
673        mark_driver_in_progress(specs_dir, "2026-01-24-002-def.1").unwrap();
674
675        // Verify driver status is still in_progress (not changed)
676        let updated_driver = Spec::load(&driver_path).unwrap();
677        assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
678    }
679
680    #[test]
681    fn test_mark_driver_in_progress_nonexistent_driver() {
682        use tempfile::TempDir;
683
684        let temp_dir = TempDir::new().unwrap();
685        let specs_dir = temp_dir.path();
686
687        // Try to mark driver as in_progress when driver doesn't exist
688        // Should not error, just skip
689        mark_driver_in_progress(specs_dir, "2026-01-24-003-ghi.1").unwrap();
690    }
691
692    #[test]
693    fn test_get_incomplete_members() {
694        // Driver with multiple incomplete members
695        let driver = Spec::parse(
696            "2026-01-24-005-mno",
697            r#"---
698status: in_progress
699---
700# Driver
701"#,
702        )
703        .unwrap();
704
705        let member1 = Spec::parse(
706            "2026-01-24-005-mno.1",
707            r#"---
708status: completed
709---
710# Member 1
711"#,
712        )
713        .unwrap();
714
715        let member2 = Spec::parse(
716            "2026-01-24-005-mno.2",
717            r#"---
718status: pending
719---
720# Member 2
721"#,
722        )
723        .unwrap();
724
725        let member3 = Spec::parse(
726            "2026-01-24-005-mno.3",
727            r#"---
728status: in_progress
729---
730# Member 3
731"#,
732        )
733        .unwrap();
734
735        let all_specs = vec![driver.clone(), member1, member2, member3];
736        let incomplete = get_incomplete_members(&driver.id, &all_specs);
737        assert_eq!(incomplete.len(), 2);
738        assert!(incomplete.contains(&"2026-01-24-005-mno.2".to_string()));
739        assert!(incomplete.contains(&"2026-01-24-005-mno.3".to_string()));
740    }
741
742    #[test]
743    fn test_auto_complete_driver_not_member_spec() {
744        use tempfile::TempDir;
745
746        let temp_dir = TempDir::new().unwrap();
747        let specs_dir = temp_dir.path();
748
749        // A non-member spec should not trigger auto-completion
750        let driver_spec = Spec::parse(
751            "2026-01-24-006-pqr",
752            r#"---
753status: in_progress
754---
755# Driver spec
756"#,
757        )
758        .unwrap();
759
760        let result =
761            auto_complete_driver_if_ready("2026-01-24-006-pqr", &[driver_spec], specs_dir).unwrap();
762        assert!(
763            !result,
764            "Non-member spec should not trigger auto-completion"
765        );
766    }
767
768    #[test]
769    fn test_auto_complete_driver_when_already_completed() {
770        use tempfile::TempDir;
771
772        let temp_dir = TempDir::new().unwrap();
773        let specs_dir = temp_dir.path();
774
775        // Create a driver spec that is already completed
776        let driver_spec = Spec::parse(
777            "2026-01-24-007-stu",
778            r#"---
779status: completed
780---
781# Driver spec
782"#,
783        )
784        .unwrap();
785
786        let member_spec = Spec::parse(
787            "2026-01-24-007-stu.1",
788            r#"---
789status: completed
790---
791# Member 1
792"#,
793        )
794        .unwrap();
795
796        let all_specs = vec![driver_spec, member_spec];
797        let result =
798            auto_complete_driver_if_ready("2026-01-24-007-stu.1", &all_specs, specs_dir).unwrap();
799        assert!(
800            !result,
801            "Driver already completed should not be re-completed"
802        );
803    }
804
805    #[test]
806    fn test_auto_complete_driver_from_pending() {
807        use std::fs;
808        use tempfile::TempDir;
809
810        let temp_dir = TempDir::new().unwrap();
811        let specs_dir = temp_dir.path();
812
813        // Create driver spec file (pending status)
814        let driver_content = r#"---
815status: pending
816---
817# Driver spec
818"#;
819        fs::write(specs_dir.join("2026-01-24-009-xyz.md"), driver_content).unwrap();
820
821        // Parse specs for all_specs
822        let driver_spec = Spec::parse("2026-01-24-009-xyz", driver_content).unwrap();
823
824        let member_spec = Spec::parse(
825            "2026-01-24-009-xyz.1",
826            r#"---
827status: completed
828---
829# Member 1
830"#,
831        )
832        .unwrap();
833
834        let all_specs = vec![driver_spec, member_spec];
835
836        // Auto-complete should succeed for pending driver (chain mode scenario)
837        let result =
838            auto_complete_driver_if_ready("2026-01-24-009-xyz.1", &all_specs, specs_dir).unwrap();
839        assert!(
840            result,
841            "Pending driver should be auto-completed when all members are done (chain mode)"
842        );
843
844        // Verify driver is now completed
845        let updated_driver = Spec::load(&specs_dir.join("2026-01-24-009-xyz.md")).unwrap();
846        assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
847    }
848
849    #[test]
850    fn test_auto_complete_driver_incomplete_members() {
851        use tempfile::TempDir;
852
853        let temp_dir = TempDir::new().unwrap();
854        let specs_dir = temp_dir.path();
855
856        // Create a driver spec that is in_progress
857        let driver_spec = Spec {
858            id: "2026-01-24-008-vwx".to_string(),
859            frontmatter: crate::spec::SpecFrontmatter {
860                status: SpecStatus::InProgress,
861                ..Default::default()
862            },
863            title: Some("Driver".to_string()),
864            body: "# Driver\n\nBody.".to_string(),
865        };
866
867        let driver_path = specs_dir.join("2026-01-24-008-vwx.md");
868        driver_spec.save(&driver_path).unwrap();
869
870        // Create member specs where not all are completed
871        let member1 = Spec::parse(
872            "2026-01-24-008-vwx.1",
873            r#"---
874status: completed
875---
876# Member 1
877"#,
878        )
879        .unwrap();
880
881        let member2 = Spec::parse(
882            "2026-01-24-008-vwx.2",
883            r#"---
884status: in_progress
885---
886# Member 2
887"#,
888        )
889        .unwrap();
890
891        let all_specs = vec![driver_spec, member1, member2];
892        let result =
893            auto_complete_driver_if_ready("2026-01-24-008-vwx.1", &all_specs, specs_dir).unwrap();
894        assert!(
895            !result,
896            "Driver should not complete when members are incomplete"
897        );
898    }
899
900    #[test]
901    fn test_auto_complete_driver_success() {
902        use tempfile::TempDir;
903
904        let temp_dir = TempDir::new().unwrap();
905        let specs_dir = temp_dir.path();
906
907        // Create a driver spec that is in_progress
908        let driver_spec = Spec {
909            id: "2026-01-24-009-yz0".to_string(),
910            frontmatter: crate::spec::SpecFrontmatter {
911                status: SpecStatus::InProgress,
912                ..Default::default()
913            },
914            title: Some("Driver".to_string()),
915            body: "# Driver\n\nBody.".to_string(),
916        };
917
918        let driver_path = specs_dir.join("2026-01-24-009-yz0.md");
919        driver_spec.save(&driver_path).unwrap();
920
921        // Create member specs where all are completed
922        let member1 = Spec::parse(
923            "2026-01-24-009-yz0.1",
924            r#"---
925status: completed
926---
927# Member 1
928"#,
929        )
930        .unwrap();
931
932        let member2 = Spec::parse(
933            "2026-01-24-009-yz0.2",
934            r#"---
935status: completed
936---
937# Member 2
938"#,
939        )
940        .unwrap();
941
942        let all_specs = vec![driver_spec, member1, member2];
943
944        // Auto-complete should succeed
945        let result =
946            auto_complete_driver_if_ready("2026-01-24-009-yz0.2", &all_specs, specs_dir).unwrap();
947        assert!(
948            result,
949            "Driver should be auto-completed when all members are completed"
950        );
951
952        // Verify driver was updated
953        let updated_driver = Spec::load(&driver_path).unwrap();
954        assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
955        assert_eq!(
956            updated_driver.frontmatter.model,
957            Some("auto-completed".to_string())
958        );
959        assert!(updated_driver.frontmatter.completed_at.is_some());
960    }
961
962    #[test]
963    fn test_auto_complete_driver_nonexistent_driver() {
964        use tempfile::TempDir;
965
966        let temp_dir = TempDir::new().unwrap();
967        let specs_dir = temp_dir.path();
968
969        // Try to auto-complete when driver doesn't exist
970        let all_specs = vec![];
971        let result =
972            auto_complete_driver_if_ready("2026-01-24-010-abc.1", &all_specs, specs_dir).unwrap();
973        assert!(
974            !result,
975            "Should return false when driver spec doesn't exist"
976        );
977    }
978
979    #[test]
980    fn test_auto_complete_driver_single_member() {
981        use tempfile::TempDir;
982
983        let temp_dir = TempDir::new().unwrap();
984        let specs_dir = temp_dir.path();
985
986        // Driver with single member
987        let driver_spec = Spec {
988            id: "2026-01-24-011-def".to_string(),
989            frontmatter: crate::spec::SpecFrontmatter {
990                status: SpecStatus::InProgress,
991                ..Default::default()
992            },
993            title: Some("Driver".to_string()),
994            body: "# Driver\n\nBody.".to_string(),
995        };
996
997        let driver_path = specs_dir.join("2026-01-24-011-def.md");
998        driver_spec.save(&driver_path).unwrap();
999
1000        // Single member
1001        let member = Spec::parse(
1002            "2026-01-24-011-def.1",
1003            r#"---
1004status: completed
1005---
1006# Member 1
1007"#,
1008        )
1009        .unwrap();
1010
1011        let all_specs = vec![driver_spec, member];
1012
1013        // Auto-complete should succeed
1014        let result =
1015            auto_complete_driver_if_ready("2026-01-24-011-def.1", &all_specs, specs_dir).unwrap();
1016        assert!(
1017            result,
1018            "Driver should be auto-completed when single member completes"
1019        );
1020
1021        // Verify driver was updated
1022        let updated_driver = Spec::load(&driver_path).unwrap();
1023        assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
1024        assert_eq!(
1025            updated_driver.frontmatter.model,
1026            Some("auto-completed".to_string())
1027        );
1028    }
1029
1030    #[test]
1031    fn test_compare_spec_ids_member_numeric_sort() {
1032        use std::cmp::Ordering;
1033
1034        // Test numeric sorting for member specs
1035        assert_eq!(
1036            compare_spec_ids("2026-01-25-00y-abc.2", "2026-01-25-00y-abc.10"),
1037            Ordering::Less
1038        );
1039        assert_eq!(
1040            compare_spec_ids("2026-01-25-00y-abc.10", "2026-01-25-00y-abc.2"),
1041            Ordering::Greater
1042        );
1043        assert_eq!(
1044            compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc.1"),
1045            Ordering::Equal
1046        );
1047
1048        // Test with larger numbers
1049        assert_eq!(
1050            compare_spec_ids("2026-01-25-00y-abc.99", "2026-01-25-00y-abc.100"),
1051            Ordering::Less
1052        );
1053    }
1054
1055    #[test]
1056    fn test_compare_spec_ids_different_drivers() {
1057        use std::cmp::Ordering;
1058
1059        // Different driver IDs should use lexicographic comparison
1060        assert_eq!(
1061            compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-def.1"),
1062            Ordering::Less
1063        );
1064        assert_eq!(
1065            compare_spec_ids("2026-01-25-00y-def.1", "2026-01-25-00y-abc.1"),
1066            Ordering::Greater
1067        );
1068    }
1069
1070    #[test]
1071    fn test_compare_spec_ids_non_member_specs() {
1072        use std::cmp::Ordering;
1073
1074        // Non-member specs should use lexicographic comparison
1075        assert_eq!(
1076            compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-def"),
1077            Ordering::Less
1078        );
1079        assert_eq!(
1080            compare_spec_ids("2026-01-25-00y-def", "2026-01-25-00y-abc"),
1081            Ordering::Greater
1082        );
1083    }
1084
1085    #[test]
1086    fn test_compare_spec_ids_driver_vs_member() {
1087        use std::cmp::Ordering;
1088
1089        // Driver should come before its members
1090        assert_eq!(
1091            compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-abc.1"),
1092            Ordering::Less
1093        );
1094        assert_eq!(
1095            compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc"),
1096            Ordering::Greater
1097        );
1098    }
1099
1100    #[test]
1101    fn test_compare_spec_ids_sorting_list() {
1102        // Test sorting a list of specs with mixed member numbers
1103        let mut ids = vec![
1104            "2026-01-25-00y-abc.10",
1105            "2026-01-25-00y-abc.2",
1106            "2026-01-25-00y-abc.1",
1107            "2026-01-25-00y-abc",
1108            "2026-01-25-00y-abc.3",
1109        ];
1110
1111        ids.sort_by(|a, b| compare_spec_ids(a, b));
1112
1113        assert_eq!(
1114            ids,
1115            vec![
1116                "2026-01-25-00y-abc",
1117                "2026-01-25-00y-abc.1",
1118                "2026-01-25-00y-abc.2",
1119                "2026-01-25-00y-abc.3",
1120                "2026-01-25-00y-abc.10",
1121            ]
1122        );
1123    }
1124
1125    #[test]
1126    fn test_compare_spec_ids_base36_sequence_rollover() {
1127        use std::cmp::Ordering;
1128
1129        // Test that base36 sequence 010 (decimal 36) sorts after 00z (decimal 35)
1130        assert_eq!(
1131            compare_spec_ids("2026-01-25-010-xxx", "2026-01-25-00z-yyy"),
1132            Ordering::Greater
1133        );
1134        assert_eq!(
1135            compare_spec_ids("2026-01-25-00z-yyy", "2026-01-25-010-xxx"),
1136            Ordering::Less
1137        );
1138
1139        // Test sorting a list with base36 rollover
1140        let mut ids = vec![
1141            "2026-01-25-010-aaa",
1142            "2026-01-25-00a-bbb",
1143            "2026-01-25-00z-ccc",
1144            "2026-01-25-001-ddd",
1145            "2026-01-25-011-eee",
1146        ];
1147
1148        ids.sort_by(|a, b| compare_spec_ids(a, b));
1149
1150        assert_eq!(
1151            ids,
1152            vec![
1153                "2026-01-25-001-ddd", // 1
1154                "2026-01-25-00a-bbb", // 10
1155                "2026-01-25-00z-ccc", // 35
1156                "2026-01-25-010-aaa", // 36
1157                "2026-01-25-011-eee", // 37
1158            ]
1159        );
1160    }
1161
1162    #[test]
1163    fn test_driver_auto_completion_with_two_members() {
1164        use tempfile::TempDir;
1165
1166        let temp_dir = TempDir::new().unwrap();
1167        let specs_dir = temp_dir.path();
1168
1169        // Create a driver spec that starts as pending
1170        let driver_spec = Spec {
1171            id: "2026-01-24-012-ghi".to_string(),
1172            frontmatter: crate::spec::SpecFrontmatter {
1173                status: SpecStatus::Pending,
1174                ..Default::default()
1175            },
1176            title: Some("Driver spec with 2 members".to_string()),
1177            body: "# Driver\n\nBody.".to_string(),
1178        };
1179
1180        let driver_path = specs_dir.join("2026-01-24-012-ghi.md");
1181        driver_spec.save(&driver_path).unwrap();
1182
1183        // Create first member (initially pending)
1184        let _member1 = Spec::parse(
1185            "2026-01-24-012-ghi.1",
1186            r#"---
1187status: pending
1188---
1189# Member 1
1190"#,
1191        )
1192        .unwrap();
1193
1194        // Create second member (initially pending)
1195        let member2 = Spec::parse(
1196            "2026-01-24-012-ghi.2",
1197            r#"---
1198status: pending
1199---
1200# Member 2
1201"#,
1202        )
1203        .unwrap();
1204
1205        // Step 1: First member starts - should mark driver as in_progress
1206        mark_driver_in_progress(specs_dir, "2026-01-24-012-ghi.1").unwrap();
1207
1208        let updated_driver = Spec::load(&driver_path).unwrap();
1209        assert_eq!(
1210            updated_driver.frontmatter.status,
1211            SpecStatus::InProgress,
1212            "Driver should be in_progress after first member starts"
1213        );
1214
1215        // Step 2: First member completes - driver should NOT complete yet
1216        let member1_completed = Spec::parse(
1217            "2026-01-24-012-ghi.1",
1218            r#"---
1219status: completed
1220---
1221# Member 1
1222"#,
1223        )
1224        .unwrap();
1225
1226        let all_specs = vec![
1227            updated_driver.clone(),
1228            member1_completed.clone(),
1229            member2.clone(),
1230        ];
1231        let result =
1232            auto_complete_driver_if_ready("2026-01-24-012-ghi.1", &all_specs, specs_dir).unwrap();
1233        assert!(
1234            !result,
1235            "Driver should NOT auto-complete when first member is done but second is pending"
1236        );
1237
1238        let still_in_progress = Spec::load(&driver_path).unwrap();
1239        assert_eq!(
1240            still_in_progress.frontmatter.status,
1241            SpecStatus::InProgress,
1242            "Driver should still be in_progress"
1243        );
1244
1245        // Step 3: Second member completes - driver SHOULD auto-complete
1246        let member2_completed = Spec::parse(
1247            "2026-01-24-012-ghi.2",
1248            r#"---
1249status: completed
1250---
1251# Member 2
1252"#,
1253        )
1254        .unwrap();
1255
1256        let all_specs = vec![
1257            still_in_progress.clone(),
1258            member1_completed.clone(),
1259            member2_completed.clone(),
1260        ];
1261        let result =
1262            auto_complete_driver_if_ready("2026-01-24-012-ghi.2", &all_specs, specs_dir).unwrap();
1263        assert!(
1264            result,
1265            "Driver should auto-complete when all members are completed"
1266        );
1267
1268        let final_driver = Spec::load(&driver_path).unwrap();
1269        assert_eq!(
1270            final_driver.frontmatter.status,
1271            SpecStatus::Completed,
1272            "Driver should be completed after all members complete"
1273        );
1274        assert_eq!(
1275            final_driver.frontmatter.model,
1276            Some("auto-completed".to_string()),
1277            "Driver should have auto-completed model"
1278        );
1279        assert!(
1280            final_driver.frontmatter.completed_at.is_some(),
1281            "Driver should have completed_at timestamp"
1282        );
1283    }
1284}