1use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5
6mod auth;
7mod chunker;
8mod defaults;
9mod endpoints;
10pub mod keychain;
11mod scope_filter;
12mod search;
13#[doc(hidden)]
14pub mod test_literals;
15use crate::indexer::build_include_globset;
16
17pub use auth::{CredentialEntry, CredentialsConfig, EndpointAuthConfig, ResolvedAuth};
18pub use chunker::ChunkerConfig;
19pub use endpoints::{
20 ChatAdapter, ChatAskConfig, ChatExpansionConfig, ChatSection, EmbeddingAdapter,
21 EmbeddingConfig, McpConfig, McpHooksConfig, RerankAdapter, RerankConfig, RerankScoreScale,
22};
23pub use scope_filter::ScopeFilter;
24pub use search::{InspectConfig, SearchConfig};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
28#[serde(rename_all = "lowercase")]
29pub enum ScopePriority {
30 Boosted,
32 Elevated,
34 #[default]
36 Normal,
37 Muted,
39 Buried,
41}
42
43impl ScopePriority {
44 #[must_use]
46 pub const fn multiplier(self) -> f64 {
47 match self {
48 Self::Boosted => 1.2,
49 Self::Elevated => 1.1,
50 Self::Normal => 1.0,
51 Self::Muted => 0.85,
52 Self::Buried => 0.5,
53 }
54 }
55
56 #[must_use]
63 pub fn apply_to_score(self, score: f64) -> f64 {
64 apply_scope_multiplier(score, self.multiplier())
65 }
66
67 #[must_use]
72 pub fn apply_to_score_with_explicit(self, score: f64, explicitly_requested: bool) -> f64 {
73 let multiplier = if explicitly_requested {
74 self.multiplier().max(Self::Normal.multiplier())
75 } else {
76 self.multiplier()
77 };
78 apply_scope_multiplier(score, multiplier)
79 }
80}
81
82fn apply_scope_multiplier(score: f64, multiplier: f64) -> f64 {
83 const POSITIVE_BOOST_RELEVANCE_FLOOR: f64 = 0.4;
84 if multiplier > 1.0 && score < POSITIVE_BOOST_RELEVANCE_FLOOR {
85 score
86 } else {
87 score * multiplier
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct ScopeResolution {
94 pub priority: ScopePriority,
96 pub default: bool,
98}
99
100impl Default for ScopeResolution {
101 fn default() -> Self {
102 Self {
103 priority: ScopePriority::Normal,
104 default: true,
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(untagged)]
112pub enum ScopeGlob {
113 Single(String),
115 Multiple(Vec<String>),
117}
118
119impl ScopeGlob {
120 #[must_use]
122 pub fn patterns(&self) -> Vec<&str> {
123 match self {
124 Self::Single(g) => vec![g.as_str()],
125 Self::Multiple(g) => g.iter().map(String::as_str).collect(),
126 }
127 }
128}
129
130pub type ScopesConfig = indexmap::IndexMap<String, Scope>;
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(deny_unknown_fields)]
140pub struct Scope {
141 pub glob: ScopeGlob,
143 pub priority: ScopePriority,
145 pub default: bool,
147 #[serde(default = "default_true")]
154 pub inspect: bool,
155}
156
157const fn default_true() -> bool {
158 true
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct TalonConfig {
165 pub vault_path: PathBuf,
167 pub db_path: PathBuf,
169 #[serde(skip)]
171 pub config_file_path: Option<PathBuf>,
172 #[serde(default)]
174 pub include_patterns: Vec<String>,
175 #[serde(default)]
177 pub ignore_patterns: Vec<String>,
178 #[serde(default)]
180 pub credentials: CredentialsConfig,
181 pub embedding: EmbeddingConfig,
183 pub rerank: RerankConfig,
185 pub chat: ChatSection,
187 #[serde(default)]
189 pub mcp: McpConfig,
190 #[serde(default)]
192 pub scopes: ScopesConfig,
193 #[serde(default)]
195 pub search: SearchConfig,
196 #[serde(default)]
198 pub inspect: InspectConfig,
199 #[serde(default, rename = "indexer")]
201 pub chunker: ChunkerConfig,
202}
203
204impl TalonConfig {
205 #[must_use]
207 pub fn vault_path(&self) -> &Path {
208 &self.vault_path
209 }
210
211 #[must_use]
213 pub fn db_path(&self) -> &Path {
214 &self.db_path
215 }
216
217 #[must_use]
222 pub fn resolve_scope(&self, path: &Path) -> ScopeResolution {
223 for scope in self.scopes.values() {
224 if matches_path_glob(path, &scope.glob) {
225 return ScopeResolution {
226 priority: scope.priority,
227 default: scope.default,
228 };
229 }
230 }
231 ScopeResolution::default()
233 }
234
235 #[must_use]
238 pub fn resolve_scope_name(&self, path: &Path) -> Option<&str> {
239 for (name, scope) in &self.scopes {
240 if matches_path_glob(path, &scope.glob) {
241 return Some(name.as_str());
242 }
243 }
244 None
245 }
246
247 #[must_use]
255 pub fn inspect_excluded(&self, path: &Path) -> bool {
256 let path_str = path.to_string_lossy();
257 let ignored = self
258 .inspect
259 .ignore
260 .iter()
261 .any(|glob| glob_matches_path(glob, path_str.as_ref()));
262 if ignored {
263 return true;
264 }
265 for scope in self.scopes.values() {
266 if matches_path_glob(path, &scope.glob) {
267 return !scope.inspect;
268 }
269 }
270 false
271 }
272
273 #[must_use]
275 pub fn default_scope_names(&self) -> Vec<&String> {
276 self.scopes
277 .iter()
278 .filter(|(_, s)| s.default)
279 .map(|(n, _)| n)
280 .collect()
281 }
282
283 pub fn get_scope(&self, name: &str) -> Result<&Scope, crate::error::TalonError> {
289 self.scopes
290 .get(name)
291 .ok_or_else(|| crate::error::TalonError::InvalidScope {
292 name: name.to_string(),
293 })
294 }
295}
296
297fn matches_path_glob(path: &Path, glob: &ScopeGlob) -> bool {
299 let path_str = path.to_string_lossy();
300 glob.patterns()
301 .iter()
302 .any(|pattern| glob_matches_path(pattern, path_str.as_ref()))
303}
304
305fn glob_matches_path(pattern: &str, path: &str) -> bool {
306 build_include_globset(&[pattern.to_string()]).is_ok_and(|set| set.is_match(path))
307}