1use crate::error::{KernelError, Result};
11use serde::{Deserialize, Serialize};
12use std::path::Path;
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(default, deny_unknown_fields)]
16pub struct KernelPolicy {
17 pub working_set_limit: usize,
18 pub recall_limit: usize,
19 pub minimum_recall_score: f32,
21 pub allow_sensitive_persistence: bool,
22 pub consolidation_min_support: usize,
23 pub consolidation_min_independent_sources: usize,
24 pub consolidation_min_confidence: f32,
26 pub calibration_min_outcomes: usize,
29 pub recall: RecallWeights,
31}
32
33impl Default for KernelPolicy {
34 fn default() -> Self {
35 Self {
36 working_set_limit: 12,
37 recall_limit: 8,
38 minimum_recall_score: 0.12,
39 allow_sensitive_persistence: false,
40 consolidation_min_support: 2,
41 consolidation_min_independent_sources: 2,
42 consolidation_min_confidence: 0.6,
43 calibration_min_outcomes: crate::calibration::MIN_OUTCOMES,
44 recall: RecallWeights::default(),
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(default, deny_unknown_fields)]
68pub struct RecallWeights {
69 pub lexical_coverage: f32,
70 pub lexical_jaccard: f32,
71 pub match_floor: f32,
73 pub cue_lexical: f32,
74 pub cue_tag: f32,
75 pub meta_recency: f32,
76 pub meta_salience: f32,
77 pub meta_confidence: f32,
78 pub meta_provenance: f32,
79 pub meta_prospective: f32,
80 pub relevance_base: f32,
81 pub metadata_share: f32,
82 pub recency_scale_days: f64,
83 pub unverified_provenance_factor: f32,
84 pub prospective_scale_days: f64,
85 pub undated_commitment_urgency: f32,
86 pub overdue_floor: f32,
87 pub listing_scale: f32,
88 pub redundant_at: f32,
89 pub redundancy_penalty: f32,
90}
91
92impl Default for RecallWeights {
93 fn default() -> Self {
94 Self {
95 lexical_coverage: 0.7,
96 lexical_jaccard: 0.3,
97 match_floor: 0.15,
98 cue_lexical: 0.8,
99 cue_tag: 0.2,
100 meta_recency: 0.25,
101 meta_salience: 0.20,
102 meta_confidence: 0.20,
103 meta_provenance: 0.25,
104 meta_prospective: 0.10,
105 relevance_base: 0.55,
106 metadata_share: 0.45,
107 recency_scale_days: 30.0,
108 unverified_provenance_factor: 0.65,
109 prospective_scale_days: 7.0,
110 undated_commitment_urgency: 0.35,
111 overdue_floor: 0.15,
112 listing_scale: 0.5,
113 redundant_at: 0.8,
114 redundancy_penalty: 0.35,
115 }
116 }
117}
118
119impl KernelPolicy {
120 pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self> {
123 let path = path.as_ref();
124 let text = std::fs::read_to_string(path).map_err(|error| {
125 KernelError::Invalid(format!("cannot read policy {}: {error}", path.display()))
126 })?;
127 Self::from_json(&text)
128 .map_err(|error| KernelError::Invalid(format!("policy {}: {error}", path.display())))
129 }
130
131 pub fn from_json(text: &str) -> Result<Self> {
136 let policy: Self = serde_json::from_str(text)?;
137 if policy.allow_sensitive_persistence {
138 return Err(KernelError::Invalid(
139 "policy field allow_sensitive_persistence cannot be enabled from a policy file; the append-only ledger cannot delete sensitive data. Enable it in code alongside an encrypted, deletion-capable store".into(),
140 ));
141 }
142 policy.validate()?;
143 Ok(policy)
144 }
145
146 pub fn validate(&self) -> Result<()> {
148 let bad = |name: &str, rule: &str| {
149 Err(KernelError::Invalid(format!("policy field {name} {rule}")))
150 };
151 if self.working_set_limit == 0 || self.recall_limit == 0 {
152 return bad("working_set_limit / recall_limit", "must be at least 1");
153 }
154 let r = &self.recall;
155 let unit = [
156 ("minimum_recall_score", self.minimum_recall_score),
157 (
158 "consolidation_min_confidence",
159 self.consolidation_min_confidence,
160 ),
161 ("recall.lexical_coverage", r.lexical_coverage),
162 ("recall.lexical_jaccard", r.lexical_jaccard),
163 ("recall.match_floor", r.match_floor),
164 ("recall.cue_lexical", r.cue_lexical),
165 ("recall.cue_tag", r.cue_tag),
166 ("recall.meta_recency", r.meta_recency),
167 ("recall.meta_salience", r.meta_salience),
168 ("recall.meta_confidence", r.meta_confidence),
169 ("recall.meta_provenance", r.meta_provenance),
170 ("recall.meta_prospective", r.meta_prospective),
171 ("recall.relevance_base", r.relevance_base),
172 ("recall.metadata_share", r.metadata_share),
173 (
174 "recall.unverified_provenance_factor",
175 r.unverified_provenance_factor,
176 ),
177 (
178 "recall.undated_commitment_urgency",
179 r.undated_commitment_urgency,
180 ),
181 ("recall.overdue_floor", r.overdue_floor),
182 ("recall.listing_scale", r.listing_scale),
183 ("recall.redundant_at", r.redundant_at),
184 ("recall.redundancy_penalty", r.redundancy_penalty),
185 ];
186 for (name, value) in unit {
187 if !(0.0..=1.0).contains(&value) {
188 return bad(name, "must be between 0 and 1");
189 }
190 }
191 if self.minimum_recall_score <= 0.0 {
192 return bad(
193 "minimum_recall_score",
194 "must be above 0 (at 0, a trace with no matching cue would be recalled)",
195 );
196 }
197 if self.calibration_min_outcomes == 0 {
198 return bad(
199 "calibration_min_outcomes",
200 "must be at least 1 (at 0, a family with no outcomes would be measurable)",
201 );
202 }
203 if self.consolidation_min_support == 0 || self.consolidation_min_independent_sources == 0 {
204 return bad(
205 "consolidation_min_support / consolidation_min_independent_sources",
206 "must be at least 1",
207 );
208 }
209 for (name, a, b) in [
212 (
213 "recall.lexical_coverage + recall.lexical_jaccard",
214 r.lexical_coverage,
215 r.lexical_jaccard,
216 ),
217 (
218 "recall.cue_lexical + recall.cue_tag",
219 r.cue_lexical,
220 r.cue_tag,
221 ),
222 (
223 "recall.relevance_base + recall.metadata_share",
224 r.relevance_base,
225 r.metadata_share,
226 ),
227 ] {
228 if a + b > 1.0 + 1e-9 {
229 return bad(name, "must sum to at most 1");
230 }
231 }
232 if r.relevance_base <= 0.0 {
233 return bad(
234 "recall.relevance_base",
235 "must be above 0 (relevance must always count)",
236 );
237 }
238 for (name, value) in [
239 ("recall.recency_scale_days", r.recency_scale_days),
240 ("recall.prospective_scale_days", r.prospective_scale_days),
241 ] {
242 if !value.is_finite() || value <= 0.0 {
243 return bad(name, "must be a positive number of days");
244 }
245 }
246 Ok(())
247 }
248}