agentstatedeveloper_cli/commands/
graph.rs1use std::collections::{HashMap, HashSet, VecDeque};
7use std::time::Instant;
8
9use anyhow::Result;
10use clap::Args;
11use serde_json::{Value, json};
12
13use agentstatedeveloper_core::{AsgIndexStore, Engine, IndexStore, Symbol};
14
15use crate::config::Config;
16
17pub use agentstatedeveloper_core::build_id_map;
25
26#[derive(Debug, Args)]
29pub struct CallersArgs {
30 pub qname: String,
32
33 #[arg(long, default_value = "1")]
35 pub depth: usize,
36
37 #[arg(long)]
39 pub timing: bool,
40}
41
42pub fn run_callers(cfg: &Config, args: CallersArgs) -> Result<()> {
43 let t = AsdTimer::new(args.timing);
44 let mut t = t;
45 let engine = Engine::open_sqlite(&cfg.db_path)?;
46 t.phase("open_engine");
47 let index_store = AsgIndexStore::from_engine(&engine);
48
49 let symbol = index_store
50 .get_symbol_by_qname(&engine.ref_name, &args.qname)?
51 .ok_or_else(|| anyhow::anyhow!("symbol not found: {}", args.qname))?;
52 t.phase("symbol_lookup");
53
54 let id_map = index_store.build_id_map(&engine);
55 t.phase("build_id_map");
56
57 let results = traverse(
58 &engine,
59 &index_store,
60 &symbol.symbol_id,
61 args.depth,
62 Direction::Callers,
63 &id_map,
64 );
65 t.phase("traverse");
66
67 let qid = crate::commands::brief::query_id("callers", &[&args.qname, &args.depth.to_string()]);
68 if cfg.brief {
69 let lines = crate::commands::brief::brief_call_list(&results);
70 println!(
71 "{}",
72 serde_json::to_string_pretty(&json!({
73 "callers": lines,
74 "query_id": qid,
75 }))?
76 );
77 } else {
78 println!(
79 "{}",
80 serde_json::to_string_pretty(&json!({
81 "symbol": args.qname,
82 "depth": args.depth,
83 "count": results.len(),
84 "callers": results,
85 "query_id": qid,
86 }))?
87 );
88 }
89 t.total("callers");
90 Ok(())
91}
92
93#[derive(Debug, Args)]
96pub struct CalleesArgs {
97 pub qname: String,
99
100 #[arg(long, default_value = "1")]
102 pub depth: usize,
103
104 #[arg(long)]
106 pub timing: bool,
107}
108
109pub fn run_callees(cfg: &Config, args: CalleesArgs) -> Result<()> {
110 let t = AsdTimer::new(args.timing);
111 let mut t = t;
112 let engine = Engine::open_sqlite(&cfg.db_path)?;
113 t.phase("open_engine");
114 let index_store = AsgIndexStore::from_engine(&engine);
115
116 let symbol = index_store
117 .get_symbol_by_qname(&engine.ref_name, &args.qname)?
118 .ok_or_else(|| anyhow::anyhow!("symbol not found: {}", args.qname))?;
119 t.phase("symbol_lookup");
120
121 let id_map = index_store.build_id_map(&engine);
122 t.phase("build_id_map");
123
124 let results = traverse(
125 &engine,
126 &index_store,
127 &symbol.symbol_id,
128 args.depth,
129 Direction::Callees,
130 &id_map,
131 );
132 t.phase("traverse");
133
134 let qid = crate::commands::brief::query_id("callees", &[&args.qname, &args.depth.to_string()]);
135 if cfg.brief {
136 let lines = crate::commands::brief::brief_call_list(&results);
137 println!(
138 "{}",
139 serde_json::to_string_pretty(&json!({
140 "callees": lines,
141 "query_id": qid,
142 }))?
143 );
144 } else {
145 println!(
146 "{}",
147 serde_json::to_string_pretty(&json!({
148 "symbol": args.qname,
149 "depth": args.depth,
150 "count": results.len(),
151 "callees": results,
152 "query_id": qid,
153 }))?
154 );
155 }
156 t.total("callees");
157 Ok(())
158}
159
160pub(crate) struct AsdTimer {
163 start: Instant,
164 last: Instant,
165 enabled: bool,
166}
167
168impl AsdTimer {
169 pub(crate) fn new(enabled: bool) -> Self {
170 let now = Instant::now();
171 Self {
172 start: now,
173 last: now,
174 enabled,
175 }
176 }
177 pub(crate) fn phase(&mut self, name: &str) {
178 if self.enabled {
179 let now = Instant::now();
180 eprintln!(
181 "[timing] {:20} {:5.0}ms (total {:5.0}ms)",
182 name,
183 (now - self.last).as_secs_f64() * 1000.0,
184 (now - self.start).as_secs_f64() * 1000.0,
185 );
186 self.last = now;
187 }
188 }
189 pub(crate) fn total(&self, label: &str) {
190 if self.enabled {
191 eprintln!(
192 "[timing] {:20} total {:5.0}ms",
193 label,
194 self.start.elapsed().as_secs_f64() * 1000.0,
195 );
196 }
197 }
198}
199
200enum Direction {
203 Callers,
204 Callees,
205}
206
207fn traverse(
208 engine: &Engine,
209 index_store: &AsgIndexStore<'_>,
210 start_id: &str,
211 max_depth: usize,
212 direction: Direction,
213 id_map: &HashMap<String, Symbol>,
214) -> Vec<Value> {
215 let mut visited: HashSet<String> = HashSet::new();
216 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
217 let mut results: Vec<Value> = Vec::new();
218
219 visited.insert(start_id.to_string());
220 queue.push_back((start_id.to_string(), 0));
221
222 while let Some((sym_id, depth)) = queue.pop_front() {
223 if depth >= max_depth {
224 continue;
225 }
226 let neighbors = match direction {
227 Direction::Callers => index_store
228 .get_callers(&engine.ref_name, &sym_id)
229 .unwrap_or_default(),
230 Direction::Callees => index_store
231 .get_callees(&engine.ref_name, &sym_id)
232 .unwrap_or_default(),
233 };
234 for neighbor_id in neighbors {
235 if visited.contains(&neighbor_id) {
236 continue;
237 }
238 visited.insert(neighbor_id.clone());
239
240 let entry = if let Some(s) = id_map.get(&neighbor_id) {
241 json!({
242 "qname": s.qname,
243 "file": s.file,
244 "line": s.start.line,
245 "depth": depth + 1,
246 })
247 } else {
248 json!({ "symbol_id": neighbor_id, "depth": depth + 1 })
249 };
250 results.push(entry);
251
252 if depth + 1 < max_depth {
253 queue.push_back((neighbor_id, depth + 1));
254 }
255 }
256 }
257
258 results
259}