infigraph-cli 1.5.3

CLI for infigraph — AST-powered code analysis and impact review
use std::path::Path;

use anyhow::{Context, Result};
use infigraph_core::Infigraph;
use infigraph_languages::bundled_registry;

pub(crate) fn cmd_search(root: &Path, query: &str, limit: usize, alpha: f32) -> Result<()> {
    use infigraph_core::embed;

    let registry = bundled_registry()?;
    let mut prism = Infigraph::open(root, registry)?;
    prism.init()?;

    let store = prism.store().context("graph not initialized")?;
    let conn = store.connection()?;
    let gq = infigraph_core::graph::GraphQuery::new(&conn);

    let rows = gq.raw_query("MATCH (s:Symbol) RETURN s.id, s.name, s.kind, s.file, s.docstring")?;

    if rows.is_empty() {
        println!("No symbols found. Run 'infigraph index' first.");
        return Ok(());
    }

    let docs: Vec<(String, String)> = rows
        .iter()
        .map(|row| {
            let id = row[0].clone();
            let name = &row[1];
            let kind = &row[2];
            let doc = row.get(4).map(|s| s.as_str()).unwrap_or("");
            let text = if doc.is_empty() {
                format!("{} {}", kind, name)
            } else {
                format!("{} {}: {}", kind, name, doc)
            };
            (id, text)
        })
        .collect();

    let bm25_index = infigraph_core::search::BM25Index::build(docs.clone());

    let embedder = embed::best_embedder();
    let emb_path = root.join(".infigraph").join("embeddings.bin");
    let symbol_embeddings: Vec<(String, Vec<f32>)> = if emb_path.exists() {
        embed::load_embeddings_cached(&emb_path)?
    } else {
        eprintln!("hint: run 'infigraph index' to cache embeddings for faster search");
        docs.iter()
            .map(|(id, text)| (id.clone(), embedder.embed(text).unwrap_or_default()))
            .collect()
    };

    let hnsw_path = root.join(".infigraph").join("hnsw_index.usearch");
    let results = infigraph_core::search::hybrid_search(
        query,
        &bm25_index,
        embedder.as_ref(),
        &symbol_embeddings,
        limit,
        alpha,
        Some(&hnsw_path),
        Some(&emb_path),
    )?;

    if results.is_empty() {
        println!("No results for '{}'", query);
        return Ok(());
    }

    println!("Results for '{}' (alpha={:.1}):", query, alpha);
    for r in &results {
        if let Some(row) = rows.iter().find(|row| row[0] == r.symbol_id) {
            println!(
                "  {:.3} (bm25:{:.2} vec:{:.2})  {:>8} {:30} {}",
                r.score, r.bm25_score, r.vector_score, row[2], row[1], row[3]
            );
            let doc = row.get(4).map(|s| s.as_str()).unwrap_or("");
            if !doc.is_empty() {
                let preview: String = doc.chars().take(80).collect();
                println!("         {}", preview);
            }
        }
    }

    Ok(())
}

pub(crate) fn cmd_search_code(
    root: &Path,
    pattern: &str,
    file_pattern: Option<&str>,
    limit: usize,
) -> Result<()> {
    let root = root.canonicalize().context("invalid project root")?;

    let matches = infigraph_core::search::grep_search(&root, pattern, file_pattern, limit)?;

    if matches.is_empty() {
        println!("No matches for '{}'", pattern);
        return Ok(());
    }

    println!("{} match(es):", matches.len());
    for m in &matches {
        println!("  {}:{}: {}", m.file, m.line_number, m.line_text);
    }

    Ok(())
}

pub(crate) fn cmd_snippet(root: &Path, symbol_id: &str) -> Result<()> {
    let registry = bundled_registry()?;
    let mut prism = Infigraph::open(root, registry)?;
    prism.init()?;

    let store = prism.store().context("graph not initialized")?;
    let conn = store.connection()?;
    let gq = infigraph_core::graph::GraphQuery::new(&conn);

    let detail = gq
        .find_symbol_by_id(symbol_id)?
        .context(format!("symbol '{}' not found in graph", symbol_id))?;

    let file_path = prism.root().join(&detail.file);
    let snippet = infigraph_core::search::read_lines_from_file(
        &file_path,
        detail.start_line,
        detail.end_line,
    )?;

    println!(
        "// {} {} ({}:L{}-{})",
        detail.kind, detail.name, detail.file, detail.start_line, detail.end_line
    );
    println!("{}", snippet);

    Ok(())
}

pub(crate) fn cmd_find_refs(root: &Path, symbol: &str) -> Result<()> {
    let registry = bundled_registry()?;
    let mut prism = Infigraph::open(root, registry)?;
    prism.init()?;
    let store = prism.store().context("graph not initialized")?;
    let conn = store.connection()?;
    let gq = infigraph_core::graph::GraphQuery::new(&conn);

    let refs = gq.find_all_references(symbol)?;
    if refs.is_empty() {
        println!("No references found for '{}'", symbol);
        return Ok(());
    }
    println!("References to '{}' ({} total):\n", symbol, refs.len());
    for r in &refs {
        println!("  {}:{:<6} in {}", r.file, r.line, r.caller_name);
    }
    Ok(())
}

pub(crate) fn cmd_search_docs(root: &Path, query: &str, limit: usize) -> Result<()> {
    let mut idx = infigraph_docs::DocIndex::open(root)?;
    idx.init()?;
    let store = idx.store().context("doc store not initialized")?;

    let results = infigraph_docs::search::hybrid_doc_search(query, store, root, limit, 0.5)?;
    if results.is_empty() {
        println!("No results. Run 'infigraph index-docs' first.");
        return Ok(());
    }
    for r in &results {
        let heading = r.heading.as_deref().unwrap_or("(no heading)");
        println!("{:.3}  {} > {}", r.score, r.doc_file, heading);
        let snippet: String = r.text.chars().take(120).collect();
        println!("      {}\n", snippet);
    }
    Ok(())
}