ralph/cli/queue/graph/
mod.rs1mod dot;
19mod json;
20mod list;
21mod shared;
22mod tree;
23
24use anyhow::{Result, anyhow};
25use clap::{Args, ValueEnum};
26
27use crate::cli::load_and_validate_queues_read_only;
28use crate::config::Resolved;
29use crate::queue::find_task_across;
30use crate::queue::graph::{GraphFormat, build_graph, find_critical_paths};
31
32#[derive(Args)]
34#[command(
35 about = "Visualize task dependencies as a graph",
36 after_long_help = "Examples:\n ralph queue graph\n ralph queue graph --task RQ-0001\n ralph queue graph --format dot\n ralph queue graph --critical\n ralph queue graph --reverse --task RQ-0001"
37)]
38pub struct QueueGraphArgs {
39 #[arg(long, short)]
41 pub task: Option<String>,
42
43 #[arg(long, short, value_enum, default_value_t = GraphFormatArg::Tree)]
45 pub format: GraphFormatArg,
46
47 #[arg(long)]
49 pub include_done: bool,
50
51 #[arg(long, short)]
53 pub critical: bool,
54
55 #[arg(long, short)]
57 pub reverse: bool,
58}
59
60#[derive(Clone, Copy, Debug, ValueEnum)]
61#[clap(rename_all = "snake_case")]
62pub enum GraphFormatArg {
63 Tree,
65 Dot,
67 Json,
69 List,
71}
72
73impl From<GraphFormatArg> for GraphFormat {
74 fn from(arg: GraphFormatArg) -> Self {
75 match arg {
76 GraphFormatArg::Tree => GraphFormat::Tree,
77 GraphFormatArg::Dot => GraphFormat::Dot,
78 GraphFormatArg::Json => GraphFormat::Json,
79 GraphFormatArg::List => GraphFormat::List,
80 }
81 }
82}
83
84pub(crate) fn handle(resolved: &Resolved, args: QueueGraphArgs) -> Result<()> {
85 let (queue_file, done_file) = load_and_validate_queues_read_only(resolved, true)?;
86 let done_ref = done_file
87 .as_ref()
88 .filter(|d| !d.tasks.is_empty() || resolved.done_path.exists());
89
90 let graph = build_graph(&queue_file, done_ref);
91 if graph.is_empty() {
92 println!("No tasks found in queue.");
93 return Ok(());
94 }
95
96 let critical_paths = if args.critical || matches!(args.format, GraphFormatArg::Tree) {
97 find_critical_paths(&graph)
98 } else {
99 Vec::new()
100 };
101
102 match args
103 .task
104 .as_deref()
105 .map(str::trim)
106 .filter(|id| !id.is_empty())
107 {
108 Some(task_id) => {
109 let task = find_task_across(&queue_file, done_ref, task_id)
110 .ok_or_else(|| anyhow!("{}", crate::error_messages::task_not_found(task_id)))?;
111 render_focused_graph(&graph, task, task_id, &critical_paths, &args)
112 }
113 None => render_full_graph(&graph, &critical_paths, &args),
114 }
115}
116
117fn render_focused_graph(
118 graph: &crate::queue::graph::DependencyGraph,
119 task: &crate::contracts::Task,
120 task_id: &str,
121 critical_paths: &[crate::queue::graph::CriticalPathResult],
122 args: &QueueGraphArgs,
123) -> Result<()> {
124 match args.format {
125 GraphFormatArg::Tree => tree::render_task_tree(
126 graph,
127 task_id,
128 critical_paths,
129 args.reverse,
130 args.include_done,
131 ),
132 GraphFormatArg::Dot => {
133 dot::render_task_dot(graph, Some(task_id), args.reverse, args.include_done)
134 }
135 GraphFormatArg::Json => {
136 json::render_task_json(graph, task, critical_paths, args.reverse, args.include_done)
137 }
138 GraphFormatArg::List => list::render_task_list(
139 graph,
140 task_id,
141 critical_paths,
142 args.reverse,
143 args.include_done,
144 ),
145 }
146}
147
148fn render_full_graph(
149 graph: &crate::queue::graph::DependencyGraph,
150 critical_paths: &[crate::queue::graph::CriticalPathResult],
151 args: &QueueGraphArgs,
152) -> Result<()> {
153 match args.format {
154 GraphFormatArg::Tree => tree::render_full_tree(graph, critical_paths, args.include_done),
155 GraphFormatArg::Dot => dot::render_task_dot(graph, None, args.reverse, args.include_done),
156 GraphFormatArg::Json => json::render_full_json(graph, critical_paths, args.include_done),
157 GraphFormatArg::List => list::render_full_list(graph, critical_paths, args.include_done),
158 }
159}