Skip to main content

writerslogic_warm/core/
config.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, Default)]
7pub struct WarmConfig {
8    pub text_encoder: super::encoding::TextEncoder,
9    pub ivf: IVFConfig,
10    pub nsg: NSGConfig,
11    pub shard: ShardConfig,
12    pub query: QueryConfig,
13    pub concepts: ConceptsConfig,
14    pub diffusion: DiffusionConfig,
15    pub security: SecurityConfig,
16    pub provenance: ProvenanceConfig,
17    pub privacy: PrivacyConfig,
18    pub meaning: MeaningConfig,
19    pub cognition: CognitionConfig,
20    pub hopfield: super::hopfield::HopfieldConfig,
21    pub embedding_space: Option<super::schema::EmbeddingSpace>,
22    pub document_checkpoint: DocumentCheckpointConfig,
23    /// Options of the local embedder `TextEncoder::LocalEmbedV1` loads (default options when
24    /// absent). Ignored by the other encoders.
25    #[cfg(feature = "local-embed")]
26    pub local_embed: Option<super::embed::EmbedOptions>,
27}
28
29/// Journal checkpoints (`documents.ckpt`, see `docs/file-format/CHECKPOINT.md`): a compaction
30/// that writes the document store as one mapped file in place of the journal frames before it,
31/// so a store opens by mapping it and serves unfiltered lexical queries from the mapping.
32#[derive(Clone, Debug)]
33pub struct DocumentCheckpointConfig {
34    /// Write checkpoints. A store that has one opens from it either way: it is the store's data.
35    pub enabled: bool,
36    /// Journal bytes after the last checkpoint at or above which `flush` or closing the engine
37    /// writes a new one: below it, replaying the frames is about as fast.
38    pub min_journal_bytes: usize,
39}
40
41impl Default for DocumentCheckpointConfig {
42    fn default() -> Self {
43        Self {
44            enabled: true,
45            min_journal_bytes: 256 << 10,
46        }
47    }
48}
49
50#[derive(Clone, Debug)]
51pub struct MeaningConfig {
52    pub enabled: bool,
53    pub beta: f64,
54    pub algebraic_max_fanout: usize,
55    pub auto_decompose: bool,
56    pub max_hop_depth: usize,
57    pub max_rule_depth: usize,
58    pub idf_clip_factor: f64,
59}
60
61impl Default for MeaningConfig {
62    fn default() -> Self {
63        Self {
64            enabled: false,
65            beta: 24.0,
66            algebraic_max_fanout: 40,
67            auto_decompose: false,
68            max_hop_depth: 10,
69            max_rule_depth: 10,
70            idf_clip_factor: 3.0,
71        }
72    }
73}
74
75#[derive(Clone, Debug, Default)]
76pub struct SecurityConfig {
77    pub signing_enabled: bool,
78    pub key_path: Option<String>,
79    pub encryption_enabled: bool,
80    pub encryption_passphrase: Option<String>,
81    /// Name of an environment variable containing the encryption passphrase.
82    /// Prefer this over embedding a secret in application configuration.
83    pub encryption_passphrase_env: Option<String>,
84    pub audit_enabled: bool,
85}
86
87#[derive(Clone, Debug, Default)]
88pub struct ProvenanceConfig {
89    pub enabled: bool,
90    pub key_path: Option<String>,
91    pub auto_sign: bool,
92    pub status_list_url: Option<String>,
93    #[cfg(feature = "provenance-scitt")]
94    pub scitt_endpoint: Option<String>,
95}
96
97#[derive(Clone, Debug)]
98pub struct PrivacyConfig {
99    /// Enable epsilon-differential privacy in bundle operations.
100    pub dp_enabled: bool,
101    /// Privacy budget epsilon. Smaller = more private, noisier.
102    /// Typical range: 0.1 (strong) to 10.0 (weak).
103    pub epsilon: f64,
104}
105
106impl Default for PrivacyConfig {
107    fn default() -> Self {
108        Self {
109            dp_enabled: false,
110            epsilon: 1.0,
111        }
112    }
113}
114
115#[derive(Clone, Debug)]
116pub struct QueryConfig {
117    pub component_similarity_threshold: f64,
118    pub component_max_neighbors: u32,
119}
120
121impl Default for QueryConfig {
122    fn default() -> Self {
123        Self {
124            component_similarity_threshold: 0.05,
125            component_max_neighbors: 20,
126        }
127    }
128}
129
130#[derive(Clone, Debug)]
131pub struct ConceptsConfig {
132    pub similarity_threshold: f64,
133    pub min_cluster_size: usize,
134}
135
136impl Default for ConceptsConfig {
137    fn default() -> Self {
138        Self {
139            similarity_threshold: 0.3,
140            min_cluster_size: 3,
141        }
142    }
143}
144
145#[derive(Clone, Debug)]
146pub struct DiffusionConfig {
147    pub steps: usize,
148    pub sigma_max: f64,
149    pub sigma_min: f64,
150    pub step_size: f64,
151    pub n_langevin: usize,
152}
153
154impl Default for DiffusionConfig {
155    fn default() -> Self {
156        Self {
157            steps: 10,
158            sigma_max: 0.5,
159            sigma_min: 0.01,
160            step_size: 0.1,
161            n_langevin: 5,
162        }
163    }
164}
165
166#[derive(Clone, Debug)]
167pub struct ShardConfig {
168    pub enabled: bool,
169    pub shard_count: usize,
170    pub auto_threshold: usize,
171    pub target_shard_size: usize,
172}
173
174impl Default for ShardConfig {
175    fn default() -> Self {
176        Self {
177            enabled: false,
178            shard_count: 0,
179            auto_threshold: 1_000_000,
180            target_shard_size: 250_000,
181        }
182    }
183}
184
185#[derive(Clone, Debug, Serialize, Deserialize)]
186pub struct NSGConfig {
187    pub max_degree: usize,
188    pub ef_construction: usize,
189    pub auto_threshold: usize,
190    pub seed: u64,
191}
192
193impl Default for NSGConfig {
194    fn default() -> Self {
195        Self {
196            max_degree: 32,
197            ef_construction: 128,
198            auto_threshold: 10_000,
199            seed: 42,
200        }
201    }
202}
203
204#[derive(Clone, Debug)]
205pub struct IVFConfig {
206    /// Controls auto-training only. Manual `train_ivf()` works regardless.
207    pub enabled: bool,
208    pub n_clusters: usize,
209    pub n_landmarks: usize,
210    pub d_reduced: usize,
211    pub n_probe: usize,
212    pub auto_threshold: usize,
213}
214
215impl Default for IVFConfig {
216    fn default() -> Self {
217        Self {
218            enabled: false,
219            n_clusters: 256,
220            n_landmarks: 1024,
221            d_reduced: 128,
222            n_probe: 8,
223            auto_threshold: 10_000,
224        }
225    }
226}
227
228#[derive(Clone, Debug)]
229pub struct CognitionConfig {
230    pub enabled: bool,
231    pub interval_secs: u64,
232    pub min_pattern_freq: usize,
233    pub min_abstraction_members: usize,
234    pub min_shared_relations: usize,
235    pub min_peer_coverage: f64,
236    pub hypothesis_beta: f64,
237    pub min_hypothesis_confidence: f64,
238    pub min_analogy_relations: usize,
239    pub governor_duplicate_threshold: f64,
240    pub governor_max_scan_size: usize,
241    pub governor_forget_unreferenced: bool,
242    pub refine_atoms: bool,
243}
244
245impl Default for CognitionConfig {
246    fn default() -> Self {
247        Self {
248            enabled: false,
249            interval_secs: 60,
250            min_pattern_freq: 3,
251            min_abstraction_members: 3,
252            min_shared_relations: 2,
253            min_peer_coverage: 0.5,
254            hypothesis_beta: 24.0,
255            min_hypothesis_confidence: 0.3,
256            min_analogy_relations: 2,
257            governor_duplicate_threshold: 0.95,
258            governor_max_scan_size: 1000,
259            governor_forget_unreferenced: false,
260            refine_atoms: false,
261        }
262    }
263}
264
265impl WarmConfig {
266    pub(crate) fn validate(&self) -> anyhow::Result<()> {
267        anyhow::ensure!(
268            self.text_encoder != super::encoding::TextEncoder::MultiscaleCappedUnionV1
269                || cfg!(feature = "experimental"),
270            "capped-union text encoding requires a build with the experimental feature"
271        );
272        anyhow::ensure!(
273            self.text_encoder != super::encoding::TextEncoder::LocalEmbedV1
274                || cfg!(feature = "local-embed"),
275            "the local-embed-v1 text encoder requires a build with the local-embed feature"
276        );
277        anyhow::ensure!(
278            cfg!(feature = "security")
279                || !(self.security.signing_enabled || self.security.encryption_enabled),
280            "signing/encryption requires a build with the security feature"
281        );
282        anyhow::ensure!(
283            cfg!(feature = "provenance") || !self.provenance.enabled,
284            "provenance requires a build with the provenance feature"
285        );
286        anyhow::ensure!(
287            self.privacy.epsilon.is_finite() && self.privacy.epsilon > 0.0,
288            "privacy epsilon must be finite and positive"
289        );
290        anyhow::ensure!(
291            self.shard.target_shard_size > 0 && self.shard.shard_count <= 1024,
292            "invalid shard configuration"
293        );
294        anyhow::ensure!(
295            self.meaning.beta.is_finite() && self.meaning.beta > 0.0,
296            "meaning beta must be finite and positive"
297        );
298        if let Some(space) = &self.embedding_space {
299            anyhow::ensure!(!space.model.is_empty() && !space.revision.is_empty() && (1..=65536).contains(&space.dimensions)
300                && space.metric == "cosine" && matches!(space.normalization.as_str(), "l2" | "none"),
301                "embedding space requires model, revision, dimensions 1..=65536, normalization l2/none and metric cosine");
302        }
303        Ok(())
304    }
305}