use serde::{Deserialize, Serialize};
pub const RETUNE_CORPUS_MILESTONE: u64 = 50;
pub const RETUNE_REGRET_RATE_THRESHOLD: f64 = 0.10;
pub const REINDEX_TOKENS_PER_1K_MEMORIES: u64 = 2_000;
pub const DEFAULT_COST_LAMBDA: f64 = 0.05;
pub const DEFAULT_COST_WEIGHT: f64 = DEFAULT_COST_LAMBDA / 6000.0;
pub const LEXICAL_FLOORS: &[f32] = &[0.3, 0.4, 0.5, 0.6];
pub const SEMANTIC_FLOORS: &[f32] = &[-1.0, 0.0, 0.25, 0.35, 0.45];
pub const RERANKER_IDS: &[&str] = &[
"off",
"ms-marco-tinybert-l-2-v2",
"jina-reranker-v1-tiny-en",
"ms-marco-minilm-l-4-v2",
];
pub const FUSION_MODES: &[&str] = &["linear", "rrf"];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TuneCombo {
pub min_lexical_coverage: f32,
pub min_semantic_score: f32,
pub reranker_id: String,
#[serde(default = "default_fusion_mode")]
pub fusion: String,
}
fn default_fusion_mode() -> String {
"linear".to_string()
}
impl TuneCombo {
pub fn all_combos() -> Vec<TuneCombo> {
let mut out = Vec::new();
for &lex in LEXICAL_FLOORS {
for &sem in SEMANTIC_FLOORS {
for &rr in RERANKER_IDS {
for &fusion in FUSION_MODES {
out.push(TuneCombo {
min_lexical_coverage: lex,
min_semantic_score: sem,
reranker_id: rr.to_string(),
fusion: fusion.to_string(),
});
}
}
}
}
out
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComboResult {
pub combo: TuneCombo,
pub mean_mrr: f64,
pub mean_tokens: f64,
pub objective: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TuneHistoryEntry {
pub timestamp: String,
pub before: TuneCombo,
pub after: TuneCombo,
pub train_objective: f64,
pub holdout_objective: f64,
pub holdout_mrr: f64,
pub baseline_holdout_objective: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_count_at_tune: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measurement: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetuneTriggerState {
pub current_memory_count: u64,
pub memory_count_at_last_tune: u64,
pub memories_added_since_tune: u64,
pub corpus_milestone_triggered: bool,
pub recent_regret_count: u64,
pub recent_served_count: u64,
pub regret_rate: f64,
pub drift_triggered: bool,
pub should_retune: bool,
pub last_tuned_at: Option<String>,
}
pub fn compute_retune_trigger(
conn: &rusqlite::Connection,
kimetsu_dir: &std::path::Path,
) -> kimetsu_core::KimetsuResult<RetuneTriggerState> {
let current_memory_count: u64 = conn.query_row(
"SELECT COUNT(*) FROM memories WHERE invalidated_at IS NULL",
[],
|r| r.get(0),
)?;
let last_entry = latest_tune_history(kimetsu_dir)?;
let memory_count_at_last_tune = last_entry
.as_ref()
.and_then(|e| e.memory_count_at_tune)
.unwrap_or(0);
let last_tuned_at = last_entry.as_ref().map(|e| e.timestamp.clone());
let memories_added_since_tune = current_memory_count.saturating_sub(memory_count_at_last_tune);
let corpus_milestone_triggered = memories_added_since_tune >= RETUNE_CORPUS_MILESTONE;
let cutoff_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
.saturating_sub(86_400);
let cutoff_iso = {
let dt = time::OffsetDateTime::from_unix_timestamp(cutoff_secs as i64)
.unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
dt.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default()
};
let recent_regret_count: u64 = conn.query_row(
"SELECT COUNT(*) FROM events WHERE kind = 'retrieval.regret' AND ts >= ?1",
rusqlite::params![cutoff_iso],
|r| r.get(0),
)?;
let recent_served_count: u64 = conn.query_row(
"SELECT COUNT(*) FROM events WHERE ts>=?1 AND kind=CASE WHEN EXISTS(SELECT 1 FROM events WHERE kind='context.injected' AND ts>=?1) THEN 'context.injected' ELSE 'context.served' END",
rusqlite::params![cutoff_iso],
|r| r.get(0),
)?;
let regret_rate = if recent_served_count > 0 {
recent_regret_count as f64 / recent_served_count as f64
} else {
0.0
};
let drift_triggered = regret_rate >= RETUNE_REGRET_RATE_THRESHOLD;
let should_retune = corpus_milestone_triggered || drift_triggered;
Ok(RetuneTriggerState {
current_memory_count,
memory_count_at_last_tune,
memories_added_since_tune,
corpus_milestone_triggered,
recent_regret_count,
recent_served_count,
regret_rate,
drift_triggered,
should_retune,
last_tuned_at,
})
}
pub const KNOWN_EMBEDDER_MODELS: &[(&str, &str, u32)] = &[
(
"jina-embeddings-v2-base-code",
"Jina v2 Code (768d, default)",
280,
),
("bge-small-en-v1.5", "BGE-small (384d, lightweight)", 130),
("nomic-embed-text-v1.5", "Nomic Embed v1.5 (768d)", 270),
("all-minilm-l6-v2", "MiniLM L6 (384d, fast)", 90),
];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelAdvisorReport {
pub recommend_grid_run: bool,
pub reason: String,
pub current_embedder: String,
pub memories_to_reindex: u64,
pub estimated_reindex_tokens: u64,
pub candidate_models: Vec<ModelCandidate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCandidate {
pub model_id: String,
pub description: String,
pub approx_download_mib: u32,
}
pub fn compute_model_advisor(
current_embedder: &str,
trigger: &RetuneTriggerState,
) -> ModelAdvisorReport {
let recommend_grid_run = trigger.corpus_milestone_triggered;
let reason = if trigger.corpus_milestone_triggered {
format!(
"Corpus grew by {} memories since last tune (≥{} threshold). \
Re-running the embedder×reranker grid is recommended to verify \
the current model remains optimal.",
trigger.memories_added_since_tune, RETUNE_CORPUS_MILESTONE,
)
} else {
format!(
"No corpus milestone triggered ({} memories added, threshold {}). \
Grid re-run is optional.",
trigger.memories_added_since_tune, RETUNE_CORPUS_MILESTONE,
)
};
let memories_to_reindex = trigger.current_memory_count;
let estimated_reindex_tokens =
(memories_to_reindex.max(1) / 1_000 + 1).saturating_mul(REINDEX_TOKENS_PER_1K_MEMORIES);
let candidate_models = KNOWN_EMBEDDER_MODELS
.iter()
.map(|(id, desc, mib)| ModelCandidate {
model_id: id.to_string(),
description: desc.to_string(),
approx_download_mib: *mib,
})
.collect();
ModelAdvisorReport {
recommend_grid_run,
reason,
current_embedder: current_embedder.to_string(),
memories_to_reindex,
estimated_reindex_tokens,
candidate_models,
}
}
pub fn compute_objective(mean_mrr: f64, mean_tokens: f64, cost_weight: f64) -> f64 {
mean_mrr - cost_weight * mean_tokens
}
pub fn compute_objective_with_regret(
quality: f64,
mean_bound: f64,
cost_weight: f64,
_regret_rate: f64,
) -> f64 {
compute_objective(quality, mean_bound, cost_weight)
}
pub fn count_regret_events(
conn: &rusqlite::Connection,
since: Option<&str>,
until: Option<&str>,
) -> kimetsu_core::KimetsuResult<u64> {
let count: u64 = match (since, until) {
(Some(lo), Some(hi)) => conn.query_row(
"SELECT COUNT(*) FROM events \
WHERE kind = 'retrieval.regret' AND ts >= ?1 AND ts <= ?2",
rusqlite::params![lo, hi],
|r| r.get(0),
)?,
(Some(lo), None) => conn.query_row(
"SELECT COUNT(*) FROM events \
WHERE kind = 'retrieval.regret' AND ts >= ?1",
rusqlite::params![lo],
|r| r.get(0),
)?,
(None, Some(hi)) => conn.query_row(
"SELECT COUNT(*) FROM events \
WHERE kind = 'retrieval.regret' AND ts <= ?1",
rusqlite::params![hi],
|r| r.get(0),
)?,
(None, None) => conn.query_row(
"SELECT COUNT(*) FROM events WHERE kind = 'retrieval.regret'",
[],
|r| r.get(0),
)?,
};
Ok(count)
}
pub fn train_holdout_split(case_count: usize) -> (Vec<usize>, Vec<usize>) {
if case_count < 2 {
return ((0..case_count).collect(), Vec::new());
}
let holdout_size = (case_count / 5).max(1); let holdout: Vec<usize> = (0..case_count).filter(|i| i % 5 == 0).collect();
let train: Vec<usize> = (0..case_count).filter(|i| i % 5 != 0).collect();
let _ = holdout_size; (train, holdout)
}
#[derive(Debug)]
pub struct FamilySplit {
pub train: Vec<usize>,
pub holdout: Vec<usize>,
pub family_count: usize,
}
pub fn grouped_train_holdout_split(cases: &[crate::eval::EvalCase]) -> FamilySplit {
use std::collections::{BTreeMap, BTreeSet};
let mut parents: Vec<usize> = (0..cases.len()).collect();
fn root(parents: &[usize], mut i: usize) -> usize {
while parents[i] != i {
i = parents[i]
}
i
}
let mut owners = BTreeMap::<String, usize>::new();
let keys: Vec<BTreeSet<String>> = cases
.iter()
.map(|c| {
let mut keys = BTreeSet::new();
keys.insert(format!(
"q:{}",
c.query
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase()
));
if !c.family.trim().is_empty() {
keys.insert(format!("f:{}", c.family.trim()));
}
for id in c.relevant.iter().chain(&c.stale) {
keys.insert(format!("m:{id}"));
}
keys
})
.collect();
for (i, case_keys) in keys.iter().enumerate() {
for key in case_keys {
if let Some(&other) = owners.get(key) {
let a = root(&parents, i);
let b = root(&parents, other);
parents[a] = b;
} else {
owners.insert(key.clone(), i);
}
}
}
let mut components = BTreeMap::<usize, (BTreeSet<String>, Vec<usize>)>::new();
for (i, case_keys) in keys.into_iter().enumerate() {
let entry = components.entry(root(&parents, i)).or_default();
entry
.0
.extend(case_keys.into_iter().filter(|k| !k.starts_with("m:")));
entry.1.push(i);
}
let mut groups: Vec<_> = components.into_values().collect();
groups.sort_by(|a, b| a.0.cmp(&b.0));
let count = groups.len();
let mut split = FamilySplit {
train: Vec::new(),
holdout: Vec::new(),
family_count: count,
};
for (i, (_, ids)) in groups.into_iter().enumerate() {
if count > 1 && i % 5 == 0 {
split.holdout.extend(ids)
} else {
split.train.extend(ids)
}
}
split.train.sort_unstable();
split.holdout.sort_unstable();
split
}
pub fn select_winner(results: &[ComboResult]) -> Option<&ComboResult> {
results.iter().max_by(|a, b| {
a.objective
.partial_cmp(&b.objective)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn append_tune_history(
kimetsu_dir: &std::path::Path,
entry: TuneHistoryEntry,
) -> kimetsu_core::KimetsuResult<()> {
let path = kimetsu_dir.join("tune-history.json");
let mut entries: Vec<TuneHistoryEntry> = if path.exists() {
let text = std::fs::read_to_string(&path)?;
serde_json::from_str(&text).unwrap_or_default()
} else {
Vec::new()
};
entries.push(entry);
let json = serde_json::to_string_pretty(&entries)?;
std::fs::write(&path, json)?;
Ok(())
}
pub fn latest_tune_history(
kimetsu_dir: &std::path::Path,
) -> kimetsu_core::KimetsuResult<Option<TuneHistoryEntry>> {
let path = kimetsu_dir.join("tune-history.json");
if !path.exists() {
return Ok(None);
}
let text = std::fs::read_to_string(&path)?;
let entries: Vec<TuneHistoryEntry> = serde_json::from_str(&text).unwrap_or_default();
Ok(entries.into_iter().last())
}
#[cfg(test)]
mod tests {
use super::*;
use ulid::Ulid;
#[test]
fn all_combos_covers_the_full_grid() {
let combos = TuneCombo::all_combos();
let expected =
LEXICAL_FLOORS.len() * SEMANTIC_FLOORS.len() * RERANKER_IDS.len() * FUSION_MODES.len();
assert_eq!(
combos.len(),
expected,
"expected {}×{}×{}×{}={expected} combos, got {}",
LEXICAL_FLOORS.len(),
SEMANTIC_FLOORS.len(),
RERANKER_IDS.len(),
FUSION_MODES.len(),
combos.len()
);
let mut keys: Vec<String> = combos
.iter()
.map(|c| {
format!(
"{}|{}|{}|{}",
c.min_lexical_coverage, c.min_semantic_score, c.reranker_id, c.fusion
)
})
.collect();
keys.sort();
let before = keys.len();
keys.dedup();
assert_eq!(before, keys.len(), "sweep grid contains duplicate combos");
}
#[test]
fn compute_objective_formula() {
let obj = compute_objective(0.75, 1000.0, 0.005);
assert!((obj - (-4.25)).abs() < 1e-9, "objective: {obj}");
}
#[test]
fn compute_objective_zero_cost_weight_is_just_mrr() {
let obj = compute_objective(0.85, 500.0, 0.0);
assert!((obj - 0.85).abs() < 1e-9, "objective with 0 cost: {obj}");
}
#[test]
fn train_holdout_split_80_20() {
let (train, holdout) = train_holdout_split(10);
assert_eq!(holdout, vec![0, 5]);
assert_eq!(train, vec![1, 2, 3, 4, 6, 7, 8, 9]);
assert_eq!(train.len() + holdout.len(), 10);
}
#[test]
fn train_holdout_split_empty() {
let (train, holdout) = train_holdout_split(0);
assert!(train.is_empty());
assert!(holdout.is_empty());
}
#[test]
fn single_family_cannot_supply_an_independent_holdout() {
let (train, holdout) = train_holdout_split(1);
assert_eq!(train, vec![0]);
assert!(holdout.is_empty());
}
#[test]
fn split_membership_is_independent_of_input_order() {
let queries = ["alpha", "bravo", "charlie", "delta", "echo"];
let mut reversed = queries;
reversed.reverse();
let held = |q: &[&str]| {
let cases: Vec<_> = q
.iter()
.map(|query| {
serde_json::from_value(serde_json::json!({"query":query,"relevant":[]}))
.unwrap()
})
.collect();
let indexes = grouped_train_holdout_split(&cases).holdout;
indexes
.into_iter()
.map(|i| q[i].to_string())
.collect::<std::collections::BTreeSet<_>>()
};
assert_eq!(held(&queries), held(&reversed));
}
#[test]
fn overlapping_aliases_and_task_families_never_leak_into_holdout() {
let cases: Vec<crate::eval::EvalCase> = serde_json::from_value(serde_json::json!([
{"query":"a","relevant":["one"]},
{"query":"b","relevant":["two"],"stale":["one"]},
{"query":"c","relevant":["two"],"family":"task"},
{"query":"d","relevant":[],"family":"task"},
{"query":"e","relevant":[]}
]))
.unwrap();
let split = grouped_train_holdout_split(&cases);
assert_eq!(split.family_count, 2);
let in_holdout = split.holdout.contains(&0);
for i in 1..4 {
assert_eq!(split.holdout.contains(&i), in_holdout);
}
let one = grouped_train_holdout_split(&cases[..4]);
assert!(one.holdout.is_empty());
assert_eq!(one.train.len(), 4);
}
#[test]
fn explicit_default_cost_policy_uses_budget_fraction_units() {
let score = compute_objective(0.75, 512.0, DEFAULT_COST_WEIGHT);
assert!((score - 0.7457333333333333).abs() < 1e-12);
assert_eq!(compute_objective(1.0, 6000.0, DEFAULT_COST_WEIGHT), 0.95);
}
#[test]
fn select_winner_picks_highest_objective() {
let combos = vec![
ComboResult {
combo: TuneCombo {
min_lexical_coverage: 0.3,
min_semantic_score: 0.0,
reranker_id: "off".to_string(),
fusion: "linear".to_string(),
},
mean_mrr: 0.7,
mean_tokens: 100.0,
objective: 0.2,
},
ComboResult {
combo: TuneCombo {
min_lexical_coverage: 0.4,
min_semantic_score: 0.25,
reranker_id: "off".to_string(),
fusion: "linear".to_string(),
},
mean_mrr: 0.9,
mean_tokens: 80.0,
objective: 0.5,
},
];
let winner = select_winner(&combos).expect("winner");
assert!((winner.objective - 0.5).abs() < 1e-9);
}
#[test]
fn tune_history_roundtrip() {
let tmp = std::env::temp_dir().join(format!("kimetsu-tune-hist-{}", Ulid::new()));
std::fs::create_dir_all(&tmp).unwrap();
let entry = TuneHistoryEntry {
timestamp: "2026-06-11T00:00:00Z".to_string(),
before: TuneCombo {
min_lexical_coverage: 0.5,
min_semantic_score: -1.0,
reranker_id: "off".to_string(),
fusion: "linear".to_string(),
},
after: TuneCombo {
min_lexical_coverage: 0.4,
min_semantic_score: 0.25,
reranker_id: "ms-marco-tinybert-l-2-v2".to_string(),
fusion: "linear".to_string(),
},
train_objective: 0.55,
holdout_objective: 0.50,
holdout_mrr: 0.70,
baseline_holdout_objective: 0.45,
memory_count_at_tune: None,
measurement: None,
};
append_tune_history(&tmp, entry.clone()).unwrap();
let latest = latest_tune_history(&tmp).unwrap().unwrap();
assert!((latest.holdout_objective - 0.50).abs() < 1e-9);
assert_eq!(latest.after.reranker_id, "ms-marco-tinybert-l-2-v2");
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn tune_history_empty_when_no_file() {
let tmp = std::env::temp_dir().join(format!("kimetsu-tune-empty-{}", Ulid::new()));
std::fs::create_dir_all(&tmp).unwrap();
let latest = latest_tune_history(&tmp).unwrap();
assert!(latest.is_none(), "no history file → None");
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn compute_objective_with_regret_zero_rate_matches_base() {
let base = compute_objective(0.75, 500.0, 0.005);
let with_regret = compute_objective_with_regret(0.75, 500.0, 0.005, 0.0);
assert!(
(base - with_regret).abs() < 1e-9,
"zero regret_rate must give same result as base objective"
);
}
#[test]
fn historical_regret_cannot_change_candidate_objective() {
let base = compute_objective(0.75, 500.0, 0.005);
let with_regret = compute_objective_with_regret(0.75, 500.0, 0.005, 0.10);
assert_eq!(with_regret, base);
}
#[test]
fn even_full_historical_regret_is_diagnostic_only() {
let base = compute_objective(0.8, 0.0, 0.0);
let with_full = compute_objective_with_regret(0.8, 0.0, 0.0, 1.0);
assert_eq!(with_full, base);
}
use crate::{
project::{init_project, load_project},
projector,
user_brain::with_user_brain_disabled,
};
use kimetsu_core::{event::Event, ids::RunId};
fn trigger_test_root(label: &str) -> std::path::PathBuf {
let root =
std::env::temp_dir().join(format!("kimetsu-tune-trigger-{label}-{}", Ulid::new()));
kimetsu_core::paths::git_init_boundary(&root);
root
}
#[test]
fn retune_trigger_no_history_no_events() {
with_user_brain_disabled(|| {
let root = trigger_test_root("empty");
std::fs::create_dir_all(&root).expect("mkdir");
init_project(&root, false).expect("init");
let paths = kimetsu_core::paths::ProjectPaths::discover(&root).expect("paths");
let (_, _, conn) = load_project(&root).expect("load");
let state = compute_retune_trigger(&conn, &paths.kimetsu_dir).expect("trigger");
assert_eq!(state.current_memory_count, 0);
assert_eq!(state.memories_added_since_tune, 0);
assert!(!state.corpus_milestone_triggered);
assert!(!state.drift_triggered);
assert!(!state.should_retune);
assert!(state.last_tuned_at.is_none());
std::fs::remove_dir_all(&root).ok();
});
}
#[test]
fn retune_trigger_corpus_milestone_when_enough_memories() {
with_user_brain_disabled(|| {
let root = trigger_test_root("milestone");
std::fs::create_dir_all(&root).expect("mkdir");
init_project(&root, false).expect("init");
let paths = kimetsu_core::paths::ProjectPaths::discover(&root).expect("paths");
let entry = TuneHistoryEntry {
timestamp: "2026-01-01T00:00:00Z".to_string(),
before: TuneCombo {
min_lexical_coverage: 0.4,
min_semantic_score: 0.0,
reranker_id: "off".to_string(),
fusion: "linear".to_string(),
},
after: TuneCombo {
min_lexical_coverage: 0.4,
min_semantic_score: 0.0,
reranker_id: "off".to_string(),
fusion: "linear".to_string(),
},
train_objective: 0.5,
holdout_objective: 0.5,
holdout_mrr: 0.7,
baseline_holdout_objective: 0.45,
memory_count_at_tune: Some(0),
measurement: None,
};
append_tune_history(&paths.kimetsu_dir, entry).expect("append");
for i in 0..RETUNE_CORPUS_MILESTONE {
crate::project::add_memory(
&root,
kimetsu_core::memory::MemoryScope::Project,
kimetsu_core::memory::MemoryKind::Fact,
&format!("milestone memory {i}"),
)
.expect("add memory");
}
let (_, _, conn) = load_project(&root).expect("load");
let state = compute_retune_trigger(&conn, &paths.kimetsu_dir).expect("trigger");
assert!(
state.corpus_milestone_triggered,
"milestone must trigger at ≥{RETUNE_CORPUS_MILESTONE} memories added"
);
assert!(state.should_retune);
std::fs::remove_dir_all(&root).ok();
});
}
#[test]
fn retune_trigger_drift_when_regret_rate_high() {
with_user_brain_disabled(|| {
let root = trigger_test_root("drift");
std::fs::create_dir_all(&root).expect("mkdir");
init_project(&root, false).expect("init");
let paths = kimetsu_core::paths::ProjectPaths::discover(&root).expect("paths");
let (_, _, conn) = load_project(&root).expect("load");
let run_id = RunId::new();
let served_ev = Event::new(
run_id,
"context.injected",
serde_json::json!({"query_hash":"abc","capsule_count":1,"skipped":false}),
);
projector::apply_events(&conn, &[served_ev]).expect("seed served");
let regret_ev = Event::new(
run_id,
"retrieval.regret",
serde_json::json!({"memory_id":"m1","dropped_at":0,"cited_at":1}),
);
projector::apply_events(&conn, &[regret_ev]).expect("seed regret");
let state = compute_retune_trigger(&conn, &paths.kimetsu_dir).expect("trigger");
assert!(
state.drift_triggered,
"regret_rate ({:.2}) must exceed threshold ({RETUNE_REGRET_RATE_THRESHOLD})",
state.regret_rate
);
assert!(state.should_retune);
std::fs::remove_dir_all(&root).ok();
});
}
#[test]
fn model_advisor_recommends_at_milestone() {
let trigger = RetuneTriggerState {
current_memory_count: 100,
memory_count_at_last_tune: 10,
memories_added_since_tune: 90,
corpus_milestone_triggered: true,
recent_regret_count: 0,
recent_served_count: 20,
regret_rate: 0.0,
drift_triggered: false,
should_retune: true,
last_tuned_at: Some("2026-01-01T00:00:00Z".to_string()),
};
let report = compute_model_advisor("jina-embeddings-v2-base-code", &trigger);
assert!(report.recommend_grid_run, "must recommend at milestone");
assert!(report.estimated_reindex_tokens > 0, "cost must be stated");
assert!(!report.candidate_models.is_empty());
}
#[test]
fn model_advisor_no_recommendation_below_milestone() {
let trigger = RetuneTriggerState {
current_memory_count: 30,
memory_count_at_last_tune: 25,
memories_added_since_tune: 5,
corpus_milestone_triggered: false,
recent_regret_count: 0,
recent_served_count: 10,
regret_rate: 0.0,
drift_triggered: false,
should_retune: false,
last_tuned_at: None,
};
let report = compute_model_advisor("jina-embeddings-v2-base-code", &trigger);
assert!(
!report.recommend_grid_run,
"must NOT recommend below milestone"
);
}
#[test]
fn count_regret_events_zero_in_empty_db() {
with_user_brain_disabled(|| {
let root = trigger_test_root("regret-count");
std::fs::create_dir_all(&root).expect("mkdir");
init_project(&root, false).expect("init");
let (_, _, conn) = load_project(&root).expect("load");
let count = count_regret_events(&conn, None, None).expect("count");
assert_eq!(count, 0);
std::fs::remove_dir_all(&root).ok();
});
}
#[test]
fn tune_history_entry_memory_count_roundtrip() {
let tmp = std::env::temp_dir().join(format!("kimetsu-tune-memcount-{}", Ulid::new()));
std::fs::create_dir_all(&tmp).unwrap();
let entry = TuneHistoryEntry {
timestamp: "2026-06-11T00:00:00Z".to_string(),
before: TuneCombo {
min_lexical_coverage: 0.5,
min_semantic_score: -1.0,
reranker_id: "off".to_string(),
fusion: "linear".to_string(),
},
after: TuneCombo {
min_lexical_coverage: 0.4,
min_semantic_score: 0.25,
reranker_id: "off".to_string(),
fusion: "linear".to_string(),
},
train_objective: 0.55,
holdout_objective: 0.50,
holdout_mrr: 0.70,
baseline_holdout_objective: 0.45,
memory_count_at_tune: Some(123),
measurement: None,
};
append_tune_history(&tmp, entry).unwrap();
let latest = latest_tune_history(&tmp).unwrap().unwrap();
assert_eq!(
latest.memory_count_at_tune,
Some(123),
"memory_count_at_tune must round-trip"
);
std::fs::remove_dir_all(&tmp).ok();
}
}