use rusqlite::{params, OptionalExtension};
use crate::error::Result;
use super::{now, YantrikDB};
const EWMA_ALPHA: f64 = 0.15;
const MIN_COUNT: u64 = 8;
const SATURATION_THRESHOLD: f64 = 0.80;
const HIGH_FLOOR: f64 = 0.70;
const MIN_CEILING: f64 = 0.75;
pub(crate) fn calibrate_importance_value(raw: f64, ewma: f64, count: u64) -> f64 {
if count < MIN_COUNT || ewma <= SATURATION_THRESHOLD || raw <= HIGH_FLOOR {
return raw;
}
let sat = ((ewma - SATURATION_THRESHOLD) / (1.0 - SATURATION_THRESHOLD)).clamp(0.0, 1.0);
let ceiling = 1.0 - sat * (1.0 - MIN_CEILING);
let frac = (raw - HIGH_FLOOR) / (1.0 - HIGH_FLOOR);
(HIGH_FLOOR + frac * (ceiling - HIGH_FLOOR)).clamp(0.0, 1.0)
}
pub(crate) fn calibrated_importance_on(
conn: &rusqlite::Connection,
namespace: &str,
raw: f64,
) -> Result<f64> {
let namespace = super::record::normalize_namespace(namespace);
let existing: Option<(f64, i64)> = conn
.query_row(
"SELECT ewma, count FROM namespace_importance_stats WHERE namespace = ?1",
params![namespace],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.optional()?;
let (ewma, count) = existing.unwrap_or((raw, 0));
let calibrated = calibrate_importance_value(raw, ewma, count as u64);
if (calibrated - raw).abs() > f64::EPSILON {
tracing::debug!(
target: "yantrikdb::audit::importance",
namespace,
raw,
calibrated,
ewma,
count,
"deflated saturated importance",
);
}
Ok(calibrated)
}
impl YantrikDB {
pub(crate) fn calibrated_importance(&self, namespace: &str, raw: f64) -> Result<f64> {
calibrated_importance_on(&self.conn(), namespace, raw)
}
pub(crate) fn advance_importance_stats_in_tx(
&self,
conn: &rusqlite::Connection,
namespace: &str,
raw: f64,
) -> Result<()> {
let namespace = super::record::normalize_namespace(namespace);
conn.execute(
"INSERT INTO namespace_importance_stats (namespace, ewma, count, updated_at) \
VALUES (?1, ?2, 1, ?3) \
ON CONFLICT(namespace) DO UPDATE SET \
ewma = CASE WHEN namespace_importance_stats.count = 0 THEN ?2 \
ELSE (1.0 - ?4) * namespace_importance_stats.ewma + ?4 * ?2 END, \
count = namespace_importance_stats.count + 1, \
updated_at = ?3",
params![namespace, raw, now(), EWMA_ALPHA],
)?;
Ok(())
}
}
const REVERSION_BASELINE: f64 = 0.5;
const REVERSION_TENURE_SECS: f64 = 90.0 * 86_400.0;
const MAX_REVERSION: f64 = 0.6;
const SAMPLE_CAP: usize = 50;
pub(crate) fn usage_corrected_importance(
prior: f64,
access_count: i64,
secs_since_access: f64,
) -> f64 {
if prior <= REVERSION_BASELINE {
return prior;
}
let staleness = (secs_since_access / REVERSION_TENURE_SECS).clamp(0.0, 1.0);
let resistance = 1.0 + (access_count.max(0) as f64).ln_1p();
let reversion = (MAX_REVERSION * staleness / resistance).clamp(0.0, MAX_REVERSION);
let target = REVERSION_BASELINE + (1.0 - REVERSION_BASELINE) * (1.0 - reversion);
prior.min(target)
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct ImportanceRecalibrationReport {
pub dry_run: bool,
pub scanned: usize,
pub adjusted: usize,
pub total_drift: f64,
pub sample_rids: Vec<String>,
}
impl YantrikDB {
pub fn recalibrate_unused_importance(
&self,
dry_run: bool,
) -> Result<ImportanceRecalibrationReport> {
let mut report = ImportanceRecalibrationReport {
dry_run,
..Default::default()
};
let now_ts = now();
let rows: Vec<(String, f64, i64, f64)> = {
let conn = self.conn();
let mut stmt = conn.prepare(
"SELECT rid, importance, access_count, last_access FROM memories \
WHERE consolidation_status = 'active' AND importance > ?1",
)?;
let rows = stmt
.query_map(params![REVERSION_BASELINE], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, f64>(1)?,
r.get::<_, i64>(2)?,
r.get::<_, f64>(3)?,
))
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
rows
};
report.scanned = rows.len();
let mut pending: Vec<(String, f64)> = Vec::new();
for (rid, importance, access_count, last_access) in rows {
let secs = (now_ts - last_access).max(0.0);
let corrected = usage_corrected_importance(importance, access_count, secs);
if importance - corrected > 1e-6 {
report.adjusted += 1;
report.total_drift += importance - corrected;
if report.sample_rids.len() < SAMPLE_CAP {
report.sample_rids.push(rid.clone());
}
pending.push((rid, corrected));
}
}
if dry_run || pending.is_empty() {
return Ok(report);
}
{
let conn = self.conn();
conn.execute_batch("SAVEPOINT importance_recal")?;
let apply: Result<()> = (|| {
for (rid, corrected) in &pending {
conn.execute(
"UPDATE memories SET importance = ?1 WHERE rid = ?2",
params![corrected, rid],
)?;
}
Ok(())
})();
match apply {
Ok(()) => conn.execute_batch("RELEASE importance_recal")?,
Err(e) => {
let _ = conn
.execute_batch("ROLLBACK TO importance_recal; RELEASE importance_recal");
return Err(e);
}
}
}
{
let mut cache = self.scoring_cache.write();
for (rid, corrected) in &pending {
if let Some(row) = cache.get_mut(rid) {
row.importance = *corrected;
}
}
}
tracing::info!(
target: "yantrikdb::audit::importance",
scanned = report.scanned,
adjusted = report.adjusted,
total_drift = report.total_drift,
"unused-importance recalibration complete",
);
Ok(report)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_below_min_count() {
assert_eq!(calibrate_importance_value(1.0, 1.0, MIN_COUNT - 1), 1.0);
}
#[test]
fn identity_when_not_saturated() {
assert_eq!(calibrate_importance_value(1.0, 0.5, 100), 1.0);
assert_eq!(
calibrate_importance_value(1.0, SATURATION_THRESHOLD, 100),
1.0
);
}
#[test]
fn identity_for_low_band_values() {
assert_eq!(calibrate_importance_value(HIGH_FLOOR, 1.0, 100), HIGH_FLOOR);
assert_eq!(calibrate_importance_value(0.5, 1.0, 100), 0.5);
}
#[test]
fn deflates_high_values_when_saturated() {
let c = calibrate_importance_value(1.0, 1.0, 100);
assert!(
c < 1.0,
"max importance deflates under full saturation: {c}"
);
assert!(c >= MIN_CEILING, "but never below the floor ceiling: {c}");
}
#[test]
fn deflation_is_monotonic() {
let lo = calibrate_importance_value(0.75, 0.95, 100);
let mid = calibrate_importance_value(0.90, 0.95, 100);
let hi = calibrate_importance_value(1.00, 0.95, 100);
assert!(lo < mid, "lo {lo} < mid {mid}");
assert!(mid < hi, "mid {mid} < hi {hi}");
assert!(hi < 1.0, "even the top is deflated: {hi}");
}
#[test]
fn deeper_saturation_deflates_harder() {
let mild = calibrate_importance_value(1.0, 0.85, 100);
let severe = calibrate_importance_value(1.0, 1.0, 100);
assert!(severe < mild, "severe {severe} < mild {mild}");
}
#[test]
fn result_stays_in_unit_interval() {
for &ewma in &[0.0, 0.5, 0.81, 0.9, 1.0] {
for &raw in &[0.0, 0.5, 0.7, 0.85, 1.0] {
let c = calibrate_importance_value(raw, ewma, 100);
assert!((0.0..=1.0).contains(&c), "raw={raw} ewma={ewma} -> {c}");
}
}
}
#[test]
fn usage_identity_when_fresh() {
assert_eq!(usage_corrected_importance(1.0, 0, 0.0), 1.0);
assert_eq!(usage_corrected_importance(0.9, 5, 0.0), 0.9);
}
#[test]
fn usage_never_inflates_low_marks() {
let ancient = REVERSION_TENURE_SECS * 4.0;
assert_eq!(usage_corrected_importance(0.5, 0, ancient), 0.5);
assert_eq!(usage_corrected_importance(0.3, 0, ancient), 0.3);
}
#[test]
fn usage_reverts_unused_high_importance() {
let c = usage_corrected_importance(1.0, 0, REVERSION_TENURE_SECS);
assert!(c < 1.0, "an unused high mark deflates: {c}");
assert!(c >= REVERSION_BASELINE, "but never below baseline: {c}");
}
#[test]
fn usage_access_resists_reversion() {
let unused = usage_corrected_importance(1.0, 0, REVERSION_TENURE_SECS);
let used = usage_corrected_importance(1.0, 50, REVERSION_TENURE_SECS);
assert!(
used > unused,
"frequent access slows reversion: {used} > {unused}"
);
}
#[test]
fn usage_monotonic_in_staleness() {
let prior = 1.0;
let young = usage_corrected_importance(prior, 0, REVERSION_TENURE_SECS * 0.25);
let old = usage_corrected_importance(prior, 0, REVERSION_TENURE_SECS * 0.75);
assert!(
old < young,
"more staleness ⇒ more reversion: {old} < {young}"
);
assert!(young <= prior && old >= REVERSION_BASELINE);
}
}