crabmap 0.1.1

Rust code satellite map — index, query, and navigate your entire codebase
use crate::ai;
use crate::health;
use crate::model::CodeGraph;
use anyhow::Result;
use serde_json::Value;

pub fn map(graph: &CodeGraph, budget: usize) -> Result<Value> {
    let stats = graph.stats();
    let entries = ai::entries(graph, 10);
    let clusters = ai::clusters(graph, 10);
    let health = health::health(graph, 10);
    let mut output = format!(
        "# Codegraph Map\n\nProject: `{}`\nNodes: {} Edges: {} Files: {} Symbols: {}\nQuality: {}\nHealth: {}\n\n",
        graph.project.root,
        stats.nodes,
        stats.edges,
        stats.files,
        stats.symbols,
        ai::quality(graph)
            .get("score")
            .and_then(Value::as_u64)
            .unwrap_or_default(),
        health
            .get("score")
            .and_then(Value::as_u64)
            .unwrap_or_default()
    );
    push_section(&mut output, "Entry Points", entries_list(&entries));
    push_section(&mut output, "Feature Clusters", clusters_list(&clusters));
    push_section(&mut output, "Hot Symbols", hot_list(&health));
    if output.len() > budget {
        output.truncate(budget.saturating_sub(16));
        output.push_str("\n...[truncated]\n");
    }
    Ok(serde_json::json!({
        "kind": "map",
        "budget": budget,
        "content": output
    }))
}

fn push_section(output: &mut String, title: &str, body: String) {
    output.push_str("## ");
    output.push_str(title);
    output.push_str("\n\n");
    output.push_str(&body);
    output.push_str("\n\n");
}

fn entries_list(value: &Value) -> String {
    value
        .get("items")
        .and_then(Value::as_array)
        .unwrap_or(&Vec::new())
        .iter()
        .map(|item| {
            let node = item.get("node").unwrap_or(&Value::Null);
            format!(
                "- {} `{}` in `{}`",
                node.get("kind").and_then(Value::as_str).unwrap_or("node"),
                node.get("qualified_name")
                    .and_then(Value::as_str)
                    .unwrap_or_default(),
                node.get("file").and_then(Value::as_str).unwrap_or_default()
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn clusters_list(value: &Value) -> String {
    value
        .get("items")
        .and_then(Value::as_array)
        .unwrap_or(&Vec::new())
        .iter()
        .map(|item| {
            format!(
                "- `{}` files={} symbols={} degree={}",
                item.get("name").and_then(Value::as_str).unwrap_or_default(),
                item.get("files")
                    .and_then(Value::as_u64)
                    .unwrap_or_default(),
                item.get("symbols")
                    .and_then(Value::as_u64)
                    .unwrap_or_default(),
                item.get("degree")
                    .and_then(Value::as_u64)
                    .unwrap_or_default()
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn hot_list(value: &Value) -> String {
    value
        .get("hot_symbols")
        .and_then(Value::as_array)
        .unwrap_or(&Vec::new())
        .iter()
        .map(|item| {
            format!(
                "- `{}` in `{}` degree={}",
                item.get("qualified_name")
                    .and_then(Value::as_str)
                    .unwrap_or_default(),
                item.get("file").and_then(Value::as_str).unwrap_or_default(),
                item.get("degree")
                    .and_then(Value::as_u64)
                    .unwrap_or_default()
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}