Skip to main content

agentstatedeveloper_cli/commands/
list.rs

1//! `asd list <symbols|effects|ledger|stats>` — enumerate indexed objects with
2//! optional filters, or show aggregate graph metrics.
3
4use 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    /// List all indexed symbols
25    Symbols {
26        /// Filter by language (rust, python, typescript, go, java, csharp, ruby, kotlin, swift)
27        #[arg(long)]
28        lang: Option<String>,
29
30        /// Filter by symbol kind (function, method, class, module)
31        #[arg(long)]
32        kind: Option<String>,
33
34        /// Filter by file path substring
35        #[arg(long)]
36        file: Option<String>,
37    },
38
39    /// List effect declarations
40    Effects {
41        /// Only show symbols with at least one declared effect
42        #[arg(long)]
43        has_declared: bool,
44
45        /// Filter by effect category (e.g. io.net.out, log, pure)
46        #[arg(long)]
47        category: Option<String>,
48
49        /// Filter by source file path substring (e.g. DriftSynthPool.swift)
50        #[arg(long)]
51        file: Option<String>,
52    },
53
54    /// List all ledger entries
55    Ledger {
56        /// Filter by entry kind (decision, assumption, constraint, rationale, hazard, tradeoff)
57        #[arg(long)]
58        kind: Option<String>,
59    },
60
61    /// Show aggregate metrics about the graph
62    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                // Join through symbol map to filter by source file path.
143                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            // --- symbols ---
197            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            // --- effects ---
227            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            // --- call graph ---
266            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            // --- ledger ---
295            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}