navi-core 0.3.6

Local agentic engine and terminal-first coding agent.
Documentation
use anyhow::Result;
use async_trait::async_trait;
use serde_json::{Value, json};

use super::helpers;
use crate::repo_intelligence::{
    build_index, churn_from_git_log, dependency_edges, discover_tests, goto_symbol,
    ranked_symbol_matches, references, search_text_matches,
};
use crate::security::SecurityPolicy;
use crate::tool::{Tool, ToolDefinition, ToolInvocation, ToolKind, ToolResult};

#[derive(Clone, Copy)]
pub(crate) enum RepoIntelligenceAction {
    AstSearch,
    SymbolGoto,
    SymbolReferences,
    DependencyGraph,
    TestDiscovery,
    OwnershipChurn,
}

pub(crate) struct RepoIntelligenceTool {
    policy: SecurityPolicy,
    action: RepoIntelligenceAction,
}

impl RepoIntelligenceTool {
    pub(crate) fn new(policy: SecurityPolicy, action: RepoIntelligenceAction) -> Self {
        Self { policy, action }
    }

    fn name(&self) -> &'static str {
        match self.action {
            RepoIntelligenceAction::AstSearch => "ast_search",
            RepoIntelligenceAction::SymbolGoto => "symbol_goto",
            RepoIntelligenceAction::SymbolReferences => "symbol_references",
            RepoIntelligenceAction::DependencyGraph => "dependency_graph_query",
            RepoIntelligenceAction::TestDiscovery => "test_discovery",
            RepoIntelligenceAction::OwnershipChurn => "ownership_churn_query",
        }
    }
}

#[async_trait]
impl Tool for RepoIntelligenceTool {
    fn definition(&self) -> ToolDefinition {
        match self.action {
            RepoIntelligenceAction::AstSearch => helpers::definition(
                self.name(),
                "Search repository symbols using the structured repo index before falling back to text grep.",
                ToolKind::Read,
                json!({
                    "type": "object",
                    "properties": {
                        "query": { "type": "string" },
                        "kind": { "type": "string" },
                        "max_results": { "type": "integer" }
                    },
                    "required": ["query"],
                    "additionalProperties": false
                }),
            ),
            RepoIntelligenceAction::SymbolGoto => helpers::definition(
                self.name(),
                "Resolve a symbol name to its defining file and line.",
                ToolKind::Read,
                json!({
                    "type": "object",
                    "properties": { "name": { "type": "string" } },
                    "required": ["name"],
                    "additionalProperties": false
                }),
            ),
            RepoIntelligenceAction::SymbolReferences => helpers::definition(
                self.name(),
                "Find identifier references using the structured repo index.",
                ToolKind::Read,
                json!({
                    "type": "object",
                    "properties": {
                        "name": { "type": "string" },
                        "max_results": { "type": "integer" }
                    },
                    "required": ["name"],
                    "additionalProperties": false
                }),
            ),
            RepoIntelligenceAction::DependencyGraph => helpers::definition(
                self.name(),
                "Query compact import/dependency edges discovered from source files.",
                ToolKind::Read,
                json!({
                    "type": "object",
                    "properties": { "max_results": { "type": "integer" } },
                    "additionalProperties": false
                }),
            ),
            RepoIntelligenceAction::TestDiscovery => helpers::definition(
                self.name(),
                "Suggest the smallest verifier/test command for touched paths.",
                ToolKind::Read,
                json!({
                    "type": "object",
                    "properties": {
                        "paths": {
                            "type": "array",
                            "items": { "type": "string" }
                        }
                    },
                    "additionalProperties": false
                }),
            ),
            RepoIntelligenceAction::OwnershipChurn => helpers::definition(
                self.name(),
                "Return files with the highest recent git churn for risk-aware planning.",
                ToolKind::Read,
                json!({
                    "type": "object",
                    "properties": { "max_results": { "type": "integer" } },
                    "additionalProperties": false
                }),
            ),
        }
    }

    async fn invoke(&self, invocation: ToolInvocation) -> Result<ToolResult> {
        let policy = self.policy.clone();
        let action = self.action;
        let input = invocation.input.clone();
        let output = tokio::task::spawn_blocking(move || run_action(&policy, action, &input))
            .await
            .map_err(|err| anyhow::anyhow!("repo intelligence task join error: {err}"))??;
        Ok(helpers::ok(invocation.id, output))
    }
}

fn run_action(
    policy: &SecurityPolicy,
    action: RepoIntelligenceAction,
    input: &Value,
) -> Result<Value> {
    let root = policy.project_root();
    match action {
        RepoIntelligenceAction::AstSearch => {
            let query = helpers::required_string(input, "query")?;
            let kind = helpers::optional_string(input, "kind");
            let max_results = bounded(input, "max_results", 80, 500);
            let index = build_index(root)?;
            let ranked = ranked_symbol_matches(&index, query, kind.as_deref());
            let matches = ranked
                .iter()
                .take(max_results)
                .map(|ranked| ranked.symbol.clone())
                .collect::<Vec<_>>();
            let ranking = ranked
                .iter()
                .take(max_results)
                .map(|ranked| {
                    json!({
                        "name": ranked.symbol.name.clone(),
                        "kind": ranked.symbol.kind.clone(),
                        "path": ranked.symbol.path.clone(),
                        "line": ranked.symbol.line,
                        "score": ranked.score,
                        "reasons": ranked.reasons.clone(),
                    })
                })
                .collect::<Vec<_>>();
            let text_matches = search_text_matches(&index, query, max_results.clamp(5, 40));
            Ok(json!({
                "schema_version": helpers::SPECIALIZED_SCHEMA_VERSION,
                "query": query,
                "matches": matches,
                "text_matches": text_matches,
                "ranking": ranking,
                "files_indexed": index.files.len(),
            }))
        }
        RepoIntelligenceAction::SymbolGoto => {
            let name = helpers::required_string(input, "name")?;
            let index = build_index(root)?;
            Ok(json!({
                "schema_version": helpers::SPECIALIZED_SCHEMA_VERSION,
                "name": name,
                "symbol": goto_symbol(&index, name),
            }))
        }
        RepoIntelligenceAction::SymbolReferences => {
            let name = helpers::required_string(input, "name")?;
            let max_results = bounded(input, "max_results", 80, 500);
            let index = build_index(root)?;
            let mut refs = references(&index, name);
            refs.truncate(max_results);
            Ok(json!({
                "schema_version": helpers::SPECIALIZED_SCHEMA_VERSION,
                "name": name,
                "references": refs,
            }))
        }
        RepoIntelligenceAction::DependencyGraph => {
            let max_results = bounded(input, "max_results", 120, 1000);
            let index = build_index(root)?;
            let mut edges = dependency_edges(&index);
            edges.truncate(max_results);
            Ok(json!({
                "schema_version": helpers::SPECIALIZED_SCHEMA_VERSION,
                "edges": edges,
                "files_indexed": index.files.len(),
            }))
        }
        RepoIntelligenceAction::TestDiscovery => {
            let paths = input
                .get("paths")
                .and_then(Value::as_array)
                .map(|values| {
                    values
                        .iter()
                        .filter_map(Value::as_str)
                        .map(std::path::PathBuf::from)
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default();
            Ok(json!({
                "schema_version": helpers::SPECIALIZED_SCHEMA_VERSION,
                "tests": discover_tests(root, &paths),
            }))
        }
        RepoIntelligenceAction::OwnershipChurn => {
            let max_results = bounded(input, "max_results", 20, 200);
            Ok(json!({
                "schema_version": helpers::SPECIALIZED_SCHEMA_VERSION,
                "churn": churn_from_git_log(root, max_results),
            }))
        }
    }
}

fn bounded(input: &Value, key: &str, default: usize, max: usize) -> usize {
    input
        .get(key)
        .and_then(Value::as_u64)
        .map(|value| value as usize)
        .unwrap_or(default)
        .clamp(1, max)
}