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    if skip {
383        return Ok(());
384    }
385
386    if let Some(driver_id) = extract_driver_id(member_id) {
387        // Try to load the driver spec
388        let driver_path = specs_dir.join(format!("{}.md", driver_id));
389        if driver_path.exists() {
390            let mut driver = Spec::load(&driver_path)?;
391            if driver.frontmatter.status == SpecStatus::Pending {
392                driver.frontmatter.status = SpecStatus::InProgress;
393                driver.save(&driver_path)?;
394            }
395        }
396    }
397    Ok(())
398}
399
400/// Mark a driver spec as in_progress when one of its members starts work.
401///
402/// Convenience wrapper that always marks the driver. For conditional marking,
403/// use `mark_driver_in_progress_conditional`.
404pub fn mark_driver_in_progress(specs_dir: &Path, member_id: &str) -> Result<()> {
405    mark_driver_in_progress_conditional(specs_dir, member_id, false)
406}
407
408/// Auto-complete a driver spec if all its members are now completed.
409///
410/// When a member spec completes, check if all other members are also completed.
411/// If so, and the driver is in `InProgress` status, automatically mark the driver
412/// as `Completed` with completion timestamp.
413///
414/// # Arguments
415///
416/// * `member_id` - The ID of the member spec that just completed
417/// * `all_specs` - All available specs
418/// * `specs_dir` - Path to the specs directory
419///
420/// # Returns
421///
422/// Returns `Ok(true)` if the driver was auto-completed.
423/// Returns `Ok(false)` if the driver was not ready for completion.
424/// Returns `Err` if file I/O fails.
425///
426/// # Examples
427///
428/// ```ignore
429/// if auto_complete_driver_if_ready("2026-01-25-00y-abc.2", &specs, &specs_dir)? {
430///     println!("Driver was auto-completed!");
431/// }
432/// ```
433pub fn auto_complete_driver_if_ready(
434    member_id: &str,
435    all_specs: &[Spec],
436    specs_dir: &Path,
437) -> Result<bool> {
438    // Only member specs can trigger driver auto-completion
439    let Some(driver_id) = extract_driver_id(member_id) else {
440        return Ok(false);
441    };
442
443    // Find the driver spec
444    let Some(driver_spec) = all_specs.iter().find(|s| s.id == driver_id) else {
445        return Ok(false);
446    };
447
448    // Only auto-complete if driver is in_progress or pending
449    // (Pending is allowed for chain mode where drivers aren't marked InProgress)
450    if driver_spec.frontmatter.status != SpecStatus::InProgress
451        && driver_spec.frontmatter.status != SpecStatus::Pending
452    {
453        return Ok(false);
454    }
455
456    // Check if all members are completed
457    if !all_members_completed(&driver_id, all_specs) {
458        return Ok(false);
459    }
460
461    // All members are completed, so auto-complete the driver
462    let driver_path = specs_dir.join(format!("{}.md", driver_id));
463    let mut driver = Spec::load(&driver_path)?;
464
465    driver.frontmatter.status = SpecStatus::Completed;
466    driver.frontmatter.completed_at = Some(
467        chrono::Local::now()
468            .format("%Y-%m-%dT%H:%M:%SZ")
469            .to_string(),
470    );
471    driver.frontmatter.model = Some("auto-completed".to_string());
472
473    driver.save(&driver_path)?;
474
475    Ok(true)
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn test_is_member_of() {
484        assert!(is_member_of("2026-01-22-001-x7m.1", "2026-01-22-001-x7m"));
485        assert!(is_member_of("2026-01-22-001-x7m.2.1", "2026-01-22-001-x7m"));
486        assert!(!is_member_of("2026-01-22-001-x7m", "2026-01-22-001-x7m"));
487        assert!(!is_member_of("2026-01-22-002-y8n", "2026-01-22-001-x7m"));
488    }
489
490    #[test]
491    fn test_extract_driver_id() {
492        assert_eq!(
493            extract_driver_id("2026-01-22-001-x7m.1"),
494            Some("2026-01-22-001-x7m".to_string())
495        );
496        assert_eq!(
497            extract_driver_id("2026-01-22-001-x7m.2.1"),
498            Some("2026-01-22-001-x7m".to_string())
499        );
500        assert_eq!(extract_driver_id("2026-01-22-001-x7m"), None);
501        assert_eq!(extract_driver_id("2026-01-22-001-x7m.abc"), None);
502    }
503
504    #[test]
505    fn test_extract_member_number() {
506        assert_eq!(extract_member_number("2026-01-24-001-abc.1"), Some(1));
507        assert_eq!(extract_member_number("2026-01-24-001-abc.3"), Some(3));
508        assert_eq!(extract_member_number("2026-01-24-001-abc.10"), Some(10));
509        assert_eq!(extract_member_number("2026-01-24-001-abc.3.2"), Some(3));
510        assert_eq!(extract_member_number("2026-01-24-001-abc"), None);
511        assert_eq!(extract_member_number("2026-01-24-001-abc.abc"), None);
512    }
513
514    #[test]
515    fn test_all_prior_siblings_completed() {
516        // Test spec for member .1 with no prior siblings
517        let spec1 = Spec::parse(
518            "2026-01-24-001-abc.1",
519            r#"---
520status: pending
521---
522# Test
523"#,
524        )
525        .unwrap();
526
527        // Should be ready since it has no prior siblings
528        assert!(all_prior_siblings_completed(&spec1.id, &[]));
529
530        // Test spec for member .3 with completed prior siblings
531        let spec_prior_1 = Spec::parse(
532            "2026-01-24-001-abc.1",
533            r#"---
534status: completed
535---
536# Test
537"#,
538        )
539        .unwrap();
540
541        let spec_prior_2 = Spec::parse(
542            "2026-01-24-001-abc.2",
543            r#"---
544status: completed
545---
546# Test
547"#,
548        )
549        .unwrap();
550
551        let spec3 = Spec::parse(
552            "2026-01-24-001-abc.3",
553            r#"---
554status: pending
555---
556# Test
557"#,
558        )
559        .unwrap();
560
561        let all_specs = vec![spec_prior_1, spec_prior_2, spec3.clone()];
562        assert!(all_prior_siblings_completed(&spec3.id, &all_specs));
563    }
564
565    #[test]
566    fn test_all_prior_siblings_completed_missing() {
567        // Test spec for member .3 with missing prior sibling
568        let spec_prior_1 = Spec::parse(
569            "2026-01-24-001-abc.1",
570            r#"---
571status: completed
572---
573# Test
574"#,
575        )
576        .unwrap();
577
578        let spec3 = Spec::parse(
579            "2026-01-24-001-abc.3",
580            r#"---
581status: pending
582---
583# Test
584"#,
585        )
586        .unwrap();
587
588        // Only spec .1 exists, .2 is missing
589        let all_specs = vec![spec_prior_1, spec3.clone()];
590        assert!(!all_prior_siblings_completed(&spec3.id, &all_specs));
591    }
592
593    #[test]
594    fn test_all_prior_siblings_completed_not_completed() {
595        // Test spec for member .2 with incomplete prior sibling
596        let spec_prior_1 = Spec::parse(
597            "2026-01-24-001-abc.1",
598            r#"---
599status: pending
600---
601# Test
602"#,
603        )
604        .unwrap();
605
606        let spec2 = Spec::parse(
607            "2026-01-24-001-abc.2",
608            r#"---
609status: pending
610---
611# Test
612"#,
613        )
614        .unwrap();
615
616        let all_specs = vec![spec_prior_1, spec2.clone()];
617        assert!(!all_prior_siblings_completed(&spec2.id, &all_specs));
618    }
619
620    #[test]
621    fn test_mark_driver_in_progress_when_member_starts() {
622        use tempfile::TempDir;
623
624        let temp_dir = TempDir::new().unwrap();
625        let specs_dir = temp_dir.path();
626
627        // Create a driver spec that is pending
628        let driver_spec = Spec {
629            id: "2026-01-24-001-abc".to_string(),
630            frontmatter: crate::spec::SpecFrontmatter {
631                status: SpecStatus::Pending,
632                ..Default::default()
633            },
634            title: Some("Driver spec".to_string()),
635            body: "# Driver spec\n\nBody content.".to_string(),
636        };
637
638        let driver_path = specs_dir.join("2026-01-24-001-abc.md");
639        driver_spec.save(&driver_path).unwrap();
640
641        // Mark driver as in_progress when member starts
642        mark_driver_in_progress(specs_dir, "2026-01-24-001-abc.1").unwrap();
643
644        // Verify driver status was updated to in_progress
645        let updated_driver = Spec::load(&driver_path).unwrap();
646        assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
647    }
648
649    #[test]
650    fn test_mark_driver_in_progress_skips_if_already_in_progress() {
651        use tempfile::TempDir;
652
653        let temp_dir = TempDir::new().unwrap();
654        let specs_dir = temp_dir.path();
655
656        // Create a driver spec that is already in_progress
657        let driver_spec = Spec {
658            id: "2026-01-24-002-def".to_string(),
659            frontmatter: crate::spec::SpecFrontmatter {
660                status: SpecStatus::InProgress,
661                ..Default::default()
662            },
663            title: Some("Driver spec".to_string()),
664            body: "# Driver spec\n\nBody content.".to_string(),
665        };
666
667        let driver_path = specs_dir.join("2026-01-24-002-def.md");
668        driver_spec.save(&driver_path).unwrap();
669
670        // Try to mark driver as in_progress
671        mark_driver_in_progress(specs_dir, "2026-01-24-002-def.1").unwrap();
672
673        // Verify driver status is still in_progress (not changed)
674        let updated_driver = Spec::load(&driver_path).unwrap();
675        assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
676    }
677
678    #[test]
679    fn test_mark_driver_in_progress_nonexistent_driver() {
680        use tempfile::TempDir;
681
682        let temp_dir = TempDir::new().unwrap();
683        let specs_dir = temp_dir.path();
684
685        // Try to mark driver as in_progress when driver doesn't exist
686        // Should not error, just skip
687        mark_driver_in_progress(specs_dir, "2026-01-24-003-ghi.1").unwrap();
688    }
689
690    #[test]
691    fn test_get_incomplete_members() {
692        // Driver with multiple incomplete members
693        let driver = Spec::parse(
694            "2026-01-24-005-mno",
695            r#"---
696status: in_progress
697---
698# Driver
699"#,
700        )
701        .unwrap();
702
703        let member1 = Spec::parse(
704            "2026-01-24-005-mno.1",
705            r#"---
706status: completed
707---
708# Member 1
709"#,
710        )
711        .unwrap();
712
713        let member2 = Spec::parse(
714            "2026-01-24-005-mno.2",
715            r#"---
716status: pending
717---
718# Member 2
719"#,
720        )
721        .unwrap();
722
723        let member3 = Spec::parse(
724            "2026-01-24-005-mno.3",
725            r#"---
726status: in_progress
727---
728# Member 3
729"#,
730        )
731        .unwrap();
732
733        let all_specs = vec![driver.clone(), member1, member2, member3];
734        let incomplete = get_incomplete_members(&driver.id, &all_specs);
735        assert_eq!(incomplete.len(), 2);
736        assert!(incomplete.contains(&"2026-01-24-005-mno.2".to_string()));
737        assert!(incomplete.contains(&"2026-01-24-005-mno.3".to_string()));
738    }
739
740    #[test]
741    fn test_auto_complete_driver_not_member_spec() {
742        use tempfile::TempDir;
743
744        let temp_dir = TempDir::new().unwrap();
745        let specs_dir = temp_dir.path();
746
747        // A non-member spec should not trigger auto-completion
748        let driver_spec = Spec::parse(
749            "2026-01-24-006-pqr",
750            r#"---
751status: in_progress
752---
753# Driver spec
754"#,
755        )
756        .unwrap();
757
758        let result =
759            auto_complete_driver_if_ready("2026-01-24-006-pqr", &[driver_spec], specs_dir).unwrap();
760        assert!(
761            !result,
762            "Non-member spec should not trigger auto-completion"
763        );
764    }
765
766    #[test]
767    fn test_auto_complete_driver_when_already_completed() {
768        use tempfile::TempDir;
769
770        let temp_dir = TempDir::new().unwrap();
771        let specs_dir = temp_dir.path();
772
773        // Create a driver spec that is already completed
774        let driver_spec = Spec::parse(
775            "2026-01-24-007-stu",
776            r#"---
777status: completed
778---
779# Driver spec
780"#,
781        )
782        .unwrap();
783
784        let member_spec = Spec::parse(
785            "2026-01-24-007-stu.1",
786            r#"---
787status: completed
788---
789# Member 1
790"#,
791        )
792        .unwrap();
793
794        let all_specs = vec![driver_spec, member_spec];
795        let result =
796            auto_complete_driver_if_ready("2026-01-24-007-stu.1", &all_specs, specs_dir).unwrap();
797        assert!(
798            !result,
799            "Driver already completed should not be re-completed"
800        );
801    }
802
803    #[test]
804    fn test_auto_complete_driver_from_pending() {
805        use std::fs;
806        use tempfile::TempDir;
807
808        let temp_dir = TempDir::new().unwrap();
809        let specs_dir = temp_dir.path();
810
811        // Create driver spec file (pending status)
812        let driver_content = r#"---
813status: pending
814---
815# Driver spec
816"#;
817        fs::write(specs_dir.join("2026-01-24-009-xyz.md"), driver_content).unwrap();
818
819        // Parse specs for all_specs
820        let driver_spec = Spec::parse("2026-01-24-009-xyz", driver_content).unwrap();
821
822        let member_spec = Spec::parse(
823            "2026-01-24-009-xyz.1",
824            r#"---
825status: completed
826---
827# Member 1
828"#,
829        )
830        .unwrap();
831
832        let all_specs = vec![driver_spec, member_spec];
833
834        // Auto-complete should succeed for pending driver (chain mode scenario)
835        let result =
836            auto_complete_driver_if_ready("2026-01-24-009-xyz.1", &all_specs, specs_dir).unwrap();
837        assert!(
838            result,
839            "Pending driver should be auto-completed when all members are done (chain mode)"
840        );
841
842        // Verify driver is now completed
843        let updated_driver = Spec::load(&specs_dir.join("2026-01-24-009-xyz.md")).unwrap();
844        assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
845    }
846
847    #[test]
848    fn test_auto_complete_driver_incomplete_members() {
849        use tempfile::TempDir;
850
851        let temp_dir = TempDir::new().unwrap();
852        let specs_dir = temp_dir.path();
853
854        // Create a driver spec that is in_progress
855        let driver_spec = Spec {
856            id: "2026-01-24-008-vwx".to_string(),
857            frontmatter: crate::spec::SpecFrontmatter {
858                status: SpecStatus::InProgress,
859                ..Default::default()
860            },
861            title: Some("Driver".to_string()),
862            body: "# Driver\n\nBody.".to_string(),
863        };
864
865        let driver_path = specs_dir.join("2026-01-24-008-vwx.md");
866        driver_spec.save(&driver_path).unwrap();
867
868        // Create member specs where not all are completed
869        let member1 = Spec::parse(
870            "2026-01-24-008-vwx.1",
871            r#"---
872status: completed
873---
874# Member 1
875"#,
876        )
877        .unwrap();
878
879        let member2 = Spec::parse(
880            "2026-01-24-008-vwx.2",
881            r#"---
882status: in_progress
883---
884# Member 2
885"#,
886        )
887        .unwrap();
888
889        let all_specs = vec![driver_spec, member1, member2];
890        let result =
891            auto_complete_driver_if_ready("2026-01-24-008-vwx.1", &all_specs, specs_dir).unwrap();
892        assert!(
893            !result,
894            "Driver should not complete when members are incomplete"
895        );
896    }
897
898    #[test]
899    fn test_auto_complete_driver_success() {
900        use tempfile::TempDir;
901
902        let temp_dir = TempDir::new().unwrap();
903        let specs_dir = temp_dir.path();
904
905        // Create a driver spec that is in_progress
906        let driver_spec = Spec {
907            id: "2026-01-24-009-yz0".to_string(),
908            frontmatter: crate::spec::SpecFrontmatter {
909                status: SpecStatus::InProgress,
910                ..Default::default()
911            },
912            title: Some("Driver".to_string()),
913            body: "# Driver\n\nBody.".to_string(),
914        };
915
916        let driver_path = specs_dir.join("2026-01-24-009-yz0.md");
917        driver_spec.save(&driver_path).unwrap();
918
919        // Create member specs where all are completed
920        let member1 = Spec::parse(
921            "2026-01-24-009-yz0.1",
922            r#"---
923status: completed
924---
925# Member 1
926"#,
927        )
928        .unwrap();
929
930        let member2 = Spec::parse(
931            "2026-01-24-009-yz0.2",
932            r#"---
933status: completed
934---
935# Member 2
936"#,
937        )
938        .unwrap();
939
940        let all_specs = vec![driver_spec, member1, member2];
941
942        // Auto-complete should succeed
943        let result =
944            auto_complete_driver_if_ready("2026-01-24-009-yz0.2", &all_specs, specs_dir).unwrap();
945        assert!(
946            result,
947            "Driver should be auto-completed when all members are completed"
948        );
949
950        // Verify driver was updated
951        let updated_driver = Spec::load(&driver_path).unwrap();
952        assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
953        assert_eq!(
954            updated_driver.frontmatter.model,
955            Some("auto-completed".to_string())
956        );
957        assert!(updated_driver.frontmatter.completed_at.is_some());
958    }
959
960    #[test]
961    fn test_auto_complete_driver_nonexistent_driver() {
962        use tempfile::TempDir;
963
964        let temp_dir = TempDir::new().unwrap();
965        let specs_dir = temp_dir.path();
966
967        // Try to auto-complete when driver doesn't exist
968        let all_specs = vec![];
969        let result =
970            auto_complete_driver_if_ready("2026-01-24-010-abc.1", &all_specs, specs_dir).unwrap();
971        assert!(
972            !result,
973            "Should return false when driver spec doesn't exist"
974        );
975    }
976
977    #[test]
978    fn test_auto_complete_driver_single_member() {
979        use tempfile::TempDir;
980
981        let temp_dir = TempDir::new().unwrap();
982        let specs_dir = temp_dir.path();
983
984        // Driver with single member
985        let driver_spec = Spec {
986            id: "2026-01-24-011-def".to_string(),
987            frontmatter: crate::spec::SpecFrontmatter {
988                status: SpecStatus::InProgress,
989                ..Default::default()
990            },
991            title: Some("Driver".to_string()),
992            body: "# Driver\n\nBody.".to_string(),
993        };
994
995        let driver_path = specs_dir.join("2026-01-24-011-def.md");
996        driver_spec.save(&driver_path).unwrap();
997
998        // Single member
999        let member = Spec::parse(
1000            "2026-01-24-011-def.1",
1001            r#"---
1002status: completed
1003---
1004# Member 1
1005"#,
1006        )
1007        .unwrap();
1008
1009        let all_specs = vec![driver_spec, member];
1010
1011        // Auto-complete should succeed
1012        let result =
1013            auto_complete_driver_if_ready("2026-01-24-011-def.1", &all_specs, specs_dir).unwrap();
1014        assert!(
1015            result,
1016            "Driver should be auto-completed when single member completes"
1017        );
1018
1019        // Verify driver was updated
1020        let updated_driver = Spec::load(&driver_path).unwrap();
1021        assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
1022        assert_eq!(
1023            updated_driver.frontmatter.model,
1024            Some("auto-completed".to_string())
1025        );
1026    }
1027
1028    #[test]
1029    fn test_compare_spec_ids_member_numeric_sort() {
1030        use std::cmp::Ordering;
1031
1032        // Test numeric sorting for member specs
1033        assert_eq!(
1034            compare_spec_ids("2026-01-25-00y-abc.2", "2026-01-25-00y-abc.10"),
1035            Ordering::Less
1036        );
1037        assert_eq!(
1038            compare_spec_ids("2026-01-25-00y-abc.10", "2026-01-25-00y-abc.2"),
1039            Ordering::Greater
1040        );
1041        assert_eq!(
1042            compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc.1"),
1043            Ordering::Equal
1044        );
1045
1046        // Test with larger numbers
1047        assert_eq!(
1048            compare_spec_ids("2026-01-25-00y-abc.99", "2026-01-25-00y-abc.100"),
1049            Ordering::Less
1050        );
1051    }
1052
1053    #[test]
1054    fn test_compare_spec_ids_different_drivers() {
1055        use std::cmp::Ordering;
1056
1057        // Different driver IDs should use lexicographic comparison
1058        assert_eq!(
1059            compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-def.1"),
1060            Ordering::Less
1061        );
1062        assert_eq!(
1063            compare_spec_ids("2026-01-25-00y-def.1", "2026-01-25-00y-abc.1"),
1064            Ordering::Greater
1065        );
1066    }
1067
1068    #[test]
1069    fn test_compare_spec_ids_non_member_specs() {
1070        use std::cmp::Ordering;
1071
1072        // Non-member specs should use lexicographic comparison
1073        assert_eq!(
1074            compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-def"),
1075            Ordering::Less
1076        );
1077        assert_eq!(
1078            compare_spec_ids("2026-01-25-00y-def", "2026-01-25-00y-abc"),
1079            Ordering::Greater
1080        );
1081    }
1082
1083    #[test]
1084    fn test_compare_spec_ids_driver_vs_member() {
1085        use std::cmp::Ordering;
1086
1087        // Driver should come before its members
1088        assert_eq!(
1089            compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-abc.1"),
1090            Ordering::Less
1091        );
1092        assert_eq!(
1093            compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc"),
1094            Ordering::Greater
1095        );
1096    }
1097
1098    #[test]
1099    fn test_compare_spec_ids_sorting_list() {
1100        // Test sorting a list of specs with mixed member numbers
1101        let mut ids = vec![
1102            "2026-01-25-00y-abc.10",
1103            "2026-01-25-00y-abc.2",
1104            "2026-01-25-00y-abc.1",
1105            "2026-01-25-00y-abc",
1106            "2026-01-25-00y-abc.3",
1107        ];
1108
1109        ids.sort_by(|a, b| compare_spec_ids(a, b));
1110
1111        assert_eq!(
1112            ids,
1113            vec![
1114                "2026-01-25-00y-abc",
1115                "2026-01-25-00y-abc.1",
1116                "2026-01-25-00y-abc.2",
1117                "2026-01-25-00y-abc.3",
1118                "2026-01-25-00y-abc.10",
1119            ]
1120        );
1121    }
1122
1123    #[test]
1124    fn test_compare_spec_ids_base36_sequence_rollover() {
1125        use std::cmp::Ordering;
1126
1127        // Test that base36 sequence 010 (decimal 36) sorts after 00z (decimal 35)
1128        assert_eq!(
1129            compare_spec_ids("2026-01-25-010-xxx", "2026-01-25-00z-yyy"),
1130            Ordering::Greater
1131        );
1132        assert_eq!(
1133            compare_spec_ids("2026-01-25-00z-yyy", "2026-01-25-010-xxx"),
1134            Ordering::Less
1135        );
1136
1137        // Test sorting a list with base36 rollover
1138        let mut ids = vec![
1139            "2026-01-25-010-aaa",
1140            "2026-01-25-00a-bbb",
1141            "2026-01-25-00z-ccc",
1142            "2026-01-25-001-ddd",
1143            "2026-01-25-011-eee",
1144        ];
1145
1146        ids.sort_by(|a, b| compare_spec_ids(a, b));
1147
1148        assert_eq!(
1149            ids,
1150            vec![
1151                "2026-01-25-001-ddd", // 1
1152                "2026-01-25-00a-bbb", // 10
1153                "2026-01-25-00z-ccc", // 35
1154                "2026-01-25-010-aaa", // 36
1155                "2026-01-25-011-eee", // 37
1156            ]
1157        );
1158    }
1159
1160    #[test]
1161    fn test_driver_auto_completion_with_two_members() {
1162        use tempfile::TempDir;
1163
1164        let temp_dir = TempDir::new().unwrap();
1165        let specs_dir = temp_dir.path();
1166
1167        // Create a driver spec that starts as pending
1168        let driver_spec = Spec {
1169            id: "2026-01-24-012-ghi".to_string(),
1170            frontmatter: crate::spec::SpecFrontmatter {
1171                status: SpecStatus::Pending,
1172                ..Default::default()
1173            },
1174            title: Some("Driver spec with 2 members".to_string()),
1175            body: "# Driver\n\nBody.".to_string(),
1176        };
1177
1178        let driver_path = specs_dir.join("2026-01-24-012-ghi.md");
1179        driver_spec.save(&driver_path).unwrap();
1180
1181        // Create first member (initially pending)
1182        let _member1 = Spec::parse(
1183            "2026-01-24-012-ghi.1",
1184            r#"---
1185status: pending
1186---
1187# Member 1
1188"#,
1189        )
1190        .unwrap();
1191
1192        // Create second member (initially pending)
1193        let member2 = Spec::parse(
1194            "2026-01-24-012-ghi.2",
1195            r#"---
1196status: pending
1197---
1198# Member 2
1199"#,
1200        )
1201        .unwrap();
1202
1203        // Step 1: First member starts - should mark driver as in_progress
1204        mark_driver_in_progress(specs_dir, "2026-01-24-012-ghi.1").unwrap();
1205
1206        let updated_driver = Spec::load(&driver_path).unwrap();
1207        assert_eq!(
1208            updated_driver.frontmatter.status,
1209            SpecStatus::InProgress,
1210            "Driver should be in_progress after first member starts"
1211        );
1212
1213        // Step 2: First member completes - driver should NOT complete yet
1214        let member1_completed = Spec::parse(
1215            "2026-01-24-012-ghi.1",
1216            r#"---
1217status: completed
1218---
1219# Member 1
1220"#,
1221        )
1222        .unwrap();
1223
1224        let all_specs = vec![
1225            updated_driver.clone(),
1226            member1_completed.clone(),
1227            member2.clone(),
1228        ];
1229        let result =
1230            auto_complete_driver_if_ready("2026-01-24-012-ghi.1", &all_specs, specs_dir).unwrap();
1231        assert!(
1232            !result,
1233            "Driver should NOT auto-complete when first member is done but second is pending"
1234        );
1235
1236        let still_in_progress = Spec::load(&driver_path).unwrap();
1237        assert_eq!(
1238            still_in_progress.frontmatter.status,
1239            SpecStatus::InProgress,
1240            "Driver should still be in_progress"
1241        );
1242
1243        // Step 3: Second member completes - driver SHOULD auto-complete
1244        let member2_completed = Spec::parse(
1245            "2026-01-24-012-ghi.2",
1246            r#"---
1247status: completed
1248---
1249# Member 2
1250"#,
1251        )
1252        .unwrap();
1253
1254        let all_specs = vec![
1255            still_in_progress.clone(),
1256            member1_completed.clone(),
1257            member2_completed.clone(),
1258        ];
1259        let result =
1260            auto_complete_driver_if_ready("2026-01-24-012-ghi.2", &all_specs, specs_dir).unwrap();
1261        assert!(
1262            result,
1263            "Driver should auto-complete when all members are completed"
1264        );
1265
1266        let final_driver = Spec::load(&driver_path).unwrap();
1267        assert_eq!(
1268            final_driver.frontmatter.status,
1269            SpecStatus::Completed,
1270            "Driver should be completed after all members complete"
1271        );
1272        assert_eq!(
1273            final_driver.frontmatter.model,
1274            Some("auto-completed".to_string()),
1275            "Driver should have auto-completed model"
1276        );
1277        assert!(
1278            final_driver.frontmatter.completed_at.is_some(),
1279            "Driver should have completed_at timestamp"
1280        );
1281    }
1282}