Skip to main content

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}