1pub mod graph;
19pub mod hierarchy;
20pub mod operations;
21pub mod prune;
22pub mod repair;
23pub mod search;
24pub mod size_check;
25pub mod validation;
26
27mod backup;
29mod id;
30mod json_repair;
31mod loader;
32mod save;
33
34pub use graph::*;
36pub use operations::*;
37pub use prune::{PruneOptions, PruneReport, prune_done_tasks};
38pub use repair::*;
39pub use search::{
40 SearchOptions, filter_tasks, fuzzy_search_tasks, search_tasks, search_tasks_with_options,
41};
42pub use size_check::{
43 SizeCheckResult, check_queue_size, count_threshold_or_default, print_size_warning_if_needed,
44 size_threshold_or_default,
45};
46pub use validation::{ValidationWarning, log_warnings, validate_queue, validate_queue_set};
47
48pub use backup::backup_queue;
50pub use id::next_id_across;
51pub use id::{format_id, normalize_prefix};
52pub use json_repair::attempt_json_repair;
53pub use loader::{
54 load_and_validate_queues, load_queue, load_queue_or_default, load_queue_with_repair,
55 load_queue_with_repair_and_validate, repair_and_validate_queues,
56};
57pub use save::save_queue;
58
59use crate::config::Resolved;
60use crate::contracts::QueueFile;
61use crate::lock;
62use anyhow::Result;
63use std::path::Path;
64
65pub fn acquire_queue_lock(repo_root: &Path, label: &str, force: bool) -> Result<lock::DirLock> {
66 let lock_dir = lock::queue_lock_dir(repo_root);
67 lock::acquire_dir_lock(&lock_dir, label, force)
68}
69
70pub fn optional_done_queue<'a>(
71 done_file: &'a QueueFile,
72 done_path: &Path,
73) -> Option<&'a QueueFile> {
74 (!done_file.tasks.is_empty() || done_path.exists()).then_some(done_file)
75}
76
77pub fn with_locked_queue_mutation<T>(
78 resolved: &Resolved,
79 label: &str,
80 operation: impl AsRef<str>,
81 force: bool,
82 mutate: impl FnOnce() -> Result<T>,
83) -> Result<T> {
84 let _queue_lock = acquire_queue_lock(&resolved.repo_root, label, force)?;
85 crate::undo::create_undo_snapshot(resolved, operation.as_ref())?;
86 mutate()
87}
88
89#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::contracts::{Task, TaskStatus};
94 use std::collections::HashMap;
95
96 fn task(id: &str) -> Task {
97 Task {
98 id: id.to_string(),
99 status: TaskStatus::Todo,
100 title: "Test task".to_string(),
101 description: None,
102 priority: Default::default(),
103 tags: vec!["code".to_string()],
104 scope: vec!["crates/ralph".to_string()],
105 evidence: vec!["observed".to_string()],
106 plan: vec!["do thing".to_string()],
107 notes: vec![],
108 request: Some("test request".to_string()),
109 agent: None,
110 created_at: Some("2026-01-18T00:00:00Z".to_string()),
111 updated_at: Some("2026-01-18T00:00:00Z".to_string()),
112 completed_at: None,
113 started_at: None,
114 scheduled_start: None,
115 depends_on: vec![],
116 blocks: vec![],
117 relates_to: vec![],
118 duplicates: None,
119 custom_fields: HashMap::new(),
120 parent_id: None,
121 estimated_minutes: None,
122 actual_minutes: None,
123 }
124 }
125
126 #[test]
127 fn task_defaults_to_medium_priority() {
128 use crate::contracts::TaskPriority;
129 let task = task("RQ-0001");
130 assert_eq!(task.priority, TaskPriority::Medium);
131 }
132
133 #[test]
134 fn priority_ord_implements_correct_ordering() {
135 use crate::contracts::TaskPriority;
136 assert!(TaskPriority::Critical > TaskPriority::High);
137 assert!(TaskPriority::High > TaskPriority::Medium);
138 assert!(TaskPriority::Medium > TaskPriority::Low);
139 }
140
141 #[test]
142 fn attempt_json_repair_fixes_trailing_comma_in_array() {
143 let input = r#"{"tasks": [{"id": "RQ-0001", "tags": ["a", "b",]}]}"#;
144 let repaired = attempt_json_repair(input).expect("should repair");
145 assert!(repaired.contains("\"tags\": [\"a\", \"b\"]"));
146 assert!(!repaired.contains("\"b\","));
147 }
148
149 #[test]
150 fn attempt_json_repair_fixes_trailing_comma_in_object() {
151 let input = r#"{"tasks": [{"id": "RQ-0001", "title": "Test",}]}"#;
152 let repaired = attempt_json_repair(input).expect("should repair");
153 assert!(repaired.contains("\"title\": \"Test\"}"));
154 assert!(!repaired.contains("\"Test\","));
155 }
156
157 #[test]
158 fn attempt_json_repair_returns_none_for_valid_json() {
159 let input = r#"{"tasks": [{"id": "RQ-0001", "title": "Test"}]}"#;
160 assert!(attempt_json_repair(input).is_none());
161 }
162}