collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! # collet::treemap — backwards-compatible API
//!
//! This module preserves the public API surface of the original `collet` 0.0.1
//! crate (which re-exported `collet-treemap`).  Existing consumers that depend
//! on `collet::treemap::generate_map` will continue to compile after upgrading.
//!
//! Internally, everything is delegated to the much richer [`crate::repo_map`]
//! subsystem (tree-sitter, BLAKE3, BM25, PageRank).
//!
//! ## Example (unchanged from 0.0.1)
//!
//! ```ignore
//! use collet::treemap::{generate_map, Config, Language};
//! use std::path::Path;
//!
//! let config = Config::default();
//! let map = generate_map(Path::new("."), config)?;
//! println!("Found {} files", map.files.len());
//! # Ok::<_, Box<dyn std::error::Error>>(())
//! ```

use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thiserror::Error;

// ---------------------------------------------------------------------------
// Error type (compatible with collet-treemap 0.0.1)
// ---------------------------------------------------------------------------

/// Errors that can occur during map generation.
#[derive(Error, Debug)]
pub enum MapError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("File not found: {0}")]
    FileNotFound(PathBuf),

    #[error("Unsupported language: {0}")]
    UnsupportedLanguage(String),

    #[error("Parse error: {0}")]
    ParseError(String),
}

// ---------------------------------------------------------------------------
// Public types (compatible with collet-treemap 0.0.1)
// ---------------------------------------------------------------------------

/// Supported programming languages.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum Language {
    Rust,
    Python,
    JavaScript,
    TypeScript,
    Go,
}

impl Language {
    pub fn from_extension(ext: &str) -> Option<Self> {
        match ext {
            "rs" => Some(Language::Rust),
            "py" => Some(Language::Python),
            "js" | "jsx" => Some(Language::JavaScript),
            "ts" | "tsx" => Some(Language::TypeScript),
            "go" => Some(Language::Go),
            _ => None,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            Language::Rust => "rust",
            Language::Python => "python",
            Language::JavaScript => "javascript",
            Language::TypeScript => "typescript",
            Language::Go => "go",
        }
    }
}

/// Configuration for map generation.
#[derive(Debug, Clone, Copy)]
pub struct Config {
    pub format: OutputFormat,
    pub language: Option<Language>,
    pub max_depth: usize,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            format: OutputFormat::Json,
            language: None,
            max_depth: 10,
        }
    }
}

/// Output format for generated maps.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OutputFormat {
    Json,
    Text,
}

/// A code symbol in the repository.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Symbol {
    pub name: String,
    pub kind: SymbolKind,
    pub range: FileRange,
    pub file: PathBuf,
}

/// Kind of code symbol.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SymbolKind {
    Module,
    Function,
    Class,
    Struct,
    Interface,
    Method,
    Property,
    Variable,
    Constant,
    Enum,
    Trait,
    Impl,
    Other(String),
}

impl SymbolKind {
    pub fn as_str(&self) -> &str {
        match self {
            SymbolKind::Module => "module",
            SymbolKind::Function => "function",
            SymbolKind::Class => "class",
            SymbolKind::Struct => "struct",
            SymbolKind::Interface => "interface",
            SymbolKind::Method => "method",
            SymbolKind::Property => "property",
            SymbolKind::Variable => "variable",
            SymbolKind::Constant => "constant",
            SymbolKind::Enum => "enum",
            SymbolKind::Trait => "trait",
            SymbolKind::Impl => "impl",
            SymbolKind::Other(s) => s,
        }
    }
}

/// File position information.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct FileRange {
    pub start_line: usize,
    pub start_col: usize,
    pub end_line: usize,
    pub end_col: usize,
}

/// Repository code map.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoMap {
    pub root: PathBuf,
    pub symbols: Vec<Symbol>,
    pub files: Vec<FileInfo>,
}

/// File metadata in the repository map.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo {
    pub path: PathBuf,
    pub language: Option<String>,
    pub lines: usize,
}

// ---------------------------------------------------------------------------
// generate_map — the main entry point (compatible with collet-treemap 0.0.1)
// ---------------------------------------------------------------------------

/// Generate a code map for the given directory.
///
/// This delegates to collet's internal [`crate::repo_map`] engine which uses
/// tree-sitter for parsing and BLAKE3 for change detection, then converts the
/// result back to the legacy `RepoMap` type for API compatibility.
pub fn generate_map(path: &Path, config: Config) -> Result<RepoMap, MapError> {
    if !path.exists() {
        return Err(MapError::FileNotFound(path.to_path_buf()));
    }
    if !path.is_dir() {
        return Err(MapError::ParseError("Path must be a directory".to_string()));
    }

    // Use the real repo_map engine.
    let mut engine = crate::repo_map::RepoMap::new(path);
    engine.rebuild();

    // Convert internal symbols → legacy Symbol format.
    let mut symbols = Vec::new();
    let mut files = Vec::new();
    let mut seen_files = std::collections::HashSet::new();

    for (file_path, file_syms) in engine.all_symbols() {
        let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");

        // Language filter
        if let Some(filter_lang) = config.language {
            let file_lang = Language::from_extension(ext);
            if file_lang != Some(filter_lang) {
                continue;
            }
        }

        for sym in file_syms {
            let kind = match sym.kind {
                crate::repo_map::parser::SymbolKind::Function => SymbolKind::Function,
                crate::repo_map::parser::SymbolKind::Struct => SymbolKind::Struct,
                crate::repo_map::parser::SymbolKind::Enum => SymbolKind::Enum,
                crate::repo_map::parser::SymbolKind::Trait => SymbolKind::Trait,
                crate::repo_map::parser::SymbolKind::Impl => SymbolKind::Impl,
                crate::repo_map::parser::SymbolKind::Const => SymbolKind::Constant,
                crate::repo_map::parser::SymbolKind::Type => SymbolKind::Other("type".into()),
                crate::repo_map::parser::SymbolKind::Mod => SymbolKind::Module,
                crate::repo_map::parser::SymbolKind::Macro => SymbolKind::Other("macro".into()),
            };
            symbols.push(Symbol {
                name: sym.name.clone(),
                kind,
                range: FileRange {
                    start_line: sym.line,
                    start_col: 0,
                    end_line: sym.line,
                    end_col: 0,
                },
                file: file_path.clone(),
            });
        }

        if seen_files.insert(file_path.clone()) {
            let lines = std::fs::read_to_string(file_path)
                .map(|c| c.lines().count())
                .unwrap_or(0);
            files.push(FileInfo {
                path: file_path.clone(),
                language: Language::from_extension(ext).map(|l| l.as_str().to_string()),
                lines,
            });
        }
    }

    Ok(RepoMap {
        root: path.to_path_buf(),
        symbols,
        files,
    })
}