agentstatedeveloper_cli/commands/
list.rs1use std::collections::HashMap;
5
6use anyhow::Result;
7use clap::{Args, Subcommand};
8use serde_json::{Value, json};
9
10use agentstatedeveloper_core::{EffectDecl, Engine, LedgerEntry, Symbol};
11
12use crate::commands::graph::build_id_map;
13
14use crate::config::Config;
15
16#[derive(Debug, Args)]
17pub struct ListArgs {
18 #[command(subcommand)]
19 pub cmd: ListCmd,
20}
21
22#[derive(Debug, Subcommand)]
23pub enum ListCmd {
24 Symbols {
26 #[arg(long)]
28 lang: Option<String>,
29
30 #[arg(long)]
32 kind: Option<String>,
33
34 #[arg(long)]
36 file: Option<String>,
37 },
38
39 Effects {
41 #[arg(long)]
43 has_declared: bool,
44
45 #[arg(long)]
47 category: Option<String>,
48
49 #[arg(long)]
51 file: Option<String>,
52 },
53
54 Ledger {
56 #[arg(long)]
58 kind: Option<String>,
59 },
60
61 Stats,
63}
64
65fn tree_or_empty(engine: &Engine, prefix: &str) -> Value {
66 engine
67 .repo
68 .get_tree(&engine.ref_name, prefix)
69 .unwrap_or(Value::Object(Default::default()))
70}
71
72fn kind_str(v: &impl serde::Serialize) -> String {
73 serde_json::to_value(v)
74 .ok()
75 .and_then(|j| j.as_str().map(String::from))
76 .unwrap_or_default()
77}
78
79pub fn run(cfg: &Config, args: ListArgs) -> Result<()> {
80 let engine = Engine::open_sqlite(&cfg.db_path)?;
81
82 match args.cmd {
83 ListCmd::Symbols { lang, kind, file } => {
84 let tree = tree_or_empty(&engine, "/asd/v1/index/by-qname");
85 let mut symbols: Vec<Symbol> = tree
86 .as_object()
87 .map(|m| {
88 m.values()
89 .filter_map(|v| serde_json::from_value(v.clone()).ok())
90 .collect()
91 })
92 .unwrap_or_default();
93
94 if let Some(l) = &lang {
95 symbols.retain(|s| &s.language == l);
96 }
97 if let Some(k) = &kind {
98 symbols.retain(|s| &kind_str(&s.kind) == k);
99 }
100 if let Some(f) = &file {
101 symbols.retain(|s| s.file.contains(f.as_str()));
102 }
103
104 symbols.sort_by(|a, b| a.qname.cmp(&b.qname));
105
106 println!(
107 "{}",
108 serde_json::to_string_pretty(&json!({
109 "count": symbols.len(),
110 "symbols": symbols,
111 }))?
112 );
113 }
114
115 ListCmd::Effects {
116 has_declared,
117 category,
118 file,
119 } => {
120 let tree = tree_or_empty(&engine, "/asd/v1/effects");
121 let mut decls: Vec<EffectDecl> = tree
122 .as_object()
123 .map(|m| {
124 m.values()
125 .filter_map(|v| serde_json::from_value(v.clone()).ok())
126 .collect()
127 })
128 .unwrap_or_default();
129
130 if has_declared {
131 decls.retain(|d| !d.declared.is_empty());
132 }
133 if let Some(cat) = &category {
134 decls.retain(|d| {
135 d.declared.iter().any(|e| e.effect.as_str() == cat.as_str())
136 || d.transitive
137 .iter()
138 .any(|t| t.effect.as_str() == cat.as_str())
139 });
140 }
141 if let Some(file_filter) = &file {
142 let id_map = build_id_map(&engine);
144 decls.retain(|d| {
145 id_map
146 .get(&d.symbol_id)
147 .map(|s| s.file.contains(file_filter.as_str()))
148 .unwrap_or(false)
149 });
150 }
151
152 decls.sort_by(|a, b| a.symbol_id.cmp(&b.symbol_id));
153
154 println!(
155 "{}",
156 serde_json::to_string_pretty(&json!({
157 "count": decls.len(),
158 "effects": decls,
159 }))?
160 );
161 }
162
163 ListCmd::Ledger { kind } => {
164 let tree = tree_or_empty(&engine, "/asd/v1/ledger");
165 let mut entries: Vec<LedgerEntry> = Vec::new();
166
167 if let Some(sym_map) = tree.as_object() {
168 for per_symbol in sym_map.values() {
169 if let Some(entry_map) = per_symbol.as_object() {
170 for entry_val in entry_map.values() {
171 if let Ok(e) = serde_json::from_value::<LedgerEntry>(entry_val.clone())
172 {
173 entries.push(e);
174 }
175 }
176 }
177 }
178 }
179
180 if let Some(k) = &kind {
181 entries.retain(|e| &kind_str(&e.kind) == k);
182 }
183
184 entries.sort_by(|a, b| b.created_at.cmp(&a.created_at));
185
186 println!(
187 "{}",
188 serde_json::to_string_pretty(&json!({
189 "count": entries.len(),
190 "entries": entries,
191 }))?
192 );
193 }
194
195 ListCmd::Stats => {
196 let sym_tree = tree_or_empty(&engine, "/asd/v1/index/by-qname");
198 let symbols: Vec<Symbol> = sym_tree
199 .as_object()
200 .map(|m| {
201 m.values()
202 .filter_map(|v| serde_json::from_value(v.clone()).ok())
203 .collect()
204 })
205 .unwrap_or_default();
206
207 let mut by_lang: HashMap<&str, usize> = HashMap::new();
208 let mut by_kind: HashMap<String, usize> = HashMap::new();
209 let mut files: std::collections::HashSet<&str> = std::collections::HashSet::new();
210 for s in &symbols {
211 *by_lang.entry(s.language.as_str()).or_default() += 1;
212 *by_kind.entry(kind_str(&s.kind)).or_default() += 1;
213 files.insert(s.file.as_str());
214 }
215 let mut by_lang: Vec<_> = by_lang
216 .into_iter()
217 .map(|(k, v)| json!({"lang": k, "count": v}))
218 .collect();
219 by_lang.sort_by(|a, b| b["count"].as_u64().cmp(&a["count"].as_u64()));
220 let mut by_kind: Vec<_> = by_kind
221 .into_iter()
222 .map(|(k, v)| json!({"kind": k, "count": v}))
223 .collect();
224 by_kind.sort_by(|a, b| b["count"].as_u64().cmp(&a["count"].as_u64()));
225
226 let eff_tree = tree_or_empty(&engine, "/asd/v1/effects");
228 let decls: Vec<EffectDecl> = eff_tree
229 .as_object()
230 .map(|m| {
231 m.values()
232 .filter_map(|v| serde_json::from_value(v.clone()).ok())
233 .collect()
234 })
235 .unwrap_or_default();
236
237 let with_declared = decls.iter().filter(|d| !d.declared.is_empty()).count();
238 let mut by_category: HashMap<&str, usize> = HashMap::new();
239 let mut by_verification: HashMap<String, usize> = HashMap::new();
240 for d in &decls {
241 for e in &d.declared {
242 *by_category.entry(e.effect.as_str()).or_default() += 1;
243 }
244 for t in &d.transitive {
245 *by_category.entry(t.effect.as_str()).or_default() += 1;
246 }
247 let vs = d
248 .verification
249 .as_ref()
250 .map(|v| kind_str(&v.status))
251 .unwrap_or_else(|| "none".into());
252 *by_verification.entry(vs).or_default() += 1;
253 }
254 let mut by_category: Vec<_> = by_category
255 .into_iter()
256 .map(|(k, v)| json!({"category": k, "count": v}))
257 .collect();
258 by_category.sort_by(|a, b| b["count"].as_u64().cmp(&a["count"].as_u64()));
259 let mut by_verification: Vec<_> = by_verification
260 .into_iter()
261 .map(|(k, v)| json!({"status": k, "count": v}))
262 .collect();
263 by_verification.sort_by(|a, b| b["count"].as_u64().cmp(&a["count"].as_u64()));
264
265 let callees_tree = tree_or_empty(&engine, "/asd/v1/index/callees");
267 let mut total_edges: usize = 0;
268 let mut symbols_with_callees: usize = 0;
269 if let Some(m) = callees_tree.as_object() {
270 for v in m.values() {
271 if let Some(arr) = v.get("callees").and_then(|c| c.as_array()) {
272 if !arr.is_empty() {
273 symbols_with_callees += 1;
274 total_edges += arr.len();
275 }
276 }
277 }
278 }
279 let callers_tree = tree_or_empty(&engine, "/asd/v1/index/callers");
280 let symbols_with_callers = callers_tree
281 .as_object()
282 .map(|m| {
283 m.values()
284 .filter(|v| {
285 v.get("callers")
286 .and_then(|c| c.as_array())
287 .map(|a| !a.is_empty())
288 .unwrap_or(false)
289 })
290 .count()
291 })
292 .unwrap_or(0);
293
294 let ledger_tree = tree_or_empty(&engine, "/asd/v1/ledger");
296 let mut ledger_total: usize = 0;
297 let mut ledger_by_kind: HashMap<String, usize> = HashMap::new();
298 if let Some(sym_map) = ledger_tree.as_object() {
299 for per_symbol in sym_map.values() {
300 if let Some(entry_map) = per_symbol.as_object() {
301 for entry_val in entry_map.values() {
302 if let Ok(e) = serde_json::from_value::<LedgerEntry>(entry_val.clone())
303 {
304 ledger_total += 1;
305 *ledger_by_kind.entry(kind_str(&e.kind)).or_default() += 1;
306 }
307 }
308 }
309 }
310 }
311 let mut ledger_by_kind: Vec<_> = ledger_by_kind
312 .into_iter()
313 .map(|(k, v)| json!({"kind": k, "count": v}))
314 .collect();
315 ledger_by_kind.sort_by(|a, b| b["count"].as_u64().cmp(&a["count"].as_u64()));
316
317 println!(
318 "{}",
319 serde_json::to_string_pretty(&json!({
320 "symbols": {
321 "total": symbols.len(),
322 "files": files.len(),
323 "by_language": by_lang,
324 "by_kind": by_kind,
325 },
326 "effects": {
327 "total_symbols": decls.len(),
328 "with_declared": with_declared,
329 "pure_or_undeclared": decls.len() - with_declared,
330 "by_category": by_category,
331 "by_verification_status": by_verification,
332 },
333 "call_graph": {
334 "total_edges": total_edges,
335 "symbols_with_callees": symbols_with_callees,
336 "symbols_with_callers": symbols_with_callers,
337 },
338 "ledger": {
339 "total": ledger_total,
340 "by_kind": ledger_by_kind,
341 },
342 }))?
343 );
344 }
345 }
346
347 Ok(())
348}