rag-rat 0.10.0

CLI and MCP entrypoint for indexing repositories into local source, graph, history, and memory evidence.
use super::*;

/// Render a complete, commented `rag-rat.toml`. The lines that reflect *this* repo's choices —
/// root/database, the language bindings, the chosen embedding model, the oracle opt-in — are
/// active; every other table is emitted as commented defaults so the full config surface is
/// discoverable without leaving the file (the bare-bindings stub used to hide it). See
/// `docs/config.md`.
pub(crate) fn render_config(plan: &InitPlan) -> String {
    let mut text = String::new();
    text.push_str(
        "# rag-rat configuration — generated by `rag-rat init`; edit freely.\n# Active lines \
         below reflect this repo; commented tables show the tunable defaults.\n# Full reference: \
         docs/config.md.\n\n",
    );

    text.push_str("[index]\n");
    text.push_str(&format!("root = {}\n", toml_string(&plan.root_value)));
    text.push_str(&format!("database = {}\n\n", toml_string(DEFAULT_DATABASE)));

    text.push_str(
        "# Language → directories to index. Each language indexes its default file extensions in \
         the\n# listed dirs; a `cpp` binding also claims `.h` headers (indexed as C++), a `c` \
         binding\n# claims `.c` + `.h`. Add or remove languages and directories as your layout \
         needs.\n",
    );
    text.push_str("[target_bindings]\n");
    for language in &plan.languages {
        let dirs = plan.bindings.get(language).cloned().unwrap_or_default();
        text.push_str(&format!("{} = [{}]\n", language.as_str(), quoted_paths(&dirs)));
    }
    text.push('\n');

    text.push_str(
        "# Richer targets (uncomment to use): a named target with an explicit kind and custom \
         file\n# globs — the only way to bind generated / test / docs trees or a non-default file \
         set.\n# `kind` is source|generated|docs|tests; `include`/`exclude` are `**/*.<ext>` \
         globs.\n# [[target]]\n# name = \"generated-bindings\"\n# language = \"typescript\"\n# \
         directories = [\"packages/app/src/generated\"]\n# kind = \"generated\"\n# include = \
         [\"**/*.ts\"]\n# exclude = [\"**/*.map\"]\n\n",
    );

    text.push_str("[local_ai.embedding]\n");
    text.push_str(&format!("# {}\n", backend_label(plan.backend)));
    text.push_str(&format!("model = {}\n\n", toml_string(plan.backend.as_str())));

    text.push_str(
        "# Embedding runtime tuning (defaults shown; uncomment to override).\n# \
         [local_ai.embedding.runtime]\n# batch_size = 64\n# ort_threads = 4          # ONNX \
         intra-op threads\n# omp_threads = 1\n# max_embedding_chars = 4000\n\n",
    );

    text.push_str(
        "# Background file watcher — keeps the index fresh as files change (defaults shown).\n# \
         [watch]\n# enabled = true\n# debounce_ms = 400           # quiet window before a reindex \
         pass\n# max_latency_ms = 2500       # force a pass after this much continuous \
         activity\n# periodic_sweep_secs = 300   # backstop pass interval, 0 disables\n\n",
    );

    text.push_str(
        "# Check crates.io for a newer rag-rat and surface it. Best-effort, cached, never \
         blocks.\n# [version_check]\n# enabled = true\n\n",
    );

    text.push_str("[oracle]\n");
    text.push_str(
        "# Background auto-refresh of compiler-grade (SCIP) importance ranking. Needs a \
         language\n# tool on PATH (e.g. rust-analyzer); runs throttled in the MCP server only. \
         Default off.\n",
    );
    text.push_str(&format!("auto_run = {}\n", plan.oracle_auto_run));
    text.push_str(
        "# auto_run_quiet_period_secs = 900    # run only after the index has been this quiet\n# \
         auto_run_min_interval_secs = 21600  # and at most this often, regardless of churn\n",
    );
    text
}
pub(crate) fn quoted_paths(paths: &[PathBuf]) -> String {
    paths.iter().map(|path| toml_string(&display_rel(path))).collect::<Vec<_>>().join(", ")
}
pub(crate) fn config_root_value(root: &Path, config_path: &Path) -> String {
    let Some(parent) = config_path.parent().filter(|path| !path.as_os_str().is_empty()) else {
        return ".".to_string();
    };
    if config_path.is_absolute() {
        absolute_config_root_value(root, parent)
    } else {
        relative_config_root_value(parent)
    }
}
pub(crate) fn absolute_config_root_value(root: &Path, parent: &Path) -> String {
    if let Ok(relative_parent) = parent.strip_prefix(root) {
        return relative_config_root_value(relative_parent);
    }
    root.display().to_string()
}
pub(crate) fn relative_config_root_value(parent: &Path) -> String {
    let depth = parent.components().filter(normal_component).count();
    if depth == 0 {
        ".".to_string()
    } else {
        std::iter::repeat_n("..", depth).collect::<Vec<_>>().join("/")
    }
}
pub(crate) fn normal_component(component: &std::path::Component<'_>) -> bool {
    matches!(component, std::path::Component::Normal(_))
}
pub(crate) fn toml_string(value: &str) -> String {
    format!("{value:?}")
}
pub(crate) fn display_rel(path: &Path) -> String {
    let text = path.to_string_lossy().replace('\\', "/");
    if text.is_empty() { ".".to_string() } else { text }
}
pub(crate) fn supported_languages() -> Vec<Language> {
    Language::all().to_vec()
}