rdar 0.6.1

radar - the repository cartographer for AI agents: compiles a repo into tiny committed MAP.md routers, with measured token benchmarks
Documentation
//! LSP enrichment at refresh time: spawn a language server
//! OFFLINE, verify the symbols the maps already carry, upgrade `fidelity:`.
//! Never on the agent's read path; any failure degrades to the syntax tier.
//!
//! The first adapters cover rust-analyzer and pyright; the
//! registry is data, extensible without code changes.

use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;

use crate::jsonrpc::RpcClient;
use crate::lang::Lang;

/// Server registry: language → (binary, args). User-extensible later via
/// Future adapters can be configured through `radar.toml`.
pub fn server_for(lang: Lang) -> Option<(&'static str, &'static [&'static str])> {
    match lang {
        Lang::Rust => Some(("rust-analyzer", &[])),
        Lang::Python => Some(("pyright-langserver", &["--stdio"])),
        _ => None,
    }
}

/// Result of enriching one language's scopes.
#[derive(Debug, Default)]
pub struct Enrichment {
    /// Symbol name → number of workspace references found by the server.
    pub verified_refs: BTreeMap<String, u64>,
    pub server: String,
}

fn uri(path: &Path) -> String {
    let p = path.to_string_lossy().replace('\\', "/");
    if p.starts_with('/') {
        format!("file://{p}")
    } else {
        format!("file:///{p}")
    }
}

/// Spawn, initialize, and verify references for `symbols` (name → one
/// (file, line, character) definition site). Wall-clock-budgeted; `Err`
/// means "stay at syntax fidelity", never a failed refresh.
pub fn enrich(
    root: &Path,
    lang: Lang,
    symbols: &[(String, String, u32)],
    budget: Duration,
) -> std::io::Result<Enrichment> {
    let (program, args) = server_for(lang)
        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Unsupported, "no adapter"))?;
    let mut client = RpcClient::spawn(program, args, root)?;
    client.timeout = budget;

    let root_uri = uri(&root.canonicalize()?);
    client.request(
        "initialize",
        serde_json::json!({
            "processId": std::process::id(),
            "rootUri": root_uri,
            "capabilities": {},
        }),
    )?;
    client.notify("initialized", serde_json::json!({}))?;

    let mut out = Enrichment {
        server: program.to_string(),
        ..Enrichment::default()
    };
    for (name, rel, line) in symbols {
        let zero_based_line = line.saturating_sub(1);
        let file = root.join(rel);
        let Ok(text) = std::fs::read_to_string(&file) else {
            continue;
        };
        let file_uri = uri(&file.canonicalize().unwrap_or(file.clone()));
        client.notify(
            "textDocument/didOpen",
            serde_json::json!({
                "textDocument": {
                    "uri": file_uri,
                    "languageId": lang.name(),
                    "version": 1,
                    "text": text,
                }
            }),
        )?;
        // Column of the symbol name on its line (LSP positions are 0-based).
        let col = text
            .lines()
            .nth(zero_based_line as usize)
            .and_then(|l| l.find(name.as_str()))
            .unwrap_or(0);
        let refs = client.request(
            "textDocument/references",
            serde_json::json!({
                "textDocument": { "uri": file_uri },
                "position": { "line": zero_based_line, "character": col },
                "context": { "includeDeclaration": false },
            }),
        )?;
        if let Some(arr) = refs.as_array() {
            out.verified_refs.insert(name.clone(), arr.len() as u64);
        }
    }
    client.shutdown();
    Ok(out)
}

/// True when the language server for `lang` actually RUNS. A file-on-PATH
/// check is not enough: rustup ships a `rust-analyzer` shim that errors out
/// when the component isn't installed (found by dogfooding) - so this
/// spawns `--version` and requires a zero exit.
pub fn available(lang: Lang) -> bool {
    let Some((program, _)) = server_for(lang) else {
        return false;
    };
    std::process::Command::new(program)
        .arg("--version")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .is_ok_and(|s| s.success())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn unsupported_language_has_no_server() {
        assert!(server_for(Lang::Bash).is_none());
        assert!(!available(Lang::Bash));
    }
}