use crate::error::{KernelError, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct KernelPolicy {
pub working_set_limit: usize,
pub recall_limit: usize,
pub minimum_recall_score: f32,
pub allow_sensitive_persistence: bool,
pub consolidation_min_support: usize,
pub consolidation_min_independent_sources: usize,
pub consolidation_min_confidence: f32,
pub calibration_min_outcomes: usize,
pub recall: RecallWeights,
}
impl Default for KernelPolicy {
fn default() -> Self {
Self {
working_set_limit: 12,
recall_limit: 8,
minimum_recall_score: 0.12,
allow_sensitive_persistence: false,
consolidation_min_support: 2,
consolidation_min_independent_sources: 2,
consolidation_min_confidence: 0.6,
calibration_min_outcomes: crate::calibration::MIN_OUTCOMES,
recall: RecallWeights::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RecallWeights {
pub lexical_coverage: f32,
pub lexical_jaccard: f32,
pub match_floor: f32,
pub cue_lexical: f32,
pub cue_tag: f32,
pub meta_recency: f32,
pub meta_salience: f32,
pub meta_confidence: f32,
pub meta_provenance: f32,
pub meta_prospective: f32,
pub relevance_base: f32,
pub metadata_share: f32,
pub recency_scale_days: f64,
pub unverified_provenance_factor: f32,
pub prospective_scale_days: f64,
pub undated_commitment_urgency: f32,
pub overdue_floor: f32,
pub listing_scale: f32,
pub redundant_at: f32,
pub redundancy_penalty: f32,
}
impl Default for RecallWeights {
fn default() -> Self {
Self {
lexical_coverage: 0.7,
lexical_jaccard: 0.3,
match_floor: 0.15,
cue_lexical: 0.8,
cue_tag: 0.2,
meta_recency: 0.25,
meta_salience: 0.20,
meta_confidence: 0.20,
meta_provenance: 0.25,
meta_prospective: 0.10,
relevance_base: 0.55,
metadata_share: 0.45,
recency_scale_days: 30.0,
unverified_provenance_factor: 0.65,
prospective_scale_days: 7.0,
undated_commitment_urgency: 0.35,
overdue_floor: 0.15,
listing_scale: 0.5,
redundant_at: 0.8,
redundancy_penalty: 0.35,
}
}
}
impl KernelPolicy {
pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|error| {
KernelError::Invalid(format!("cannot read policy {}: {error}", path.display()))
})?;
Self::from_json(&text)
.map_err(|error| KernelError::Invalid(format!("policy {}: {error}", path.display())))
}
pub fn from_json(text: &str) -> Result<Self> {
let policy: Self = serde_json::from_str(text)?;
if policy.allow_sensitive_persistence {
return Err(KernelError::Invalid(
"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(),
));
}
policy.validate()?;
Ok(policy)
}
pub fn validate(&self) -> Result<()> {
let bad = |name: &str, rule: &str| {
Err(KernelError::Invalid(format!("policy field {name} {rule}")))
};
if self.working_set_limit == 0 || self.recall_limit == 0 {
return bad("working_set_limit / recall_limit", "must be at least 1");
}
let r = &self.recall;
let unit = [
("minimum_recall_score", self.minimum_recall_score),
(
"consolidation_min_confidence",
self.consolidation_min_confidence,
),
("recall.lexical_coverage", r.lexical_coverage),
("recall.lexical_jaccard", r.lexical_jaccard),
("recall.match_floor", r.match_floor),
("recall.cue_lexical", r.cue_lexical),
("recall.cue_tag", r.cue_tag),
("recall.meta_recency", r.meta_recency),
("recall.meta_salience", r.meta_salience),
("recall.meta_confidence", r.meta_confidence),
("recall.meta_provenance", r.meta_provenance),
("recall.meta_prospective", r.meta_prospective),
("recall.relevance_base", r.relevance_base),
("recall.metadata_share", r.metadata_share),
(
"recall.unverified_provenance_factor",
r.unverified_provenance_factor,
),
(
"recall.undated_commitment_urgency",
r.undated_commitment_urgency,
),
("recall.overdue_floor", r.overdue_floor),
("recall.listing_scale", r.listing_scale),
("recall.redundant_at", r.redundant_at),
("recall.redundancy_penalty", r.redundancy_penalty),
];
for (name, value) in unit {
if !(0.0..=1.0).contains(&value) {
return bad(name, "must be between 0 and 1");
}
}
if self.minimum_recall_score <= 0.0 {
return bad(
"minimum_recall_score",
"must be above 0 (at 0, a trace with no matching cue would be recalled)",
);
}
if self.calibration_min_outcomes == 0 {
return bad(
"calibration_min_outcomes",
"must be at least 1 (at 0, a family with no outcomes would be measurable)",
);
}
if self.consolidation_min_support == 0 || self.consolidation_min_independent_sources == 0 {
return bad(
"consolidation_min_support / consolidation_min_independent_sources",
"must be at least 1",
);
}
for (name, a, b) in [
(
"recall.lexical_coverage + recall.lexical_jaccard",
r.lexical_coverage,
r.lexical_jaccard,
),
(
"recall.cue_lexical + recall.cue_tag",
r.cue_lexical,
r.cue_tag,
),
(
"recall.relevance_base + recall.metadata_share",
r.relevance_base,
r.metadata_share,
),
] {
if a + b > 1.0 + 1e-9 {
return bad(name, "must sum to at most 1");
}
}
if r.relevance_base <= 0.0 {
return bad(
"recall.relevance_base",
"must be above 0 (relevance must always count)",
);
}
for (name, value) in [
("recall.recency_scale_days", r.recency_scale_days),
("recall.prospective_scale_days", r.prospective_scale_days),
] {
if !value.is_finite() || value <= 0.0 {
return bad(name, "must be a positive number of days");
}
}
Ok(())
}
}