Skip to main content

cortex_runtime/cli/
pathfind_cmd.rs

1//! `cortex pathfind <domain>` — find shortest path between nodes.
2
3use crate::cli::output::{self, Styled};
4use crate::intelligence::cache::MapCache;
5use crate::map::types::PathConstraints;
6use anyhow::{bail, Result};
7
8/// Run the pathfind command.
9pub async fn run(domain: &str, from: u32, to: u32) -> Result<()> {
10    let s = Styled::new();
11
12    // Load cached map
13    let mut cache = MapCache::default_cache()?;
14    let map = match cache.load_map(domain)? {
15        Some(m) => m,
16        None => {
17            if output::is_json() {
18                output::print_json(&serde_json::json!({
19                    "error": "no_map",
20                    "message": format!("No cached map for '{domain}'"),
21                    "hint": format!("Run: cortex map {domain}")
22                }));
23                return Ok(());
24            }
25            bail!("No map found for '{domain}'. Run 'cortex map {domain}' first.");
26        }
27    };
28
29    // Validate node indices
30    let max_node = map.nodes.len() as u32;
31    if from >= max_node {
32        bail!(
33            "Source node {from} doesn't exist in this map (max: {}).",
34            max_node - 1
35        );
36    }
37    if to >= max_node {
38        bail!(
39            "Target node {to} doesn't exist in this map (max: {}).",
40            max_node - 1
41        );
42    }
43
44    let constraints = PathConstraints::default();
45    let path = map.shortest_path(from, to, &constraints);
46
47    if output::is_json() {
48        match &path {
49            Some(p) => {
50                let nodes: Vec<serde_json::Value> = p
51                    .nodes
52                    .iter()
53                    .map(|&idx| {
54                        let url = map.urls.get(idx as usize).cloned().unwrap_or_default();
55                        let node = &map.nodes[idx as usize];
56                        serde_json::json!({
57                            "index": idx,
58                            "url": url,
59                            "page_type": format!("{:?}", node.page_type),
60                        })
61                    })
62                    .collect();
63                output::print_json(&serde_json::json!({
64                    "found": true,
65                    "hops": p.hops,
66                    "total_weight": p.total_weight,
67                    "nodes": nodes,
68                    "required_actions": p.required_actions.len(),
69                }));
70            }
71            None => {
72                output::print_json(&serde_json::json!({
73                    "found": false,
74                    "from": from,
75                    "to": to,
76                }));
77            }
78        }
79        return Ok(());
80    }
81
82    match path {
83        Some(path) => {
84            if !output::is_quiet() {
85                eprintln!("  Path from node {from} to node {to}:");
86                eprintln!("  Hops:   {}", path.hops);
87                eprintln!("  Weight: {:.2}", path.total_weight);
88
89                if path.hops > 20 {
90                    eprintln!();
91                    eprintln!(
92                        "  {} Path has {} hops. This seems unusually long.",
93                        s.warn_sym(),
94                        path.hops
95                    );
96                }
97
98                eprintln!();
99                eprintln!("  Route:");
100                for (i, &node_idx) in path.nodes.iter().enumerate() {
101                    let url = map
102                        .urls
103                        .get(node_idx as usize)
104                        .map(|s| s.as_str())
105                        .unwrap_or("?");
106                    let node = &map.nodes[node_idx as usize];
107                    let arrow = if i < path.nodes.len() - 1 {
108                        " \u{2192}"
109                    } else {
110                        "  "
111                    };
112                    eprintln!("    [{node_idx:>5}] {:?} {url}{arrow}", node.page_type);
113                }
114
115                if !path.required_actions.is_empty() {
116                    eprintln!();
117                    eprintln!("  Required actions:");
118                    for action in &path.required_actions {
119                        eprintln!(
120                            "    At node {}: opcode ({:#04x}, {:#04x})",
121                            action.at_node, action.opcode.category, action.opcode.action
122                        );
123                    }
124                }
125            }
126        }
127        None => {
128            if !output::is_quiet() {
129                eprintln!("  No path found from node {from} to node {to}.");
130                eprintln!("  Try relaxing constraints or checking that both nodes are connected.");
131            }
132        }
133    }
134
135    Ok(())
136}