Skip to main content

agentstatedeveloper_cli/commands/
architecture.rs

1//! `asd architecture` — a one-call "orient me" overview for a cold agent.
2//!
3//! Assembles existing index data into a single snapshot: languages, top
4//! packages, architectural layers, HTTP routes, call-graph communities, and
5//! hotspots. Read-only. The JSON body is built by
6//! [`agentstatedeveloper_core::architecture_overview`] so the CLI and the
7//! `asd-mcp` `architecture` tool return byte-identical payloads.
8
9use anyhow::Result;
10use clap::Args;
11use serde_json::Value;
12
13use agentstatedeveloper_core::{Engine, architecture_overview};
14
15use crate::config::Config;
16
17#[derive(Debug, Args)]
18pub struct ArchitectureArgs {
19    /// Machine-readable JSON.
20    #[arg(long)]
21    pub agent: bool,
22
23    /// How many packages / hotspots to list.
24    #[arg(long, default_value = "12")]
25    pub top: usize,
26}
27
28pub fn run(cfg: &Config, args: ArchitectureArgs) -> Result<()> {
29    let engine = Engine::open_sqlite(&cfg.db_path)?;
30    let out = architecture_overview(&engine, args.top);
31
32    if args.agent {
33        println!("{}", serde_json::to_string_pretty(&out)?);
34        return Ok(());
35    }
36    print_human(&out);
37    Ok(())
38}
39
40fn print_human(out: &Value) {
41    println!(
42        "Architecture overview — {} ({} symbols)",
43        out["repo_id"].as_str().unwrap_or("?"),
44        out["symbols"].as_u64().unwrap_or(0)
45    );
46    let row = |label: &str, items: &Value| {
47        if let Some(arr) = items.as_array() {
48            let parts: Vec<String> = arr
49                .iter()
50                .map(|e| {
51                    let name = e
52                        .get("name")
53                        .or_else(|| e.get("layer"))
54                        .and_then(|v| v.as_str())
55                        .unwrap_or("?");
56                    format!("{} ({})", name, e["symbols"].as_u64().unwrap_or(0))
57                })
58                .collect();
59            println!("  {label}: {}", parts.join(", "));
60        }
61    };
62    row("languages", &out["languages"]);
63    row("layers", &out["layers"]);
64    row("packages", &out["packages"]);
65
66    if let Some(cl) = out["clusters"].as_array() {
67        if !cl.is_empty() {
68            println!("  clusters (call-graph communities):");
69            for c in cl.iter().take(8) {
70                println!(
71                    "    {:>4} symbols  {}  (repr: {})",
72                    c["size"].as_u64().unwrap_or(0),
73                    c["package"].as_str().unwrap_or("?"),
74                    c["representative"].as_str().unwrap_or("?")
75                );
76            }
77        }
78    }
79
80    let inbound = out["routes"]["inbound"]
81        .as_array()
82        .cloned()
83        .unwrap_or_default();
84    println!(
85        "  routes: {} inbound, {} outbound consumers",
86        inbound.len(),
87        out["routes"]["outbound_consumers"].as_u64().unwrap_or(0)
88    );
89    for r in inbound.iter().take(10) {
90        println!(
91            "    {}  ({})",
92            r["contract"].as_str().unwrap_or("?"),
93            r["qname"].as_str().unwrap_or("?")
94        );
95    }
96
97    if let Some(hs) = out["hotspots"].as_array() {
98        println!("  hotspots (call-graph degree):");
99        for h in hs.iter().take(10) {
100            println!(
101                "    {:>3}  {}  ({})",
102                h["degree"].as_u64().unwrap_or(0),
103                h["qname"].as_str().unwrap_or("?"),
104                h["file"].as_str().unwrap_or("?")
105            );
106        }
107    }
108}