Skip to main content

agentstatedeveloper_cli/commands/
read.rs

1//! `asd read <qname>` — look up a symbol by qname, pull its effect
2//! declaration, ledger entries, and direct callers/callees from the call graph.
3
4use anyhow::Result;
5use clap::Args;
6use serde_json::json;
7
8use agentstatedeveloper_core::{
9    AsgEffectStore, AsgIndexStore, AsgLedgerStore, EffectStore, Engine, IndexStore, LedgerStore,
10};
11
12use crate::commands::graph::build_id_map;
13use crate::config::Config;
14
15#[derive(Debug, Args)]
16pub struct ReadArgs {
17    /// Fully-qualified symbol name, e.g. `pkg.module.func`.
18    pub qname: String,
19
20    /// Maximum callers/callees to emit. Default 10 — most agent
21    /// queries care about the immediate few. AcmeFlow field-eval
22    /// (2026-06-04) reported `asd read` was "verbose in callers/
23    /// callees, though the ledger section is now very useful." Cap
24    /// is per-direction (10 callers + 10 callees). When truncated,
25    /// the array's last entry is a sentinel `{"truncated": N}`.
26    #[arg(long, default_value = "10")]
27    pub limit: usize,
28
29    /// Bypass the --limit cap and emit ALL callers/callees. Useful
30    /// for broad blast-radius queries; produces large output on
31    /// hot symbols.
32    #[arg(long)]
33    pub full: bool,
34}
35
36pub fn run(cfg: &Config, args: ReadArgs) -> Result<()> {
37    let engine = Engine::open_sqlite(&cfg.db_path)?;
38
39    let index_store = AsgIndexStore::from_engine(&engine);
40    let effect_store = AsgEffectStore::from_engine(&engine);
41    let ledger_store = AsgLedgerStore::from_engine(&engine);
42
43    let symbol = index_store
44        .get_symbol_by_qname(&engine.ref_name, &args.qname)?
45        .ok_or_else(|| anyhow::anyhow!("symbol not found: {}", args.qname))?;
46
47    let effects = effect_store.get_effects(&engine.ref_name, &symbol.symbol_id)?;
48    let mut ledger = ledger_store.list_entries(&engine.ref_name, &symbol.symbol_id)?;
49    ledger.truncate(5);
50
51    // Call graph — resolve symbol IDs to qname + location.
52    let callee_ids = index_store.get_callees(&engine.ref_name, &symbol.symbol_id)?;
53    let caller_ids = index_store.get_callers(&engine.ref_name, &symbol.symbol_id)?;
54
55    let id_map = build_id_map(&engine);
56
57    let resolve = |ids: Vec<String>| -> Vec<serde_json::Value> {
58        ids.iter()
59            .map(|id| {
60                if let Some(s) = id_map.get(id) {
61                    json!({ "qname": s.qname, "file": s.file, "line": s.start.line })
62                } else {
63                    json!({ "symbol_id": id })
64                }
65            })
66            .collect()
67    };
68
69    let mut callers = resolve(caller_ids);
70    let mut callees = resolve(callee_ids);
71
72    // AcmeFlow refinement (1.0.82): cap callers/callees by
73    // default. `--full` to bypass. Truncation marker appended so
74    // the agent knows there's more — matches the pattern from
75    // trim_for_agent in core::search_fts.
76    //
77    // 1.0.85: --limit N now means "at most N entries TOTAL,
78    // including any truncation sentinel." Pre-fix the cap left
79    // N entries then appended a sentinel for a total of N+1.
80    // Off-by-one caught by AcmeFlow field-eval 2026-06-04.
81    if !args.full {
82        if callers.len() > args.limit {
83            // Reserve a slot for the truncation sentinel: keep
84            // (limit - 1) real entries, push the marker as the N-th.
85            let keep = args.limit.saturating_sub(1);
86            let extra = callers.len() - keep;
87            callers.truncate(keep);
88            callers.push(json!({ "truncated": extra }));
89        }
90        if callees.len() > args.limit {
91            let keep = args.limit.saturating_sub(1);
92            let extra = callees.len() - keep;
93            callees.truncate(keep);
94            callees.push(json!({ "truncated": extra }));
95        }
96    }
97    let effects_json = effects
98        .as_ref()
99        .map(|e| serde_json::to_value(e).unwrap_or(serde_json::Value::Null));
100
101    let qid = crate::commands::brief::query_id("read", &[&args.qname]);
102
103    if cfg.brief {
104        let mut out = crate::commands::brief::brief_read(
105            &symbol,
106            &callers,
107            &callees,
108            effects_json.as_ref(),
109            ledger.len(),
110        );
111        if let serde_json::Value::Object(ref mut m) = out {
112            m.insert("query_id".into(), serde_json::Value::String(qid));
113        }
114        println!("{}", serde_json::to_string_pretty(&out)?);
115        return Ok(());
116    }
117
118    let out = json!({
119        "symbol": symbol,
120        "callers": callers,
121        "callees": callees,
122        "effects": effects,
123        "ledger": ledger,
124        "query_id": qid,
125    });
126    println!("{}", serde_json::to_string_pretty(&out)?);
127    Ok(())
128}