gradatum_core/config.rs
1//! Configuration runtime chargée depuis `<vault_root>/.gradatum/config.toml`.
2//!
3//! See ARCHITECTURE.md for the configuration design.
4//!
5//! Tous les champs sont `Option<T>` avec `#[serde(default)]` pour permettre les
6//! configs partielles. Les defaults sont appliqués aux sites de consommation
7//! (ex. `NoteStatus::is_embeddable_default()` quand `embed.embeddable_status` est `None`).
8//!
9//! ## Chargement
10//!
11//! ```rust,no_run
12//! use gradatum_core::config::VaultConfig;
13//! use std::path::Path;
14//!
15//! let cfg = VaultConfig::load_from_root(Path::new("/my/vault")).unwrap();
16//! ```
17//!
18//! Fichier absent → `VaultConfig::default()` sans erreur.
19//! TOML malformé → `ConfigError::Parse`.
20
21use serde::{Deserialize, Serialize};
22use std::path::Path;
23
24/// Configuration complète d'un vault Gradatum.
25///
26/// Chargée depuis `<vault_root>/.gradatum/config.toml`. Toutes les sections
27/// sont optionnelles — un fichier minimal peut ne contenir que `[vault]`.
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29pub struct VaultConfig {
30 /// Paramètres généraux du vault (tenant, version schéma).
31 #[serde(default)]
32 pub vault: VaultSection,
33
34 /// Configuration du pipeline d'embedding (D-perf-1, B21).
35 #[serde(default)]
36 pub embed: EmbedConfig,
37
38 /// Configuration du curator (D-perf-3, B23).
39 #[serde(default)]
40 pub curator: CuratorConfig,
41
42 /// Configuration du moteur d'indexation.
43 #[serde(default)]
44 pub index: IndexConfig,
45
46 /// Configuration du drift detector.
47 #[serde(default)]
48 pub drift: DriftConfig,
49
50 /// Configuration de l'audit log.
51 #[serde(default)]
52 pub audit: AuditConfig,
53
54 /// Configuration de la politique de rétention des snapshots `.history/`.
55 #[serde(default)]
56 pub history: HistoryConfig,
57}
58
59/// Section `[vault]` — identité du vault.
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct VaultSection {
62 /// Tenant par défaut. `None` → "main" appliqué par le storage layer.
63 pub default_tenant_id: Option<String>,
64
65 /// Version du schéma SQLite attendue. `None` → pas de vérification stricte.
66 pub schema_version: Option<u32>,
67}
68
69/// Section `[embed]` — pipeline d'embedding (D-perf-1, B21).
70///
71/// Contrôle quel backend est utilisé, avec quel modèle, et quels statuts de
72/// notes sont éligibles à l'embedding.
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
74pub struct EmbedConfig {
75 /// Statuts de notes pouvant être embeddés (kebab-case, ex. `["live", "pending-review"]`).
76 ///
77 /// `None` → utiliser `NoteStatus::is_embeddable_default()`.
78 ///
79 /// **Choix architectural** : `Vec<String>` (pas `Vec<NoteStatus>`) pour que `config.rs`
80 /// reste libre de tout type métier et évite tout cycle de dépendance.
81 /// La comparaison s'effectue dans `NoteStatus::is_embeddable(&EmbedConfig)` via
82 /// `serde_kebab_repr()`. Décision T03b 2026-05-04.
83 pub embeddable_status: Option<Vec<String>>,
84
85 /// Identifiant du modèle d'embedding (ex. "bge-m3", "bge-small-en-v1.5").
86 pub embedder_id: Option<String>,
87
88 /// Dimensions du vecteur de sortie. `None` → inféré depuis `embedder_id`.
89 pub dim: Option<u16>,
90
91 /// Backend d'embedding sélectionné (D-perf-1, B21).
92 ///
93 /// Valeurs : `"http"` | `"fastembed"` | `"noop"`. `None` → "http".
94 pub backend: Option<String>,
95
96 /// Backend de fallback si le backend principal est indisponible.
97 pub fallback_backend: Option<String>,
98
99 /// URL du backend HTTP. Requise si `backend = "http"`.
100 pub http_url: Option<String>,
101
102 /// Timeout en millisecondes pour les requêtes HTTP d'embedding.
103 pub http_timeout_ms: Option<u32>,
104
105 /// Nom du modèle passé dans la requête HTTP.
106 pub http_model: Option<String>,
107}
108
109/// Section `[curator]` — configuration du pipeline de curation (D-perf-3, B23).
110///
111/// Contrôle les seuils heuristiques et le passage en revue LLM pour les notes
112/// de faible confiance.
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114pub struct CuratorConfig {
115 /// Seuil heuristique d'admission directe (0.0–1.0).
116 /// Au-dessus → admettre sans revue LLM.
117 pub heuristic_admit_threshold: Option<f32>,
118
119 /// Statut assigné par défaut par l'heuristique (kebab-case string).
120 ///
121 /// **Choix architectural** : `String` (pas `NoteStatus`) pour que `config.rs`
122 /// reste libre de tout type métier. La résolution s'effectue dans `gradatum-chat`
123 /// par comparaison kebab-case. Décision T03b 2026-05-04.
124 pub heuristic_default_status: Option<String>,
125
126 /// Active la revue LLM pour les notes sous `confidence_threshold`.
127 pub llm_review_enabled: Option<bool>,
128
129 /// Seuil de confiance sous lequel la revue LLM est déclenchée.
130 pub confidence_threshold: Option<f32>,
131
132 /// URL de l'endpoint LLM pour la revue (compatible OpenAI Chat API).
133 pub llm_review_endpoint: Option<String>,
134
135 /// Modèle LLM utilisé pour la revue.
136 pub llm_review_model: Option<String>,
137
138 /// Timeout en millisecondes pour les appels LLM de revue.
139 pub llm_review_timeout_ms: Option<u32>,
140
141 /// Nombre maximum de tokens générés par le LLM de revue.
142 pub llm_review_max_tokens: Option<u32>,
143
144 /// Comportement en cas d'échec ou de timeout LLM.
145 ///
146 /// Valeurs : `"pending-review-fallback"` | `"reject"` | `"admit-pending-review"`.
147 pub llm_review_fallback: Option<String>,
148}
149
150/// Section `[index]` — configuration du moteur d'indexation.
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152pub struct IndexConfig {
153 /// Backend d'index. Valeurs : `"sqlite"`. `None` → "sqlite".
154 pub backend: Option<String>,
155
156 /// Tokeniseur FTS5 pour la recherche plein texte.
157 ///
158 /// Valeurs : `"unicode61"` | `"ascii"` | `"porter"`. `None` → "unicode61".
159 pub fts_tokenizer: Option<String>,
160}
161
162/// Section `[drift]` — configuration du drift detector.
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164pub struct DriftConfig {
165 /// Intervalle entre deux scans de drift en secondes. `None` → 3600.
166 pub scan_interval_seconds: Option<u32>,
167}
168
169/// Section `[history]` — politique de rétention des snapshots CoW.
170///
171/// Contrôle combien de snapshots `.history/<id>/` sont conservés par note
172/// et combien de jours ils sont retenus.
173///
174/// ## Defaults
175///
176/// Sans `[history]` dans le TOML, les valeurs par défaut s'appliquent :
177/// - `max_versions = 50` — cap count
178/// - `ttl_days = None` — pas de purge par âge
179///
180/// ## Ordre d'application
181///
182/// 1. **TTL d'abord** : les snapshots plus vieux que `ttl_days` jours sont
183/// supprimés, quelle que soit la valeur de `max_versions`.
184/// 2. **Cap count ensuite** : si le nombre restant dépasse encore `max_versions`,
185/// les plus anciens (timestamps les plus petits) sont supprimés.
186///
187/// Cet ordre garantit que les snapshots retenus après TTL sont toujours les
188/// `max_versions` les plus récents. Il est déterministe et idempotent.
189///
190/// ## Exemple TOML
191///
192/// ```toml
193/// [history]
194/// # Conserver au maximum 20 versions par note (défaut : 50)
195/// max_versions = 20
196/// # Purger les snapshots de plus de 90 jours (défaut : aucune purge)
197/// ttl_days = 90
198/// ```
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct HistoryConfig {
201 /// Nombre maximum de snapshots CoW conservés par note.
202 ///
203 /// Après chaque écriture CoW réussie, si le nombre de snapshots excède
204 /// cette limite, les plus anciens sont supprimés.
205 ///
206 /// Valeur par défaut : `50`. Une valeur de `0` est interprétée comme `1`
207 /// (au moins un snapshot est toujours conservé si le CoW a réussi).
208 pub max_versions: usize,
209
210 /// Durée de rétention des snapshots en jours.
211 ///
212 /// `None` (défaut) — aucune purge par âge, seul `max_versions` s'applique.
213 /// `Some(n)` — les snapshots dont le timestamp est antérieur à
214 /// `maintenant - n jours` sont supprimés avant le cap count.
215 pub ttl_days: Option<u32>,
216}
217
218impl Default for HistoryConfig {
219 /// Retourne les valeurs par défaut : `max_versions = 50`, `ttl_days = None`.
220 ///
221 /// Ces valeurs conservent 50 snapshots maximum par note sans purge par âge.
222 fn default() -> Self {
223 Self {
224 max_versions: 50,
225 ttl_days: None,
226 }
227 }
228}
229
230/// Section `[audit]` — configuration de l'audit log.
231///
232/// Contrôle la rotation, la rétention et le mode de fsync des events d'audit.
233#[derive(Debug, Clone, Default, Serialize, Deserialize)]
234pub struct AuditConfig {
235 /// Politique de rotation du log d'audit.
236 ///
237 /// Valeurs : `"daily"` | `"weekly"` | `"size-100mb"`. `None` → "daily".
238 pub rotation: Option<String>,
239
240 /// Nombre de jours de rétention. `0` = rétention infinie. `None` → 30.
241 pub retention_days: Option<u32>,
242
243 /// Mode strict de fsync.
244 ///
245 /// `false` (défaut) = BufWriter 64 KB + fsync toutes les 100 ms ou 100 events.
246 /// `true` = fsync par event, bypass buffer (~200 µs/event NVMe — forensic-grade).
247 #[serde(default)]
248 pub strict_mode: bool,
249}
250
251impl VaultConfig {
252 /// Charge `<vault_root>/.gradatum/config.toml`.
253 ///
254 /// - Fichier absent → `Ok(VaultConfig::default())`.
255 /// - TOML malformé → `Err(ConfigError::Parse(...))`.
256 /// - Erreur IO autre que NotFound → `Err(ConfigError::Io(...))`.
257 ///
258 /// # Panics
259 ///
260 /// Jamais. Toutes les erreurs sont propagées via `Result`.
261 pub fn load_from_root(root: &Path) -> Result<Self, ConfigError> {
262 let path = root.join(".gradatum").join("config.toml");
263 match std::fs::read_to_string(&path) {
264 Ok(content) => toml::from_str(&content).map_err(ConfigError::Parse),
265 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
266 Err(e) => Err(ConfigError::Io(e)),
267 }
268 }
269}
270
271/// Erreurs de chargement de la configuration.
272#[derive(Debug, thiserror::Error)]
273pub enum ConfigError {
274 /// Erreur IO (permissions, chemin invalide, etc.).
275 #[error("config IO: {0}")]
276 Io(#[from] std::io::Error),
277
278 /// TOML malformé ou champ de type incorrect.
279 #[error("config parse: {0}")]
280 Parse(#[from] toml::de::Error),
281}