Skip to main content

ralph/cli/queue/graph/
mod.rs

1//! `ralph queue graph` subcommand facade.
2//!
3//! Responsibilities:
4//! - Define CLI arguments for graph rendering.
5//! - Load validated queue data and dispatch to format-specific renderers.
6//! - Keep command-level behavior stable while renderer internals evolve.
7//!
8//! Not handled here:
9//! - Dependency graph construction (see `crate::queue::graph`).
10//! - Format-specific rendering details (see sibling renderer modules).
11//! - Queue file mutation.
12//!
13//! Invariants/assumptions:
14//! - Queue files are already validated before rendering.
15//! - Focus task IDs are trimmed before lookup.
16//! - Rendering writes directly to stdout.
17
18mod 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/// Arguments for `ralph queue graph`.
33#[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    /// Focus on a specific task (show its dependency tree).
40    #[arg(long, short)]
41    pub task: Option<String>,
42
43    /// Output format.
44    #[arg(long, short, value_enum, default_value_t = GraphFormatArg::Tree)]
45    pub format: GraphFormatArg,
46
47    /// Include completed tasks in output.
48    #[arg(long)]
49    pub include_done: bool,
50
51    /// Show only critical path.
52    #[arg(long, short)]
53    pub critical: bool,
54
55    /// Show reverse dependencies (what this task blocks).
56    #[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    /// ASCII art tree (default).
64    Tree,
65    /// Graphviz DOT format.
66    Dot,
67    /// JSON for external tools.
68    Json,
69    /// Flat list with indentation.
70    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}