collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
/// Whether a user query is likely to benefit from BM25 code search.
///
/// Returns `false` for short greetings, git commands, simple instructions,
/// and other prompts where injecting code context would just add noise.
pub fn needs_code_search(input: &str) -> bool {
    let trimmed = input.trim();

    // Too short — "hi", "ok", "push"
    // Use char count (not byte len) since CJK chars are 3 bytes each.
    // 6 chars is ~2 English words or ~3 Korean syllables.
    if trimmed.chars().count() < 6 {
        return false;
    }

    let lower = trimmed.to_lowercase();

    // Starts with a slash command → user already knows what they want
    if lower.starts_with('/') {
        return false;
    }

    // Git / deployment / meta instructions — no code search needed
    let skip_prefixes = [
        "git ", "push", "pull", "commit", "merge", "rebase", "deploy", "release", "thank",
        "thanks", "ok", "yes", "no", "ㄱㅅ", "고마", "좋아", "알겠", "", "아니",
    ];
    for prefix in &skip_prefixes {
        if lower.starts_with(prefix) {
            return false;
        }
    }

    // Contains code-relevant signal words → worth searching
    let code_signals = [
        "error",
        "bug",
        "fix",
        "implement",
        "add",
        "create",
        "update",
        "refactor",
        "where",
        "how",
        "find",
        "search",
        "function",
        "struct",
        "class",
        "method",
        "module",
        "file",
        "test",
        "why",
        "what",
        "change",
        "modify",
        "delete",
        "remove",
        "performance",
        "optimize",
        "slow",
        "fast",
        "import",
        "에러",
        "버그",
        "수정",
        "구현",
        "추가",
        "만들",
        "변경",
        "리팩",
        "함수",
        "파일",
        "모듈",
        "테스트",
        "어디",
        "",
    ];
    for signal in &code_signals {
        if lower.contains(signal) {
            return true;
        }
    }

    // Contains a file path or identifier pattern → probably code-related
    if trimmed.contains('/') || trimmed.contains("::") || trimmed.contains('_') {
        return true;
    }

    // Longer queries (>30 chars) without skip signals → probably code-related
    trimmed.len() > 30
}

/// Classify the query intent for context injection strategy.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum QueryIntent {
    /// Needs code search (function, struct, implementation questions)
    Code,
    /// Needs documentation search (README, comments, architecture)
    Docs,
    /// No search needed (greeting, git command, short instruction)
    Skip,
}

/// Classify a user query for optimal context injection.
pub fn classify_query(input: &str) -> QueryIntent {
    let trimmed = input.trim();
    if trimmed.chars().count() < 6 || trimmed.starts_with('/') {
        return QueryIntent::Skip;
    }

    let lower = trimmed.to_lowercase();

    // Check skip prefixes first
    let skip_prefixes = [
        "git ", "push", "pull", "commit", "merge", "rebase", "deploy", "release", "thank",
        "thanks", "ok", "yes", "no", "ㄱㅅ", "고마", "좋아", "알겠", "", "아니",
    ];
    for prefix in &skip_prefixes {
        if lower.starts_with(prefix) {
            return QueryIntent::Skip;
        }
    }

    // Documentation signals (checked before code to prioritize docs intent)
    let doc_signals = [
        "readme",
        "document",
        "explain",
        "architecture",
        "design",
        "convention",
        "rule",
        "guide",
        "how does",
        "how do",
        "overview",
        "summary",
        "describe",
        "설명",
        "문서",
        "아키텍",
        "구조",
        "설계",
        "가이드",
    ];
    for signal in &doc_signals {
        if lower.contains(signal) {
            return QueryIntent::Docs;
        }
    }

    // Code signals
    if needs_code_search(input) {
        return QueryIntent::Code;
    }

    QueryIntent::Skip
}