Skip to main content

knowledge_runtime/
config.rs

1use crate::error::RuntimeError;
2use crate::ids::Scope;
3use serde::{Deserialize, Serialize};
4
5/// Runtime-level hint for semantic-memory candidate backend selection.
6///
7/// This is intentionally a hint/trace value only. Knowledge Runtime does not
8/// construct or depend on proveKV/poly-kv backends directly.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "snake_case")]
11pub enum RuntimeCandidateBackendHint {
12    SemanticMemoryDefault,
13    ProveKvPoolCandidate,
14}
15
16impl Default for RuntimeCandidateBackendHint {
17    fn default() -> Self {
18        Self::SemanticMemoryDefault
19    }
20}
21
22/// Top-level runtime configuration.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct RuntimeConfig {
25    /// Default scope for queries that don't supply one.
26    pub default_scope: Scope,
27
28    /// Query pipeline settings.
29    #[serde(default)]
30    pub query: QueryConfig,
31
32    /// Entity registry settings.
33    #[serde(default)]
34    pub entity: EntityConfig,
35
36    /// Projection lifecycle settings.
37    #[serde(default)]
38    pub projection: ProjectionConfig,
39
40    /// When true, temporal queries that cannot be executed on a supported
41    /// projection-backed route return `RuntimeError::TemporalNotSupported`
42    /// instead of downgrading to hybrid retrieval with a warning.
43    ///
44    /// Default: `false` (downgrade with warning).
45    #[serde(default)]
46    pub strict_temporal: bool,
47
48    /// When true, queries whose scope includes dimensions beyond namespace
49    /// (domain, workspace_id, repo_id) return `RuntimeError::ScopeNotFullyEnforced`
50    /// instead of proceeding with namespace-only hybrid fallback.
51    ///
52    /// Default: `false` (warn but proceed).
53    #[serde(default)]
54    pub strict_scope: bool,
55}
56
57impl RuntimeConfig {
58    /// Validate and normalize configuration. Returns error on invalid values.
59    pub fn normalize_and_validate(&mut self) -> Result<(), RuntimeError> {
60        if self.default_scope.namespace.is_empty() {
61            return Err(RuntimeError::InvalidConfig {
62                field: "default_scope.namespace",
63                reason: "namespace must not be empty".into(),
64            });
65        }
66        self.query.normalize_and_validate()?;
67        self.projection.normalize_and_validate()?;
68        Ok(())
69    }
70}
71
72/// Query pipeline tuning.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct QueryConfig {
75    /// Maximum number of search results per route leg.
76    pub max_results_per_leg: usize,
77    /// Maximum number of route legs.
78    pub max_route_legs: usize,
79    /// Default result limit when caller does not specify.
80    pub default_limit: usize,
81    /// Semantic-memory candidate backend hint to disclose in runtime traces.
82    ///
83    /// The owning semantic-memory store remains authoritative for actual
84    /// backend configuration and exact rerank enforcement.
85    #[serde(default)]
86    pub candidate_backend_hint: RuntimeCandidateBackendHint,
87}
88
89impl Default for QueryConfig {
90    fn default() -> Self {
91        Self {
92            max_results_per_leg: 20,
93            max_route_legs: 4,
94            default_limit: 10,
95            candidate_backend_hint: RuntimeCandidateBackendHint::default(),
96        }
97    }
98}
99
100impl QueryConfig {
101    fn normalize_and_validate(&mut self) -> Result<(), RuntimeError> {
102        if self.max_results_per_leg == 0 {
103            self.max_results_per_leg = 20;
104        }
105        if self.max_route_legs == 0 {
106            self.max_route_legs = 4;
107        }
108        if self.default_limit == 0 {
109            self.default_limit = 10;
110        }
111        Ok(())
112    }
113}
114
115/// Entity registry settings.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct EntityConfig {
118    /// Maximum number of aliases per entity.
119    pub max_aliases: usize,
120    /// Maximum number of entities in the registry (across all scopes).
121    pub max_entities: usize,
122}
123
124impl Default for EntityConfig {
125    fn default() -> Self {
126        Self {
127            max_aliases: 16,
128            max_entities: 10_000,
129        }
130    }
131}
132
133/// Projection lifecycle settings.
134///
135/// **Note**: Projection persistence is not implemented in this crate today.
136/// The `persist` field is retained solely for serde backward compatibility
137/// with existing config files; setting it to `true` is rejected at validation
138/// time because runtime projections remain derived and rebuildable.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ProjectionConfig {
141    /// Staleness threshold in seconds. Projections older than this are
142    /// candidates for rebuild.
143    pub staleness_threshold_secs: u64,
144    /// Import staleness threshold in seconds. If the last successful import
145    /// for a namespace is older than this, the runtime emits a
146    /// `ProjectionImportStale` warning during query execution.
147    /// Set to 0 to disable import staleness checking.
148    pub import_staleness_threshold_secs: u64,
149    /// Whether to persist projections to SQLite.
150    ///
151    /// Not implemented in this crate today. The runtime's projections
152    /// are derived, non-authoritative, and rebuildable from upstream
153    /// `semantic-memory` state, so enabling durable persistence here is
154    /// outside the current contract.
155    ///
156    /// This field is retained only for serde backward compatibility with
157    /// config files that already include it. Setting `persist: true` will
158    /// cause config validation to return `InvalidConfig`.
159    #[serde(default)]
160    pub persist: bool,
161}
162
163impl Default for ProjectionConfig {
164    fn default() -> Self {
165        Self {
166            staleness_threshold_secs: 3600,
167            import_staleness_threshold_secs: 3600,
168            persist: false,
169        }
170    }
171}
172
173impl ProjectionConfig {
174    fn normalize_and_validate(&mut self) -> Result<(), RuntimeError> {
175        if self.persist {
176            return Err(RuntimeError::InvalidConfig {
177                field: "projection.persist",
178                reason: "projection persistence is not implemented in this crate today; \
179                         projections are derived, non-authoritative, and rebuildable \
180                         from upstream semantic-memory state; \
181                         set persist=false or omit the field"
182                    .into(),
183            });
184        }
185        if self.staleness_threshold_secs == 0 {
186            self.staleness_threshold_secs = 3600;
187        }
188        Ok(())
189    }
190}