aft/config.rs
1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::harness::Harness;
7
8/// Runtime configuration for the aft process.
9///
10/// Holds project-scoped settings and tuning knobs. Values are set at startup
11/// and remain immutable for the lifetime of the process.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum SemanticBackend {
15 Fastembed,
16 #[serde(rename = "openai_compatible")]
17 OpenAiCompatible,
18 Ollama,
19}
20
21impl SemanticBackend {
22 pub const fn as_str(&self) -> &'static str {
23 match self {
24 Self::Fastembed => "fastembed",
25 Self::OpenAiCompatible => "openai_compatible",
26 Self::Ollama => "ollama",
27 }
28 }
29
30 pub fn from_name(name: &str) -> Option<Self> {
31 match name {
32 "fastembed" => Some(Self::Fastembed),
33 "openai_compatible" => Some(Self::OpenAiCompatible),
34 "ollama" => Some(Self::Ollama),
35 _ => None,
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct SemanticBackendConfig {
42 pub backend: SemanticBackend,
43 pub model: String,
44 pub base_url: Option<String>,
45 pub api_key_env: Option<String>,
46 pub timeout_ms: u64,
47 pub max_batch_size: usize,
48}
49
50#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
51pub struct UserServerDef {
52 pub id: String,
53 pub extensions: Vec<String>,
54 pub binary: String,
55 pub args: Vec<String>,
56 pub root_markers: Vec<String>,
57 pub env: HashMap<String, String>,
58 pub initialization_options: Option<serde_json::Value>,
59 pub disabled: bool,
60}
61
62impl Default for SemanticBackendConfig {
63 fn default() -> Self {
64 Self {
65 backend: SemanticBackend::Fastembed,
66 model: DEFAULT_SEMANTIC_MODEL.to_string(),
67 base_url: None,
68 api_key_env: None,
69 // Keep the default below the plugin bridge timeout to avoid bridge-killed
70 // semantic_search requests when callers do not set an explicit timeout.
71 timeout_ms: 25_000,
72 max_batch_size: 64,
73 }
74 }
75}
76
77pub const DEFAULT_SEMANTIC_MODEL: &str = "all-MiniLM-L6-v2";
78
79impl Config {
80 pub fn semantic_backend_label(&self) -> &'static str {
81 self.semantic.backend.as_str()
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(default)]
87pub struct Config {
88 /// Root directory of the project being analyzed. `None` if not scoped.
89 pub project_root: Option<PathBuf>,
90 /// How many levels of call-graph edges to follow during validation (default: 1).
91 pub validation_depth: u32,
92 /// Hours before a checkpoint expires and is eligible for cleanup (default: 24).
93 pub checkpoint_ttl_hours: u32,
94 /// Maximum depth for recursive symbol resolution (default: 10).
95 pub max_symbol_depth: u32,
96 /// Seconds before killing a formatter subprocess (default: 10).
97 pub formatter_timeout_secs: u32,
98 /// Seconds before killing a type-checker subprocess (default: 30).
99 pub type_checker_timeout_secs: u32,
100 /// Whether to auto-format files after edits (default: true).
101 pub format_on_edit: bool,
102 /// Whether to auto-validate files after edits (default: false).
103 /// When "syntax", only tree-sitter parse check. When "full", runs type checker.
104 pub validate_on_edit: Option<String>,
105 /// Per-language formatter overrides. Keys: "typescript", "python", "rust", "go".
106 /// Values: "biome", "prettier", "deno", "ruff", "black", "rustfmt", "goimports", "gofmt", "none".
107 pub formatter: HashMap<String, String>,
108 /// Per-language type checker overrides. Keys: "typescript", "python", "rust", "go".
109 /// Values: "tsc", "biome", "pyright", "ruff", "cargo", "go", "staticcheck", "none".
110 pub checker: HashMap<String, String>,
111 /// Whether to restrict file operations to within `project_root` (default: false).
112 /// When true, write-capable commands reject paths outside the project root.
113 pub restrict_to_project_root: bool,
114 /// Enable the trigram search index (default: false).
115 pub search_index: bool,
116 /// Enable semantic search (default: false).
117 pub semantic_search: bool,
118 /// Enable experimental bash command rewriting (default: false).
119 pub experimental_bash_rewrite: bool,
120 /// Enable experimental bash command compression (default: false).
121 pub experimental_bash_compress: bool,
122 /// Enable experimental bash background execution (default: false).
123 pub experimental_bash_background: bool,
124 /// Maximum number of background bash tasks allowed to run concurrently (default: 8).
125 pub max_background_bash_tasks: usize,
126 /// Emit reminders for long-running bash tasks (default: true).
127 pub bash_long_running_reminder_enabled: bool,
128 /// Milliseconds between long-running bash reminders (default: 10 minutes).
129 pub bash_long_running_reminder_interval_ms: u64,
130 /// Enable OpenCode-style bash permission prompts (default: false).
131 pub bash_permissions: bool,
132 /// Maximum file size to fully index in bytes (default: 1MB).
133 pub search_index_max_file_size: u64,
134 /// Maximum number of source files allowed for call-graph operations
135 /// (`callers`, `trace_to`, `trace_data`, `impact`). When a project
136 /// exceeds this count the reverse index is not built and those
137 /// commands return a `project_too_large` error. Does not affect
138 /// `grep`, `glob`, `read`, `edit`, or other non-callgraph features.
139 /// Default: 5_000 (matches measured per-op cost ceilings; raise for
140 /// very large projects if you accept multi-minute per-call latency).
141 pub max_callgraph_files: usize,
142 pub semantic: SemanticBackendConfig,
143 /// Enable Astral ty as an experimental Python LSP server (default: false).
144 pub experimental_lsp_ty: bool,
145 /// User-defined LSP servers registered by the OpenCode plugin.
146 pub lsp_servers: Vec<UserServerDef>,
147 /// Lowercase LSP server IDs disabled by user config.
148 pub disabled_lsp: HashSet<String>,
149 /// Extra directories to search when resolving LSP binaries.
150 /// The plugin populates these from its own auto-install cache (e.g.
151 /// `~/.cache/aft/lsp-packages/<pkg>/node_modules/.bin/`) so a binary AFT
152 /// installed itself is discoverable without needing it on PATH.
153 /// Resolution order: `<project_root>/node_modules/.bin/<bin>` →
154 /// `lsp_paths_extra/<bin>` (in order) → PATH via `which`.
155 pub lsp_paths_extra: Vec<PathBuf>,
156 /// Binary names the hosting plugin knows how to auto-install.
157 ///
158 /// Built-in LSPs discovered from files only emit missing-binary warnings
159 /// when their binary is in this set. User-configured `lsp_servers` keep
160 /// warning unconditionally.
161 pub lsp_auto_install_binaries: HashSet<String>,
162 /// Binary names with plugin-managed auto-installs currently in flight.
163 ///
164 /// Missing-binary warnings are suppressed while the install is actively
165 /// running; install failure reporting is handled by the plugin after the
166 /// background work settles.
167 pub lsp_inflight_installs: HashSet<String>,
168 /// Persistent storage directory for indexes (trigram, semantic).
169 /// Set by the plugin to the XDG-compliant path (e.g. ~/.local/share/opencode/storage/plugin/aft/).
170 /// Falls back to ~/.cache/aft/ if not set.
171 pub storage_dir: Option<PathBuf>,
172 /// Hosting harness identity supplied by configure.
173 #[serde(default)]
174 pub harness: Option<Harness>,
175 /// Maximum number of (server, file) entries kept in the in-memory
176 /// diagnostic cache. Older entries are evicted in LRU order when the
177 /// cap is exceeded. Set to 0 to disable the cap entirely.
178 /// Default: 5000 (covers very large monorepos with bounded memory).
179 pub diagnostic_cache_size: usize,
180}
181
182impl Default for Config {
183 fn default() -> Self {
184 Config {
185 project_root: None,
186 validation_depth: 1,
187 checkpoint_ttl_hours: 24,
188 max_symbol_depth: 10,
189 formatter_timeout_secs: 10,
190 type_checker_timeout_secs: 30,
191 format_on_edit: true,
192 validate_on_edit: None,
193 formatter: HashMap::new(),
194 checker: HashMap::new(),
195 // Default to false to match OpenCode's existing permission-based model.
196 // The plugin opts into root restriction explicitly when desired.
197 restrict_to_project_root: false,
198 search_index: false,
199 semantic_search: false,
200 experimental_bash_rewrite: false,
201 experimental_bash_compress: false,
202 experimental_bash_background: false,
203 max_background_bash_tasks: 8,
204 bash_long_running_reminder_enabled: true,
205 bash_long_running_reminder_interval_ms: 600_000,
206 bash_permissions: false,
207 search_index_max_file_size: 1_048_576,
208 // Projects larger than this skip call-graph reverse index construction.
209 //
210 // The previous default (20_000) was set by hand-wave to "fits under
211 // the 30 s bridge timeout" without measurement. Direct benchmarks
212 // showed the cost is super-linear (tree-sitter parse + reverse-index
213 // build per file): a 6.8K-file Rust project took 41 s — already past
214 // the 60 s per-callgraph-op timeout. At 10 K extrapolated cost is
215 // ~80–100 s; at 20 K it's 5+ minutes. So the old default routinely
216 // produced "timed out, restarting bridge" rather than a clean
217 // `project_too_large` rejection.
218 //
219 // 5_000 reflects measured reality: at this size, callgraph
220 // operations on a real Rust/TS project complete in roughly 30–40 s,
221 // matching the per-op timeout budget. Users with bigger projects
222 // can raise this knob, but the default should not advertise
223 // capabilities that fail in practice. Read/edit/grep/glob/outline/
224 // semantic_search/AST/LSP all remain unaffected by this cap —
225 // it only gates `aft_navigate` and `aft_refactor op="move"`.
226 max_callgraph_files: 5_000,
227 semantic: SemanticBackendConfig::default(),
228 experimental_lsp_ty: false,
229 lsp_servers: Vec::new(),
230 disabled_lsp: HashSet::new(),
231 lsp_paths_extra: Vec::new(),
232 lsp_auto_install_binaries: HashSet::new(),
233 lsp_inflight_installs: HashSet::new(),
234 storage_dir: None,
235 harness: None,
236 diagnostic_cache_size: 5000,
237 }
238 }
239}