aft/config.rs
1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3
4/// Runtime configuration for the aft process.
5///
6/// Holds project-scoped settings and tuning knobs. Values are set at startup
7/// and remain immutable for the lifetime of the process.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SemanticBackend {
10 Fastembed,
11 OpenAiCompatible,
12 Ollama,
13}
14
15impl SemanticBackend {
16 pub const fn as_str(&self) -> &'static str {
17 match self {
18 Self::Fastembed => "fastembed",
19 Self::OpenAiCompatible => "openai_compatible",
20 Self::Ollama => "ollama",
21 }
22 }
23
24 pub fn from_name(name: &str) -> Option<Self> {
25 match name {
26 "fastembed" => Some(Self::Fastembed),
27 "openai_compatible" => Some(Self::OpenAiCompatible),
28 "ollama" => Some(Self::Ollama),
29 _ => None,
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct SemanticBackendConfig {
36 pub backend: SemanticBackend,
37 pub model: String,
38 pub base_url: Option<String>,
39 pub api_key_env: Option<String>,
40 pub timeout_ms: u64,
41 pub max_batch_size: usize,
42}
43
44#[derive(Debug, Clone, Default, PartialEq, Eq)]
45pub struct UserServerDef {
46 pub id: String,
47 pub extensions: Vec<String>,
48 pub binary: String,
49 pub args: Vec<String>,
50 pub root_markers: Vec<String>,
51 pub env: HashMap<String, String>,
52 pub initialization_options: Option<serde_json::Value>,
53 pub disabled: bool,
54}
55
56impl Default for SemanticBackendConfig {
57 fn default() -> Self {
58 Self {
59 backend: SemanticBackend::Fastembed,
60 model: DEFAULT_SEMANTIC_MODEL.to_string(),
61 base_url: None,
62 api_key_env: None,
63 // Keep the default below the plugin bridge timeout to avoid bridge-killed
64 // semantic_search requests when callers do not set an explicit timeout.
65 timeout_ms: 25_000,
66 max_batch_size: 64,
67 }
68 }
69}
70
71pub const DEFAULT_SEMANTIC_MODEL: &str = "all-MiniLM-L6-v2";
72
73impl Config {
74 pub fn semantic_backend_label(&self) -> &'static str {
75 self.semantic.backend.as_str()
76 }
77}
78
79#[derive(Debug, Clone)]
80pub struct Config {
81 /// Root directory of the project being analyzed. `None` if not scoped.
82 pub project_root: Option<PathBuf>,
83 /// How many levels of call-graph edges to follow during validation (default: 1).
84 pub validation_depth: u32,
85 /// Hours before a checkpoint expires and is eligible for cleanup (default: 24).
86 pub checkpoint_ttl_hours: u32,
87 /// Maximum depth for recursive symbol resolution (default: 10).
88 pub max_symbol_depth: u32,
89 /// Seconds before killing a formatter subprocess (default: 10).
90 pub formatter_timeout_secs: u32,
91 /// Seconds before killing a type-checker subprocess (default: 30).
92 pub type_checker_timeout_secs: u32,
93 /// Whether to auto-format files after edits (default: true).
94 pub format_on_edit: bool,
95 /// Whether to auto-validate files after edits (default: false).
96 /// When "syntax", only tree-sitter parse check. When "full", runs type checker.
97 pub validate_on_edit: Option<String>,
98 /// Per-language formatter overrides. Keys: "typescript", "python", "rust", "go".
99 /// Values: "biome", "prettier", "deno", "ruff", "black", "rustfmt", "goimports", "gofmt", "none".
100 pub formatter: HashMap<String, String>,
101 /// Per-language type checker overrides. Keys: "typescript", "python", "rust", "go".
102 /// Values: "tsc", "biome", "pyright", "ruff", "cargo", "go", "staticcheck", "none".
103 pub checker: HashMap<String, String>,
104 /// Whether to restrict file operations to within `project_root` (default: false).
105 /// When true, write-capable commands reject paths outside the project root.
106 pub restrict_to_project_root: bool,
107 /// Enable the experimental trigram search index (default: false).
108 pub experimental_search_index: bool,
109 /// Enable the experimental semantic search index (default: false).
110 pub experimental_semantic_search: bool,
111 /// Maximum file size to fully index in bytes (default: 1MB).
112 pub search_index_max_file_size: u64,
113 /// Maximum number of source files allowed for call-graph operations
114 /// (`callers`, `trace_to`, `trace_data`, `impact`). When a project
115 /// exceeds this count the reverse index is not built and those
116 /// commands return a `project_too_large` error. Does not affect
117 /// `grep`, `glob`, `read`, `edit`, or other non-callgraph features.
118 /// Default: 20_000 (covers typical monorepos; rejects OS-wide roots).
119 pub max_callgraph_files: usize,
120 pub semantic: SemanticBackendConfig,
121 /// Enable Astral ty as an experimental Python LSP server (default: false).
122 pub experimental_lsp_ty: bool,
123 /// User-defined LSP servers registered by the OpenCode plugin.
124 pub lsp_servers: Vec<UserServerDef>,
125 /// Lowercase LSP server IDs disabled by user config.
126 pub disabled_lsp: HashSet<String>,
127 /// Extra directories to search when resolving LSP binaries.
128 /// The plugin populates these from its own auto-install cache (e.g.
129 /// `~/.cache/aft/lsp-packages/<pkg>/node_modules/.bin/`) so a binary AFT
130 /// installed itself is discoverable without needing it on PATH.
131 /// Resolution order: `<project_root>/node_modules/.bin/<bin>` →
132 /// `lsp_paths_extra/<bin>` (in order) → PATH via `which`.
133 pub lsp_paths_extra: Vec<PathBuf>,
134 /// Persistent storage directory for indexes (trigram, semantic).
135 /// Set by the plugin to the XDG-compliant path (e.g. ~/.local/share/opencode/storage/plugin/aft/).
136 /// Falls back to ~/.cache/aft/ if not set.
137 pub storage_dir: Option<PathBuf>,
138 /// Maximum number of (server, file) entries kept in the in-memory
139 /// diagnostic cache. Older entries are evicted in LRU order when the
140 /// cap is exceeded. Set to 0 to disable the cap entirely.
141 /// Default: 5000 (covers very large monorepos with bounded memory).
142 pub diagnostic_cache_size: usize,
143}
144
145impl Default for Config {
146 fn default() -> Self {
147 Config {
148 project_root: None,
149 validation_depth: 1,
150 checkpoint_ttl_hours: 24,
151 max_symbol_depth: 10,
152 formatter_timeout_secs: 10,
153 type_checker_timeout_secs: 30,
154 format_on_edit: true,
155 validate_on_edit: None,
156 formatter: HashMap::new(),
157 checker: HashMap::new(),
158 // Default to false to match OpenCode's existing permission-based model.
159 // The plugin opts into root restriction explicitly when desired.
160 restrict_to_project_root: false,
161 experimental_search_index: false,
162 experimental_semantic_search: false,
163 search_index_max_file_size: 1_048_576,
164 // Projects larger than this skip call-graph reverse index construction.
165 // Chosen to cover typical monorepos (AFT ~2K, OpenCode ~5K, Reth ~8K)
166 // while rejecting OS-wide roots (/home, ~/Work) that would otherwise
167 // walk hundreds of thousands of files per callers/trace_to query.
168 max_callgraph_files: 20_000,
169 semantic: SemanticBackendConfig::default(),
170 experimental_lsp_ty: false,
171 lsp_servers: Vec::new(),
172 disabled_lsp: HashSet::new(),
173 lsp_paths_extra: Vec::new(),
174 storage_dir: None,
175 diagnostic_cache_size: 5000,
176 }
177 }
178}