use std::cmp::Ordering;
use std::collections::HashMap;
use kimetsu_core::config::{BrokerWeights, StageWeights};
use kimetsu_core::memory::MemoryScope;
use kimetsu_core::{KimetsuResult, ids::new_id};
use rusqlite::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TaskKind {
#[default]
Feature,
Debug,
Refactor,
Docs,
Investigation,
}
pub fn classify_task(task: &str) -> TaskKind {
let lower = task.to_ascii_lowercase();
const DEBUG_KW: &[&str] = &[
"fix",
"bug",
"error",
"fail",
"crash",
"panic",
"regression",
"broken",
"debug",
"stack trace",
"exception",
];
if DEBUG_KW.iter().any(|kw| lower.contains(kw)) {
return TaskKind::Debug;
}
const INVESTIGATE_KW: &[&str] = &[
"investigate",
"analyze",
"understand",
" why ",
"explore",
"find out",
"root cause",
"audit",
"trace",
];
if INVESTIGATE_KW.iter().any(|kw| lower.contains(kw)) {
return TaskKind::Investigation;
}
const REFACTOR_KW: &[&str] = &[
"refactor",
"rename",
"cleanup",
"clean up",
"restructure",
"simplify",
"extract",
"deduplicate",
"reorganize",
];
if REFACTOR_KW.iter().any(|kw| lower.contains(kw)) {
return TaskKind::Refactor;
}
const DOCS_KW: &[&str] = &[
"document",
"readme",
"changelog",
"comment",
"docstring",
"docs",
"tutorial",
"guide",
];
if DOCS_KW.iter().any(|kw| lower.contains(kw)) {
return TaskKind::Docs;
}
TaskKind::Feature
}
fn weights_for_task_kind(base: StageWeights, kind: TaskKind) -> StageWeights {
match kind {
TaskKind::Feature => base,
TaskKind::Debug => renorm(StageWeights {
freshness: base.freshness * 1.6,
..base
}),
TaskKind::Refactor => renorm(StageWeights {
scope: base.scope * 1.6,
..base
}),
TaskKind::Investigation => renorm(StageWeights {
relevance: base.relevance * 1.4,
..base
}),
TaskKind::Docs => renorm(StageWeights {
confidence: base.confidence * 1.15,
..base
}),
}
}
fn renorm(w: StageWeights) -> StageWeights {
let sum = w.relevance + w.confidence + w.freshness + w.scope;
if sum <= f32::EPSILON {
return w;
}
StageWeights {
relevance: w.relevance / sum,
confidence: w.confidence / sum,
freshness: w.freshness / sum,
scope: w.scope / sum,
}
}
fn task_kind_prefer_roles(kind: TaskKind) -> &'static [&'static str] {
match kind {
TaskKind::Feature => &[],
TaskKind::Debug => &["failure_pattern"],
TaskKind::Refactor => &["convention"],
TaskKind::Investigation => &["fact", "preference"],
TaskKind::Docs => &["convention"],
}
}
use time::OffsetDateTime;
use crate::embeddings::{
self, DEFAULT_HYBRID_ALPHA, Embedder, cosine_similarity, decode_embedding,
};
#[derive(Debug, Clone)]
struct QueryEmbedding {
vector: Vec<f32>,
model_id: String,
}
impl QueryEmbedding {
fn from_embedder(embedder: &dyn Embedder, query: &str) -> Option<Self> {
if embedder.is_noop() {
return None;
}
match embedder.embed(query) {
Ok(v) if v.len() == embedder.dim() => Some(Self {
vector: v,
model_id: embedder.model_id().to_string(),
}),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextCapsule {
pub id: String,
pub kind: String,
pub summary: String,
pub token_estimate: u32,
pub expansion_handle: String,
pub provenance: Vec<ProvenanceRef>,
pub confidence: f32,
pub freshness: f32,
pub relevance: f32,
pub scope_weight: f32,
pub score: f32,
}
impl ContextCapsule {
pub fn wire_minimal(summary: String, kind: String, score: f32) -> Self {
Self {
id: String::new(),
kind,
summary,
token_estimate: 0,
expansion_handle: String::new(),
provenance: Vec::new(),
confidence: 0.0,
freshness: 0.0,
relevance: 0.0,
scope_weight: 0.0,
score,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvenanceRef {
pub source: String,
pub id: String,
pub excerpt: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ContextRequest {
pub stage: String,
pub query: String,
pub budget_tokens: u32,
pub tags: Vec<String>,
pub min_score: f32,
pub max_capsules: usize,
pub prefer_roles: Vec<String>,
pub kinds: Vec<String>,
pub min_semantic_score: f32,
pub min_lexical_coverage: f32,
pub task_kind: TaskKind,
}
#[derive(Debug, Clone)]
pub struct ContextBundle {
pub stage: String,
pub budget_tokens: u32,
pub used_tokens: u32,
pub capsules: Vec<ContextCapsule>,
pub excluded: Vec<ContextCapsule>,
pub skipped: bool,
pub top_score: f32,
}
#[derive(Debug, Clone)]
struct Candidate {
capsule: ContextCapsule,
raw_relevance: f32,
embedding: Option<Vec<f32>>,
cosine: Option<f32>,
}
pub fn retrieve_context(
conn: &Connection,
repo_root: &str,
weights: &BrokerWeights,
request: ContextRequest,
) -> KimetsuResult<ContextBundle> {
retrieve_context_multi(conn, repo_root, weights, request, &[])
}
pub fn retrieve_context_multi(
conn: &Connection,
repo_root: &str,
weights: &BrokerWeights,
request: ContextRequest,
extra_memory_conns: &[&Connection],
) -> KimetsuResult<ContextBundle> {
let embedder = embeddings::open_default_embedder();
retrieve_context_with_embedder(
conn,
repo_root,
weights,
request,
extra_memory_conns,
embedder,
)
}
pub fn retrieve_context_with_embedder(
conn: &Connection,
repo_root: &str,
weights: &BrokerWeights,
request: ContextRequest,
extra_memory_conns: &[&Connection],
embedder: &dyn Embedder,
) -> KimetsuResult<ContextBundle> {
let query_embedding = QueryEmbedding::from_embedder(embedder, &request.query);
let half_life_days = weights.decay_half_life_days;
let mut candidates = Vec::new();
candidates.extend(memory_candidates(
conn,
&request.query,
query_embedding.as_ref(),
half_life_days,
)?);
for extra in extra_memory_conns {
candidates.extend(memory_candidates(
extra,
&request.query,
query_embedding.as_ref(),
half_life_days,
)?);
}
candidates.extend(repo_file_candidates(conn, repo_root, &request.query, 30)?);
candidates.extend(manifest_candidates(conn, repo_root, &request.query)?);
if !request.kinds.is_empty() {
candidates.retain(|c| {
request
.kinds
.iter()
.any(|k| capsule_matches_kind(&c.capsule, k))
});
}
if request.min_lexical_coverage > 0.0 {
let content = content_tokens(&request.query);
if !content.is_empty() {
let idf = corpus_token_idf(conn, &content)?;
let total_idf: f32 = content
.iter()
.map(|t| idf.get(t).copied().unwrap_or(0.0))
.sum();
if total_idf > f32::EPSILON {
candidates.retain(|c| {
if c.capsule.kind != "memory" {
return true; }
if c.cosine.is_some_and(|cos| cos >= SEMANTIC_KEEP_COSINE) {
return true;
}
weighted_coverage(&content, &idf, &c.capsule.summary)
>= request.min_lexical_coverage
});
}
}
}
let stage_weights = weights_for_stage(weights, &request.stage);
let effective_weights = weights_for_task_kind(stage_weights, request.task_kind);
normalize_and_score(&mut candidates, effective_weights);
let kind_role_hints = task_kind_prefer_roles(request.task_kind);
let mut effective_prefer_roles: Vec<String> = request.prefer_roles.clone();
for &hint in kind_role_hints {
let hint_s = hint.to_string();
if !effective_prefer_roles.contains(&hint_s) {
effective_prefer_roles.push(hint_s);
}
}
if !request.tags.is_empty() || !effective_prefer_roles.is_empty() {
let tags_lc: Vec<String> = request
.tags
.iter()
.map(|t| t.to_ascii_lowercase())
.collect();
for c in &mut candidates {
let summary_lc = c.capsule.summary.to_ascii_lowercase();
if !tags_lc.is_empty() && tags_lc.iter().any(|t| summary_lc.contains(t.as_str())) {
c.capsule.score *= 1.4;
}
if !effective_prefer_roles.is_empty()
&& effective_prefer_roles.iter().any(|r| {
if c.capsule.kind == "memory" {
capsule_matches_kind(&c.capsule, r.as_str())
} else {
c.capsule.kind.contains(r.as_str())
}
})
{
c.capsule.score *= 1.3;
}
}
}
if query_embedding.is_some() && request.min_semantic_score > 0.0 {
candidates.retain(|c| {
match c.cosine {
Some(cos) => cos >= request.min_semantic_score,
None => true,
}
});
}
candidates.sort_by(|a, b| {
b.capsule
.score
.partial_cmp(&a.capsule.score)
.unwrap_or(Ordering::Equal)
.then_with(|| {
b.capsule
.freshness
.partial_cmp(&a.capsule.freshness)
.unwrap_or(Ordering::Equal)
})
.then_with(|| a.capsule.expansion_handle.cmp(&b.capsule.expansion_handle))
});
let embedding_mmr_ran = query_embedding.is_some() && !candidates.is_empty();
let candidates = if embedding_mmr_ran {
apply_candidate_mmr_diversity(candidates, 0.7)
} else {
candidates
};
let mut capsules = candidates
.into_iter()
.map(|candidate| candidate.capsule)
.collect::<Vec<_>>();
if !embedding_mmr_ran {
capsules.sort_by(|left, right| {
right
.score
.partial_cmp(&left.score)
.unwrap_or(Ordering::Equal)
.then_with(|| {
right
.freshness
.partial_cmp(&left.freshness)
.unwrap_or(Ordering::Equal)
})
.then_with(|| left.expansion_handle.cmp(&right.expansion_handle))
});
}
let top_score = capsules.first().map(|c| c.score).unwrap_or(0.0);
if request.min_score > 0.0 && top_score < request.min_score {
return Ok(ContextBundle {
stage: request.stage,
budget_tokens: request.budget_tokens,
used_tokens: 0,
capsules: Vec::new(),
excluded: capsules,
skipped: true,
top_score,
});
}
let capsules = apply_mmr_diversity(capsules, 0.7);
let capsule_budget = request.budget_tokens / 2;
let mut used_tokens = 0u32;
let mut included = Vec::new();
let mut excluded = Vec::new();
for capsule in capsules {
if request.max_capsules > 0 && included.len() >= request.max_capsules {
excluded.push(capsule);
continue;
}
if used_tokens.saturating_add(capsule.token_estimate) <= capsule_budget {
used_tokens += capsule.token_estimate;
included.push(capsule);
} else {
excluded.push(capsule);
}
}
Ok(ContextBundle {
stage: request.stage,
budget_tokens: request.budget_tokens,
used_tokens,
capsules: included,
excluded,
skipped: false,
top_score,
})
}
pub fn search_repo_files(
conn: &Connection,
repo_root: &str,
query: &str,
limit: u32,
) -> KimetsuResult<Vec<ContextCapsule>> {
let candidates = repo_file_candidates(conn, repo_root, query, limit)?;
let mut capsules = candidates
.into_iter()
.map(|mut candidate| {
candidate.capsule.relevance = candidate.raw_relevance;
candidate.capsule.score = candidate.raw_relevance;
candidate.capsule
})
.collect::<Vec<_>>();
capsules.sort_by(|left, right| {
right
.score
.partial_cmp(&left.score)
.unwrap_or(Ordering::Equal)
.then_with(|| left.expansion_handle.cmp(&right.expansion_handle))
});
Ok(capsules)
}
#[cfg(feature = "embeddings")]
fn memory_ann_candidates(
conn: &Connection,
qe: &QueryEmbedding,
k: u32,
query_tokens: &[String],
half_life_days: f32,
) -> KimetsuResult<Vec<Candidate>> {
let handle = crate::ann::handle_for_query(conn, qe.vector.len(), &qe.model_id)?;
let hits = handle
.read()
.unwrap_or_else(|p| p.into_inner())
.search(&qe.vector, k as usize)?;
let knn_rowids: Vec<i64> = hits.into_iter().map(|(rowid, _dist)| rowid).collect();
if knn_rowids.is_empty() {
return Ok(Vec::new());
}
let placeholders: String = knn_rowids
.iter()
.enumerate()
.map(|(i, _)| format!("?{}", i + 1))
.collect::<Vec<_>>()
.join(", ");
let sql = format!(
"SELECT memory_id, scope, kind, text, confidence, created_at,
use_count, usefulness_score, embedding, embedding_model,
last_useful_at
FROM memories
WHERE invalidated_at IS NULL
AND embedding_model = ?{model_param}
AND rowid IN ({placeholders})",
model_param = knn_rowids.len() + 1
);
let mut stmt = conn.prepare(&sql)?;
let mut params_vec: Vec<&dyn rusqlite::ToSql> = knn_rowids
.iter()
.map(|n| n as &dyn rusqlite::ToSql)
.collect();
params_vec.push(&qe.model_id);
let rows_iter = stmt.query_map(params_vec.as_slice(), |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, f32>(4)?,
row.get::<_, String>(5)?,
row.get::<_, i64>(6)?,
row.get::<_, f64>(7)?,
row.get::<_, Option<Vec<u8>>>(8)?,
row.get::<_, Option<String>>(9)?,
row.get::<_, Option<String>>(10)?,
))
})?;
let mut candidates = Vec::new();
for row in rows_iter {
let (
memory_id,
scope,
kind,
text,
confidence,
created_at,
use_count,
usefulness_score,
embedding,
embedding_model,
last_useful_at,
) = row?;
let (cosine, row_vec) =
compute_cosine_and_vec(Some(qe), embedding.as_deref(), embedding_model.as_deref());
if let Some(candidate) = memory_row_to_candidate(
query_tokens,
memory_id,
scope,
kind,
text,
confidence,
created_at,
use_count,
usefulness_score,
last_useful_at,
half_life_days,
None, cosine,
row_vec,
) {
candidates.push(candidate);
}
}
Ok(candidates)
}
fn memory_candidates(
conn: &Connection,
query: &str,
query_embedding: Option<&QueryEmbedding>,
half_life_days: f32,
) -> KimetsuResult<Vec<Candidate>> {
let query_tokens = query_tokens(query);
#[cfg(feature = "embeddings")]
if let Some(qe) = query_embedding {
let fts_candidates = if let Some(fts_query) = fts_query(query) {
memory_fts_candidates(
conn,
&query_tokens,
&fts_query,
80,
Some(qe),
half_life_days,
)?
} else {
Vec::new()
};
let ann_candidates = memory_ann_candidates(conn, qe, 80, &query_tokens, half_life_days)?;
let mut seen: HashMap<String, usize> = HashMap::new();
let mut merged: Vec<Candidate> = Vec::new();
for candidate in fts_candidates.into_iter().chain(ann_candidates) {
let mid = candidate
.capsule
.expansion_handle
.strip_prefix("memory:")
.unwrap_or(&candidate.capsule.expansion_handle)
.to_string();
if let Some(&idx) = seen.get(&mid) {
if candidate.raw_relevance > merged[idx].raw_relevance {
merged[idx] = candidate;
}
} else {
seen.insert(mid, merged.len());
merged.push(candidate);
}
}
return Ok(merged);
}
if let Some(fts_query) = fts_query(query) {
let candidates = memory_fts_candidates(
conn,
&query_tokens,
&fts_query,
80,
query_embedding,
half_life_days,
)?;
if !candidates.is_empty() {
return Ok(candidates);
}
}
latest_memory_candidates(conn, &query_tokens, 200, query_embedding, half_life_days)
}
fn latest_memory_candidates(
conn: &Connection,
query_tokens: &[String],
limit: u32,
query_embedding: Option<&QueryEmbedding>,
half_life_days: f32,
) -> KimetsuResult<Vec<Candidate>> {
let mut stmt = conn.prepare_cached(
"
SELECT memory_id, scope, kind, text, confidence, created_at,
use_count, usefulness_score, embedding, embedding_model,
last_useful_at
FROM memories
WHERE invalidated_at IS NULL
ORDER BY created_at DESC
LIMIT ?1
",
)?;
let rows = stmt.query_map(params![limit], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, f32>(4)?,
row.get::<_, String>(5)?,
row.get::<_, i64>(6)?,
row.get::<_, f64>(7)?,
row.get::<_, Option<Vec<u8>>>(8)?,
row.get::<_, Option<String>>(9)?,
row.get::<_, Option<String>>(10)?,
))
})?;
let mut candidates = Vec::new();
for row in rows {
let (
memory_id,
scope,
kind,
text,
confidence,
created_at,
use_count,
usefulness_score,
embedding,
embedding_model,
last_useful_at,
) = row?;
let (cosine, row_vec) = compute_cosine_and_vec(
query_embedding,
embedding.as_deref(),
embedding_model.as_deref(),
);
if let Some(candidate) = memory_row_to_candidate(
query_tokens,
memory_id,
scope,
kind,
text,
confidence,
created_at,
use_count,
usefulness_score,
last_useful_at,
half_life_days,
None,
cosine,
row_vec,
) {
candidates.push(candidate);
}
}
Ok(candidates)
}
fn memory_fts_candidates(
conn: &Connection,
query_tokens: &[String],
fts_query: &str,
limit: u32,
query_embedding: Option<&QueryEmbedding>,
half_life_days: f32,
) -> KimetsuResult<Vec<Candidate>> {
let mut stmt = conn.prepare_cached(
"
SELECT m.memory_id, m.scope, m.kind, m.text, m.confidence, m.created_at,
m.use_count, m.usefulness_score, bm25(memories_fts) AS rank,
m.embedding, m.embedding_model, m.last_useful_at
FROM memories_fts
JOIN memories m
ON m.memory_id = memories_fts.memory_id
WHERE m.invalidated_at IS NULL
AND memories_fts MATCH ?1
ORDER BY rank
LIMIT ?2
",
)?;
let rows = stmt.query_map(params![fts_query, limit], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, f32>(4)?,
row.get::<_, String>(5)?,
row.get::<_, i64>(6)?,
row.get::<_, f64>(7)?,
row.get::<_, f64>(8)?,
row.get::<_, Option<Vec<u8>>>(9)?,
row.get::<_, Option<String>>(10)?,
row.get::<_, Option<String>>(11)?,
))
})?;
let mut candidates = Vec::new();
for row in rows {
let (
memory_id,
scope,
kind,
text,
confidence,
created_at,
use_count,
usefulness_score,
rank,
embedding,
embedding_model,
last_useful_at,
) = row?;
let fts_relevance = (-rank as f32).max(0.0);
let (cosine, row_vec) = compute_cosine_and_vec(
query_embedding,
embedding.as_deref(),
embedding_model.as_deref(),
);
if let Some(candidate) = memory_row_to_candidate(
query_tokens,
memory_id,
scope,
kind,
text,
confidence,
created_at,
use_count,
usefulness_score,
last_useful_at,
half_life_days,
Some(fts_relevance),
cosine,
row_vec,
) {
candidates.push(candidate);
}
}
Ok(candidates)
}
fn compute_cosine_and_vec(
query_embedding: Option<&QueryEmbedding>,
row_bytes: Option<&[u8]>,
row_model: Option<&str>,
) -> (Option<f32>, Option<Vec<f32>>) {
let q = match query_embedding {
Some(q) => q,
None => return (None, None),
};
let bytes = match row_bytes {
Some(b) => b,
None => return (None, None),
};
let model = match row_model {
Some(m) => m,
None => return (None, None),
};
if model != q.model_id {
return (None, None);
}
let row_vec = match decode_embedding(bytes, Some(q.vector.len())) {
Ok(v) => v,
Err(_) => return (None, None),
};
let score = cosine_similarity(&q.vector, &row_vec);
(Some(score), Some(row_vec))
}
#[allow(clippy::too_many_arguments)]
fn memory_row_to_candidate(
query_tokens: &[String],
memory_id: String,
scope: String,
kind: String,
text: String,
confidence: f32,
created_at: String,
use_count: i64,
usefulness_score: f64,
last_useful_at: Option<String>,
half_life_days: f32,
raw_relevance_override: Option<f32>,
cosine_score: Option<f32>,
row_embedding: Option<Vec<f32>>,
) -> Option<Candidate> {
let lexical = lexical_relevance(query_tokens, &format!("{kind} {text}"));
let lexical_term = raw_relevance_override.unwrap_or(lexical).max(lexical);
let raw_relevance = match cosine_score {
Some(c) => {
let normalized_cos = ((c + 1.0) * 0.5).clamp(0.0, 1.0);
(1.0 - DEFAULT_HYBRID_ALPHA) * lexical_term + DEFAULT_HYBRID_ALPHA * normalized_cos
}
None => lexical_term,
};
if raw_relevance <= 0.0 && !query_tokens.is_empty() {
return None;
}
let freshness = freshness(&created_at);
let scope_weight = scope_weight(&scope);
let raw_multiplier = usefulness_multiplier(usefulness_score as f32, use_count as u32);
let decay = usefulness_decay(last_useful_at.as_deref(), &created_at, half_life_days);
let multiplier = 1.0 + (raw_multiplier - 1.0) * decay;
let biased_relevance = raw_relevance * multiplier;
Some(Candidate {
raw_relevance: biased_relevance,
embedding: row_embedding,
cosine: cosine_score,
capsule: ContextCapsule {
id: new_id().to_string(),
kind: "memory".to_string(),
summary: format!("{scope}:{kind} - {text}"),
token_estimate: estimate_tokens(&text) + 8,
expansion_handle: format!("memory:{memory_id}"),
provenance: vec![ProvenanceRef {
source: "Memory".to_string(),
id: memory_id,
excerpt: Some(excerpt(&text)),
}],
confidence,
freshness,
relevance: 0.0,
scope_weight,
score: 0.0,
},
})
}
pub(crate) fn usefulness_decay(
last_useful_at: Option<&str>,
created_at: &str,
half_life_days: f32,
) -> f32 {
if half_life_days <= 0.0 {
return 1.0;
}
let reference = last_useful_at.unwrap_or(created_at);
let Ok(reference_ts) =
OffsetDateTime::parse(reference, &time::format_description::well_known::Rfc3339)
else {
return 1.0;
};
let age = OffsetDateTime::now_utc() - reference_ts;
let age_days = (age.whole_seconds().max(0) as f32) / 86_400.0;
let exponent = -std::f32::consts::LN_2 * age_days / half_life_days;
exponent.exp().clamp(0.0, 1.0)
}
pub(crate) fn usefulness_multiplier(usefulness_score: f32, use_count: u32) -> f32 {
const FULL_CONFIDENCE_USES: u32 = 3;
const MULTIPLIER_MIN: f32 = 0.5;
const MULTIPLIER_MAX: f32 = 1.5;
if use_count == 0 {
return 1.0;
}
let ratio = usefulness_score / use_count as f32; let normalized = ((ratio + 1.0) / 2.0).clamp(0.0, 1.0); let full_multiplier = MULTIPLIER_MIN + normalized * (MULTIPLIER_MAX - MULTIPLIER_MIN);
let confidence = (use_count as f32 / FULL_CONFIDENCE_USES as f32).min(1.0);
1.0 * (1.0 - confidence) + full_multiplier * confidence
}
fn repo_file_candidates(
conn: &Connection,
repo_root: &str,
query: &str,
limit: u32,
) -> KimetsuResult<Vec<Candidate>> {
let Some(fts_query) = fts_query(query) else {
return Ok(Vec::new());
};
let mut stmt = conn.prepare_cached(
"
SELECT path, snippet, language_guess, bm25(repo_files_fts) AS rank
FROM repo_files_fts
WHERE repo_root = ?1 AND repo_files_fts MATCH ?2
ORDER BY rank
LIMIT ?3
",
)?;
let rows = stmt.query_map(params![repo_root, fts_query, limit], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, f64>(3)?,
))
})?;
let mut candidates = Vec::new();
for row in rows {
let (path, snippet, language, rank) = row?;
let raw_relevance = (-rank as f32).max(0.0);
let summary = format!("{path} ({language}) - {}", excerpt(&snippet));
let token_estimate = estimate_tokens(&summary) + 8;
candidates.push(Candidate {
raw_relevance,
embedding: None,
cosine: None,
capsule: ContextCapsule {
id: new_id().to_string(),
kind: "repo_file".to_string(),
summary,
token_estimate,
expansion_handle: format!("file:{path}"),
provenance: vec![ProvenanceRef {
source: "RepoFile".to_string(),
id: path.clone(),
excerpt: Some(excerpt(&snippet)),
}],
confidence: 0.9,
freshness: 1.0,
relevance: 0.0,
scope_weight: 0.9,
score: 0.0,
},
});
}
Ok(candidates)
}
fn manifest_candidates(
conn: &Connection,
repo_root: &str,
query: &str,
) -> KimetsuResult<Vec<Candidate>> {
if let Some(fts_query) = fts_query(query) {
let candidates = manifest_fts_candidates(conn, repo_root, &fts_query, 30)?;
if !candidates.is_empty() {
return Ok(candidates);
}
}
let query_tokens = query_tokens(query);
let mut stmt = conn.prepare_cached(
"
SELECT manifest_path, manifest_kind, parsed_summary_json
FROM repo_manifests
WHERE repo_root = ?1
ORDER BY manifest_path
",
)?;
let rows = stmt.query_map(params![repo_root], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?;
let mut candidates = Vec::new();
for row in rows {
let (path, kind, summary_json) = row?;
let raw_relevance =
lexical_relevance(&query_tokens, &format!("{path} {kind} {summary_json}"));
if raw_relevance <= 0.0 && !query_tokens.is_empty() {
continue;
}
let summary = format!("{path} manifest ({kind})");
let token_estimate = estimate_tokens(&summary) + 8;
candidates.push(Candidate {
raw_relevance,
embedding: None,
cosine: None,
capsule: ContextCapsule {
id: new_id().to_string(),
kind: "repo_manifest".to_string(),
summary,
token_estimate,
expansion_handle: format!("file:{path}"),
provenance: vec![ProvenanceRef {
source: "Manifest".to_string(),
id: path,
excerpt: Some(excerpt(&summary_json)),
}],
confidence: 0.95,
freshness: 1.0,
relevance: 0.0,
scope_weight: 0.9,
score: 0.0,
},
});
}
Ok(candidates)
}
fn manifest_fts_candidates(
conn: &Connection,
repo_root: &str,
fts_query: &str,
limit: u32,
) -> KimetsuResult<Vec<Candidate>> {
let mut stmt = conn.prepare_cached(
"
SELECT manifest_path, manifest_kind, parsed_summary_json,
bm25(repo_manifests_fts) AS rank
FROM repo_manifests_fts
WHERE repo_root = ?1 AND repo_manifests_fts MATCH ?2
ORDER BY rank
LIMIT ?3
",
)?;
let rows = stmt.query_map(params![repo_root, fts_query, limit], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, f64>(3)?,
))
})?;
let mut candidates = Vec::new();
for row in rows {
let (path, kind, summary_json, rank) = row?;
let raw_relevance = (-rank as f32).max(0.0);
let summary = format!("{path} manifest ({kind})");
let token_estimate = estimate_tokens(&summary) + 8;
candidates.push(Candidate {
raw_relevance,
embedding: None,
cosine: None,
capsule: ContextCapsule {
id: new_id().to_string(),
kind: "repo_manifest".to_string(),
summary,
token_estimate,
expansion_handle: format!("file:{path}"),
provenance: vec![ProvenanceRef {
source: "Manifest".to_string(),
id: path,
excerpt: Some(excerpt(&summary_json)),
}],
confidence: 0.95,
freshness: 1.0,
relevance: 0.0,
scope_weight: 0.9,
score: 0.0,
},
});
}
Ok(candidates)
}
fn normalize_and_score(candidates: &mut [Candidate], weights: StageWeights) {
let mut max_by_kind = HashMap::<String, f32>::new();
for candidate in candidates.iter() {
max_by_kind
.entry(candidate.capsule.kind.clone())
.and_modify(|max| *max = (*max).max(candidate.raw_relevance))
.or_insert(candidate.raw_relevance);
}
for candidate in candidates {
let max = max_by_kind
.get(&candidate.capsule.kind)
.copied()
.unwrap_or(0.0);
let relevance = if max <= f32::EPSILON {
if candidate.raw_relevance > 0.0 {
1.0
} else {
0.0
}
} else {
(candidate.raw_relevance / max).clamp(0.0, 1.0)
};
candidate.capsule.relevance = relevance;
candidate.capsule.score = weights.relevance * relevance
+ weights.confidence * candidate.capsule.confidence
+ weights.freshness * candidate.capsule.freshness
+ weights.scope * candidate.capsule.scope_weight;
}
}
fn weights_for_stage(weights: &BrokerWeights, stage: &str) -> StageWeights {
match stage {
"localization" => weights.localization.clone(),
"patch_plan" => weights.patch_plan.clone(),
"verification" => weights.verification.clone(),
"review" => weights.review.clone(),
_ => None,
}
.unwrap_or(StageWeights {
relevance: weights.relevance,
confidence: weights.confidence,
freshness: weights.freshness,
scope: weights.scope,
})
}
fn scope_weight(scope: &str) -> f32 {
match scope.parse::<MemoryScope>() {
Ok(MemoryScope::Run) => 1.0,
Ok(MemoryScope::Repo) => 0.9,
Ok(MemoryScope::Project) => 0.7,
Ok(MemoryScope::GlobalUser) => 0.5,
Err(_) => 0.3,
}
}
fn freshness(created_at: &str) -> f32 {
let Ok(created_at) =
OffsetDateTime::parse(created_at, &time::format_description::well_known::Rfc3339)
else {
return 0.5;
};
let age = OffsetDateTime::now_utc() - created_at;
let age_days = age.whole_seconds().max(0) as f32 / 86_400.0;
(-age_days / 30.0).exp().clamp(0.0, 1.0)
}
const SEMANTIC_KEEP_COSINE: f32 = 0.20;
const STOPWORDS: &[&str] = &[
"the", "and", "for", "are", "but", "not", "you", "your", "with", "this", "that", "these",
"those", "from", "into", "about", "what", "whats", "which", "who", "whom", "how", "why",
"when", "where", "can", "could", "would", "should", "will", "shall", "does", "did", "was",
"were", "been", "being", "have", "has", "had", "its", "it", "is", "as", "at", "by", "of", "to",
"in", "on", "or", "an", "be", "do", "me", "my", "we", "us", "our", "im", "ive", "let", "lets",
"please", "tell", "give", "show", "want", "need", "get", "got", "use", "using", "there",
"their", "they", "them", "then", "than", "some", "any", "all", "more", "most", "such", "via",
"per",
];
fn content_tokens(query: &str) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
query
.split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_')
.map(str::trim)
.filter(|part| part.len() >= 2)
.map(str::to_ascii_lowercase)
.filter(|t| !STOPWORDS.contains(&t.as_str()))
.map(|t| light_stem(&t).to_string())
.filter(|t| seen.insert(t.clone()))
.collect()
}
fn corpus_token_idf(conn: &Connection, tokens: &[String]) -> KimetsuResult<HashMap<String, f32>> {
let mut idf = HashMap::new();
let n: i64 = conn
.query_row(
"SELECT COUNT(*) FROM memories WHERE invalidated_at IS NULL",
[],
|row| row.get(0),
)
.unwrap_or(0);
if n == 0 {
return Ok(idf);
}
let mut stmt = conn.prepare_cached(
"SELECT COUNT(*) FROM memories \
WHERE invalidated_at IS NULL AND lower(text) LIKE ?1 ESCAPE '\\'",
)?;
for token in tokens {
let pattern = format!("%{}%", escape_like(token));
let df: i64 = stmt
.query_row(params![pattern], |row| row.get(0))
.unwrap_or(0);
let weight = if df == 0 {
0.0
} else {
(((n + 1) as f32) / ((df + 1) as f32)).ln().max(0.0)
};
idf.insert(token.clone(), weight);
}
Ok(idf)
}
fn escape_like(token: &str) -> String {
token
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_")
}
fn weighted_coverage(content: &[String], idf: &HashMap<String, f32>, summary: &str) -> f32 {
let haystack = summary.to_ascii_lowercase();
let mut total = 0.0f32;
let mut hit = 0.0f32;
for token in content {
let weight = idf.get(token).copied().unwrap_or(0.0);
total += weight;
if weight > 0.0 && haystack.contains(token.as_str()) {
hit += weight;
}
}
if total <= f32::EPSILON {
0.0
} else {
(hit / total).clamp(0.0, 1.0)
}
}
fn light_stem(token: &str) -> &str {
for suffix in ["ing", "ed", "es", "s"] {
if let Some(stem) = token.strip_suffix(suffix)
&& stem.len() >= 4
{
return stem;
}
}
token
}
fn query_tokens(query: &str) -> Vec<String> {
let mut tokens: Vec<String> = query
.split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_')
.map(str::trim)
.filter(|part| part.len() >= 2)
.map(str::to_ascii_lowercase)
.map(|t| light_stem(&t).to_string())
.collect();
let lower = query.to_ascii_lowercase();
for (triggers, expansions) in CLASS_HINTS.iter() {
if triggers.iter().any(|t| lower.contains(t)) {
tokens.extend(expansions.iter().map(|e| e.to_string()));
}
}
tokens
}
const CLASS_HINTS: &[(&[&str], &[&str])] = &[
(
&[
"build",
"compile",
"make",
"cargo",
"cmake",
"configure",
"install",
"train",
"benchmark",
"test suite",
"ray trace",
"render",
],
&[
"shell_background",
"shell_status",
"shell_output",
"shell_stop",
"long_running",
],
),
(
&[
"edit", "modify", "change", "fix", "update", "patch", "refactor", "rename",
],
&["edit_file", "apply_patch", "old_string", "new_string"],
),
(
&[
"read", "inspect", "review", "analyze", "examine", "view", "show",
],
&["read_file", "offset", "limit", "multi_read"],
),
(
&["find", "locate", "search", "look up", "discover", "list"],
&["glob", "search_files", "list_files"],
),
(
&["plan", "step", "checklist", "todo", "task list", "phase"],
&["plan", "todos"],
),
(
&[
"verify",
"check",
"ensure",
"validate",
"pass test",
"verifier",
],
&["finish", "verifier", "verification"],
),
(
&[
"image",
"png",
"jpeg",
"jpg",
"pdf",
"diagram",
"screenshot",
],
&["view_image", "base64", "sha256"],
),
(&["delete", "remove", "rm "], &["delete_file", "recursive"]),
(&["rename", "move file", "mv "], &["move_file"]),
];
fn capsule_matches_kind(capsule: &ContextCapsule, wanted: &str) -> bool {
if capsule.kind == wanted {
return true;
}
if capsule.kind == "memory"
&& let Some((prefix, _)) = capsule.summary.split_once(" - ")
&& let Some((_scope, mkind)) = prefix.split_once(':')
{
return mkind == wanted;
}
false
}
pub(crate) fn fts_query(query: &str) -> Option<String> {
let tokens = query_tokens(query);
if tokens.is_empty() {
return None;
}
Some(
tokens
.into_iter()
.take(12)
.map(|token| format!("{token}*"))
.collect::<Vec<_>>()
.join(" OR "),
)
}
fn apply_candidate_mmr_diversity(mut sorted: Vec<Candidate>, lambda: f32) -> Vec<Candidate> {
if sorted.len() <= 1 {
return sorted;
}
let summaries: Vec<std::collections::HashSet<String>> = sorted
.iter()
.map(|c| summary_token_set(&c.capsule.summary))
.collect();
let mut picked_indices: Vec<usize> = Vec::with_capacity(sorted.len());
let mut remaining: Vec<usize> = (0..sorted.len()).collect();
picked_indices.push(remaining.remove(0));
while !remaining.is_empty() {
let mut best_idx_in_remaining = 0;
let mut best_score = f32::MIN;
for (i, &cand) in remaining.iter().enumerate() {
let mut max_overlap = 0.0f32;
for &p in &picked_indices {
let same_kind = sorted[cand].capsule.kind == sorted[p].capsule.kind;
let raw_overlap = candidate_pair_overlap(
&sorted[cand],
&sorted[p],
&summaries[cand],
&summaries[p],
);
let overlap = if same_kind {
raw_overlap
} else {
raw_overlap * 0.5
};
if overlap > max_overlap {
max_overlap = overlap;
}
}
let mmr = lambda * sorted[cand].capsule.score - (1.0 - lambda) * max_overlap;
if mmr > best_score {
best_score = mmr;
best_idx_in_remaining = i;
}
}
picked_indices.push(remaining.remove(best_idx_in_remaining));
}
let mut taken: Vec<Option<Candidate>> = sorted.drain(..).map(Some).collect();
let mut out = Vec::with_capacity(taken.len());
for idx in picked_indices {
if let Some(c) = taken[idx].take() {
out.push(c);
}
}
out
}
fn candidate_pair_overlap(
a: &Candidate,
b: &Candidate,
tokens_a: &std::collections::HashSet<String>,
tokens_b: &std::collections::HashSet<String>,
) -> f32 {
if let (Some(va), Some(vb)) = (a.embedding.as_deref(), b.embedding.as_deref()) {
cosine_similarity(va, vb).max(0.0)
} else {
jaccard(tokens_a, tokens_b)
}
}
fn apply_mmr_diversity(mut sorted: Vec<ContextCapsule>, lambda: f32) -> Vec<ContextCapsule> {
if sorted.len() <= 1 {
return sorted;
}
let summaries: Vec<std::collections::HashSet<String>> = sorted
.iter()
.map(|c| summary_token_set(&c.summary))
.collect();
let mut picked_indices: Vec<usize> = Vec::with_capacity(sorted.len());
let mut remaining: Vec<usize> = (0..sorted.len()).collect();
picked_indices.push(remaining.remove(0));
while !remaining.is_empty() {
let mut best_idx_in_remaining = 0;
let mut best_score = f32::MIN;
for (i, &cand) in remaining.iter().enumerate() {
let mut max_overlap = 0.0f32;
for &p in &picked_indices {
let raw = jaccard(&summaries[cand], &summaries[p]);
let overlap = if sorted[cand].kind == sorted[p].kind {
raw
} else {
raw * 0.5
};
if overlap > max_overlap {
max_overlap = overlap;
}
}
let mmr = lambda * sorted[cand].score - (1.0 - lambda) * max_overlap;
if mmr > best_score {
best_score = mmr;
best_idx_in_remaining = i;
}
}
picked_indices.push(remaining.remove(best_idx_in_remaining));
}
let mut out = Vec::with_capacity(sorted.len());
let mut taken: Vec<Option<ContextCapsule>> = sorted.drain(..).map(Some).collect();
for idx in picked_indices {
if let Some(c) = taken[idx].take() {
out.push(c);
}
}
out
}
fn summary_token_set(s: &str) -> std::collections::HashSet<String> {
s.split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_')
.filter(|t| t.len() >= 3)
.map(str::to_ascii_lowercase)
.collect()
}
fn jaccard(a: &std::collections::HashSet<String>, b: &std::collections::HashSet<String>) -> f32 {
if a.is_empty() && b.is_empty() {
return 0.0;
}
let intersection = a.intersection(b).count();
let union = a.union(b).count();
intersection as f32 / union.max(1) as f32
}
fn lexical_relevance(tokens: &[String], haystack: &str) -> f32 {
if tokens.is_empty() {
return 0.0;
}
let haystack = haystack.to_ascii_lowercase();
let matches = tokens
.iter()
.filter(|token| haystack.contains(token.as_str()))
.count();
matches as f32 / tokens.len() as f32
}
fn estimate_tokens(text: &str) -> u32 {
((text.split_whitespace().count() as f32) * 1.33).ceil() as u32
}
fn excerpt(text: &str) -> String {
let value = one_line(text);
value.chars().take(256).collect()
}
fn one_line(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
const FILE_EXPAND_CAP_BYTES: usize = 2048;
pub fn resolve_capsule(
conn: &Connection,
repo_root: &std::path::Path,
handle: &str,
) -> kimetsu_core::KimetsuResult<String> {
if let Some(memory_id) = handle.strip_prefix("memory:") {
let mut stmt = conn.prepare_cached(
"SELECT text FROM memories WHERE memory_id = ? AND invalidated_at IS NULL",
)?;
let text: Option<String> = stmt
.query_row(rusqlite::params![memory_id], |row| row.get(0))
.optional()?;
match text {
Some(t) => Ok(t),
None => {
Err(format!("expand_capsule: no active memory found for handle `{handle}`").into())
}
}
} else if let Some(rel_path) = handle.strip_prefix("file:") {
let path = std::path::Path::new(rel_path);
if path.is_absolute() {
return Err(format!(
"expand_capsule: `{handle}` is an absolute path — only repo-relative paths are supported"
)
.into());
}
for component in path.components() {
match component {
std::path::Component::ParentDir => {
return Err(format!(
"expand_capsule: `{handle}` contains `..` traversal — rejected"
)
.into());
}
std::path::Component::RootDir | std::path::Component::Prefix(_) => {
return Err(format!(
"expand_capsule: `{handle}` is an absolute path — only repo-relative paths are supported"
)
.into());
}
_ => {}
}
}
let full_path = repo_root.join(path);
let bytes = std::fs::read(&full_path)
.map_err(|e| format!("expand_capsule: could not read `{rel_path}`: {e}"))?;
let bounded = if bytes.len() > FILE_EXPAND_CAP_BYTES {
let mut end = FILE_EXPAND_CAP_BYTES;
while end > 0 && (bytes[end] & 0xC0) == 0x80 {
end -= 1;
}
let s = String::from_utf8_lossy(&bytes[..end]);
format!(
"{s}\n[... truncated at {FILE_EXPAND_CAP_BYTES} bytes; call expand_capsule again with a line range if needed]"
)
} else {
String::from_utf8_lossy(&bytes).into_owned()
};
Ok(bounded)
} else if handle.starts_with("run:") {
Err(format!(
"expand_capsule: `run:` handle expansion is not yet supported (handle: `{handle}`)"
)
.into())
} else {
Err(format!(
"expand_capsule: unrecognised handle format `{handle}`; \
expected `memory:<id>`, `file:<path>`, or `run:<id>`"
)
.into())
}
}
pub fn rerank_capsules(
query: &str,
capsules: Vec<ContextCapsule>,
reranker: &dyn crate::embeddings::Reranker,
floor: f32,
cap: usize,
) -> Vec<ContextCapsule> {
if capsules.is_empty() {
return capsules;
}
let docs: Vec<&str> = capsules.iter().map(|c| c.summary.as_str()).collect();
let scores = match reranker.rerank(query, &docs) {
Ok(s) if s.len() == docs.len() => s,
_ => {
let mut out = capsules;
if cap > 0 && out.len() > cap {
out.truncate(cap);
}
return out;
}
};
let mut ranked: Vec<ContextCapsule> = capsules
.into_iter()
.zip(scores)
.map(|(mut c, s)| {
c.score = s;
c
})
.collect();
ranked.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
ranked.retain(|c| c.score >= floor);
if cap > 0 && ranked.len() > cap {
ranked.truncate(cap);
}
ranked
}
#[cfg(test)]
mod tests {
use super::*;
fn capsule(kind: &str, summary: &str) -> ContextCapsule {
ContextCapsule {
id: "c".into(),
kind: kind.into(),
summary: summary.into(),
token_estimate: 1,
expansion_handle: "memory:x".into(),
provenance: vec![],
confidence: 1.0,
freshness: 1.0,
relevance: 1.0,
scope_weight: 1.0,
score: 1.0,
}
}
fn make_test_dir(tag: &str) -> std::path::PathBuf {
use std::time::{SystemTime, UNIX_EPOCH};
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!("kbrain_test_{tag}_{ts}"));
std::fs::create_dir_all(&dir).expect("create test dir");
dir
}
#[test]
fn capsule_matches_kind_reads_memory_summary_prefix() {
let mem = capsule("memory", "project:failure_pattern - linker not found");
assert!(capsule_matches_kind(&mem, "failure_pattern"));
assert!(!capsule_matches_kind(&mem, "command"));
let repo = capsule("repo_file", "src/lib.rs:command - run build");
assert!(capsule_matches_kind(&repo, "repo_file"));
assert!(!capsule_matches_kind(&repo, "command"));
}
#[test]
fn usefulness_multiplier_neutral_at_zero_uses() {
assert!((usefulness_multiplier(0.0, 0) - 1.0).abs() < f32::EPSILON);
assert!((usefulness_multiplier(5.0, 0) - 1.0).abs() < f32::EPSILON);
assert!((usefulness_multiplier(-5.0, 0) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn usefulness_multiplier_blends_smoothly_in_transition() {
let one_use = usefulness_multiplier(1.0, 1);
assert!((one_use - 1.166_666_6).abs() < 1e-4, "got {one_use}");
let two_uses = usefulness_multiplier(2.0, 2);
assert!((two_uses - 1.333_333_4).abs() < 1e-4, "got {two_uses}");
let two_uses_bad = usefulness_multiplier(-2.0, 2);
assert!(
(two_uses_bad - 0.666_666_7).abs() < 1e-4,
"got {two_uses_bad}"
);
}
#[test]
fn usefulness_multiplier_maps_ratio_onto_envelope() {
assert!((usefulness_multiplier(5.0, 5) - 1.5).abs() < f32::EPSILON);
assert!((usefulness_multiplier(-5.0, 5) - 0.5).abs() < f32::EPSILON);
let mid = usefulness_multiplier(0.0, 6);
assert!((mid - 1.0).abs() < f32::EPSILON, "got {mid}");
let high = usefulness_multiplier(2.0, 4);
assert!((high - 1.25).abs() < f32::EPSILON, "got {high}");
let low = usefulness_multiplier(-2.0, 4);
assert!((low - 0.75).abs() < f32::EPSILON, "got {low}");
}
#[test]
fn usefulness_multiplier_clamps_to_envelope() {
assert!((usefulness_multiplier(100.0, 5) - 1.5).abs() < f32::EPSILON);
assert!((usefulness_multiplier(-100.0, 5) - 0.5).abs() < f32::EPSILON);
}
#[test]
fn query_tokens_expands_build_class() {
let toks = query_tokens("Build the project from source");
assert!(toks.iter().any(|t| t == "build"));
assert!(toks.iter().any(|t| t == "shell_background"));
assert!(toks.iter().any(|t| t == "long_running"));
}
#[test]
fn query_tokens_expands_edit_class() {
let toks = query_tokens("Modify the config to fix the bug");
assert!(toks.iter().any(|t| t == "edit_file"));
assert!(toks.iter().any(|t| t == "apply_patch"));
}
#[test]
fn query_tokens_expands_search_class() {
let toks = query_tokens("Find all references to the symbol");
assert!(toks.iter().any(|t| t == "glob"));
assert!(toks.iter().any(|t| t == "search_files"));
}
#[test]
fn query_tokens_no_expansion_on_unrelated_query() {
let toks = query_tokens("hello world testing nothing");
assert!(toks.iter().any(|t| t == "hello"));
assert!(toks.iter().any(|t| t == "world"));
}
#[test]
fn jaccard_is_zero_for_disjoint_sets() {
let a: std::collections::HashSet<String> =
["foo", "bar"].iter().map(|s| s.to_string()).collect();
let b: std::collections::HashSet<String> =
["baz", "qux"].iter().map(|s| s.to_string()).collect();
assert!((jaccard(&a, &b) - 0.0).abs() < f32::EPSILON);
}
#[test]
fn jaccard_is_one_for_identical_sets() {
let a: std::collections::HashSet<String> =
["foo", "bar"].iter().map(|s| s.to_string()).collect();
let b = a.clone();
assert!((jaccard(&a, &b) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn jaccard_partial_overlap() {
let a: std::collections::HashSet<String> = ["foo", "bar", "baz"]
.iter()
.map(|s| s.to_string())
.collect();
let b: std::collections::HashSet<String> =
["bar", "qux"].iter().map(|s| s.to_string()).collect();
assert!((jaccard(&a, &b) - 0.25).abs() < f32::EPSILON);
}
#[test]
fn summary_token_set_lowercases_and_filters_short() {
let set = summary_token_set("Build the Foo-bar project");
assert!(set.contains("build"));
assert!(set.contains("foo"));
assert!(set.contains("bar"));
assert!(set.contains("project"));
assert!(set.contains("the"));
}
fn insert_memory_with_embedding(
conn: &rusqlite::Connection,
memory_id: &str,
text: &str,
embedder: &dyn embeddings::Embedder,
) {
let normalized = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"
INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score, embedding, embedding_model
)
VALUES (?1, 'global_user', 'fact', ?2, ?3, 1.0, NULL, '{}',
'2026-05-01T00:00:00Z', 0, 0.0, ?4, ?5)
",
rusqlite::params![
memory_id,
text,
normalized,
embeddings::encode_embedding(&embedder.embed(text).expect("embed test row")),
embedder.model_id(),
],
)
.expect("insert memory");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope) VALUES (?1, ?2, 'fact', 'global_user')",
rusqlite::params![memory_id, text],
)
.expect("insert fts row");
}
#[test]
fn hybrid_retrieval_uses_cosine_score_to_rerank() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let stub = embeddings::StubEmbedder::new();
insert_memory_with_embedding(&conn, "m_rg", "use ripgrep for code search", &stub);
insert_memory_with_embedding(
&conn,
"m_unrelated",
"cookie recipe with chocolate chips",
&stub,
);
let weights = kimetsu_core::config::BrokerWeights::default();
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "ripgrep search".to_string(),
budget_tokens: 4000,
..Default::default()
},
&[],
&stub,
)
.expect("retrieve");
let memory_handles: Vec<_> = bundle
.capsules
.iter()
.filter(|c| c.expansion_handle.starts_with("memory:"))
.collect();
assert!(
!memory_handles.is_empty(),
"at least one memory should surface"
);
assert_eq!(
memory_handles[0].expansion_handle,
"memory:m_rg",
"ripgrep memory should outrank the cookie recipe; ranked: {:?}",
memory_handles
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
}
#[test]
fn hybrid_retrieval_skips_cosine_on_model_id_mismatch() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let stub = embeddings::StubEmbedder::new();
insert_memory_with_embedding(&conn, "m_xref", "use ripgrep for code search", &stub);
conn.execute(
"UPDATE memories SET embedding_model = 'bge-small-en-v1.5' WHERE memory_id = 'm_xref'",
[],
)
.expect("force model_id mismatch");
let weights = kimetsu_core::config::BrokerWeights::default();
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "ripgrep search".to_string(),
budget_tokens: 4000,
..Default::default()
},
&[],
&stub,
)
.expect("retrieve");
assert!(
bundle
.capsules
.iter()
.any(|c| c.expansion_handle == "memory:m_xref"),
"cross-model row should still match lexically (cosine skipped, FTS works)"
);
}
#[test]
fn usefulness_decay_disabled_when_half_life_is_zero_or_negative() {
let ancient = "2021-01-01T00:00:00Z";
assert!((usefulness_decay(Some(ancient), ancient, 0.0) - 1.0).abs() < f32::EPSILON);
assert!((usefulness_decay(Some(ancient), ancient, -1.0) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn usefulness_decay_returns_one_on_unparseable_timestamps() {
assert!(
(usefulness_decay(Some("not-a-date"), "also-not", 30.0) - 1.0).abs() < f32::EPSILON
);
}
#[test]
fn usefulness_decay_full_at_zero_age() {
let future = "2099-01-01T00:00:00Z";
let d = usefulness_decay(Some(future), future, 30.0);
assert!((d - 1.0).abs() < f32::EPSILON, "got {d}");
}
#[test]
fn usefulness_decay_follows_half_life_curve() {
let half_life = 10.0_f32;
let now = OffsetDateTime::now_utc();
let fmt = &time::format_description::well_known::Rfc3339;
let one_half_life_ago = (now - time::Duration::seconds((half_life * 86_400.0) as i64))
.format(fmt)
.expect("format");
let d1 = usefulness_decay(Some(&one_half_life_ago), &one_half_life_ago, half_life);
assert!(
(d1 - 0.5).abs() < 0.01,
"expected ~0.5 at one half-life, got {d1}"
);
let two_half_lives_ago = (now
- time::Duration::seconds((2.0 * half_life * 86_400.0) as i64))
.format(fmt)
.expect("format");
let d2 = usefulness_decay(Some(&two_half_lives_ago), &two_half_lives_ago, half_life);
assert!(
(d2 - 0.25).abs() < 0.01,
"expected ~0.25 at two half-lives, got {d2}"
);
}
#[test]
fn usefulness_decay_falls_back_to_created_at_when_last_useful_is_none() {
let now = OffsetDateTime::now_utc();
let fmt = &time::format_description::well_known::Rfc3339;
let one_day_ago = (now - time::Duration::seconds(86_400))
.format(fmt)
.expect("format");
let d = usefulness_decay(None, &one_day_ago, 30.0);
assert!(
(d - 0.977).abs() < 0.01,
"expected ~0.977 for 1-day-old created_at under 30d half-life, got {d}"
);
}
#[test]
fn aged_cited_memory_ranks_below_recently_cited_memory() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let now = OffsetDateTime::now_utc();
let fmt = &time::format_description::well_known::Rfc3339;
let one_day_ago = (now - time::Duration::seconds(86_400))
.format(fmt)
.expect("format");
let one_year_ago = (now - time::Duration::seconds(365 * 86_400))
.format(fmt)
.expect("format");
for (mid, last_useful) in [("m_recent", &one_day_ago), ("m_aged", &one_year_ago)] {
let text = "use ripgrep for code search";
let normalized = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"
INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score, last_useful_at
)
VALUES (?1, 'global_user', 'fact', ?2, ?3, 1.0, NULL, '{}',
'2024-01-01T00:00:00Z', 5, 5.0, ?4)
",
rusqlite::params![mid, text, normalized, last_useful],
)
.expect("insert memory");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, 'fact', 'global_user')",
rusqlite::params![mid, text],
)
.expect("insert fts");
}
let weights = kimetsu_core::config::BrokerWeights::default();
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "ripgrep search".to_string(),
budget_tokens: 4000,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve");
let mem_order: Vec<&str> = bundle
.capsules
.iter()
.filter_map(|c| c.expansion_handle.strip_prefix("memory:"))
.collect();
assert_eq!(
mem_order.first().copied(),
Some("m_recent"),
"recently-cited memory must rank first under decay; got order {mem_order:?}"
);
}
#[test]
fn aged_cited_memory_does_not_decay_when_half_life_is_zero() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let now = OffsetDateTime::now_utc();
let fmt = &time::format_description::well_known::Rfc3339;
let one_day_ago = (now - time::Duration::seconds(86_400))
.format(fmt)
.expect("format");
let one_year_ago = (now - time::Duration::seconds(365 * 86_400))
.format(fmt)
.expect("format");
for (mid, last_useful) in [("m_recent", &one_day_ago), ("m_aged", &one_year_ago)] {
let text = "use ripgrep for code search";
let normalized = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"
INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score, last_useful_at
)
VALUES (?1, 'global_user', 'fact', ?2, ?3, 1.0, NULL, '{}',
'2024-01-01T00:00:00Z', 5, 5.0, ?4)
",
rusqlite::params![mid, text, normalized, last_useful],
)
.expect("insert memory");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, 'fact', 'global_user')",
rusqlite::params![mid, text],
)
.expect("insert fts");
}
let weights = kimetsu_core::config::BrokerWeights {
decay_half_life_days: 0.0,
..Default::default()
};
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "ripgrep search".to_string(),
budget_tokens: 4000,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve");
let scores: Vec<(String, f32)> = bundle
.capsules
.iter()
.filter_map(|c| {
c.expansion_handle
.strip_prefix("memory:")
.map(|id| (id.to_string(), c.score))
})
.collect();
assert_eq!(scores.len(), 2, "both memories should surface");
let recent_score = scores
.iter()
.find(|(id, _)| id == "m_recent")
.map(|(_, s)| *s)
.expect("m_recent present");
let aged_score = scores
.iter()
.find(|(id, _)| id == "m_aged")
.map(|(_, s)| *s)
.expect("m_aged present");
assert!(
(recent_score - aged_score).abs() < 1e-4,
"with decay disabled the two memories should tie on score: recent={recent_score} aged={aged_score}"
);
}
#[test]
fn hybrid_retrieval_with_noop_embedder_is_lexical_only() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let stub = embeddings::StubEmbedder::new();
insert_memory_with_embedding(&conn, "m_a", "use ripgrep", &stub);
insert_memory_with_embedding(&conn, "m_b", "use ripgrep too", &stub);
let weights = kimetsu_core::config::BrokerWeights::default();
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "ripgrep".to_string(),
budget_tokens: 4000,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve");
let count = bundle
.capsules
.iter()
.filter(|c| c.expansion_handle.starts_with("memory:"))
.count();
assert_eq!(count, 2, "both memories should surface via FTS");
}
#[cfg(feature = "embeddings")]
#[test]
fn ann_finds_semantic_match_fts_misses() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
struct OracleEmbedder;
impl embeddings::Embedder for OracleEmbedder {
fn embed(&self, _text: &str) -> Result<Vec<f32>, embeddings::EmbedderError> {
Ok(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
}
fn model_id(&self) -> &str {
"oracle-d8"
}
fn dim(&self) -> usize {
8
}
}
let model_id = "oracle-d8";
let sem_vec = embeddings::encode_embedding(&[1.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let sem_text = "cookie recipe chocolate";
let sem_norm = kimetsu_core::memory::normalize_memory_text(sem_text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score, embedding, embedding_model
)
VALUES ('m_semantic', 'global_user', 'fact', ?1, ?2, 1.0, NULL, '{}',
'2026-01-01T00:00:00Z', 0, 0.0, ?3, ?4)",
rusqlite::params![sem_text, sem_norm, sem_vec, model_id],
)
.expect("insert m_semantic");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES ('m_semantic', ?1, 'fact', 'global_user')",
rusqlite::params![sem_text],
)
.expect("insert m_semantic fts");
let decoy_vec = embeddings::encode_embedding(&[0.0f32, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let decoy_text = "git rebase squash commits";
let decoy_norm = kimetsu_core::memory::normalize_memory_text(decoy_text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score, embedding, embedding_model
)
VALUES ('m_decoy', 'global_user', 'fact', ?1, ?2, 1.0, NULL, '{}',
'2026-01-01T00:00:00Z', 0, 0.0, ?3, ?4)",
rusqlite::params![decoy_text, decoy_norm, decoy_vec, model_id],
)
.expect("insert m_decoy");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES ('m_decoy', ?1, 'fact', 'global_user')",
rusqlite::params![decoy_text],
)
.expect("insert m_decoy fts");
let fts_hits: i64 = conn
.query_row(
"SELECT COUNT(*) FROM memories_fts \
WHERE memories_fts MATCH 'phosphorescent bioluminescent'",
[],
|r| r.get(0),
)
.unwrap_or(0);
assert_eq!(
fts_hits, 0,
"sanity: query tokens must not appear in any memory text"
);
let weights = kimetsu_core::config::BrokerWeights::default();
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "phosphorescent bioluminescent organism".to_string(),
budget_tokens: 4000,
..Default::default()
},
&[],
&OracleEmbedder,
)
.expect("retrieve");
let handles: Vec<&str> = bundle
.capsules
.iter()
.filter_map(|c| c.expansion_handle.strip_prefix("memory:"))
.collect();
assert!(
handles.contains(&"m_semantic"),
"ANN must surface m_semantic (cosine=1 with oracle query) even though \
FTS found nothing; got handles: {handles:?}"
);
}
#[cfg(feature = "embeddings")]
#[test]
fn dedup_memory_matched_by_fts_and_ann_appears_once() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let stub = embeddings::StubEmbedder::new();
insert_memory_with_embedding(&conn, "m_both", "use ripgrep for fast search", &stub);
let weights = kimetsu_core::config::BrokerWeights::default();
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "ripgrep".to_string(),
budget_tokens: 4000,
..Default::default()
},
&[],
&stub,
)
.expect("retrieve");
let count = bundle
.capsules
.iter()
.filter(|c| c.expansion_handle == "memory:m_both")
.count();
assert_eq!(
count,
1,
"m_both (matched by both FTS and ANN) must appear exactly once; \
bundle: {:?}",
bundle
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
}
#[cfg(feature = "embeddings")]
#[test]
fn embedding_mmr_collapses_paraphrases_but_jaccard_does_not() {
struct OracleEmbedder;
impl embeddings::Embedder for OracleEmbedder {
fn embed(&self, _text: &str) -> Result<Vec<f32>, embeddings::EmbedderError> {
let mut v = vec![0.0f32; 8];
v[0] = 1.0;
Ok(v)
}
fn model_id(&self) -> &str {
"oracle-d8"
}
fn dim(&self) -> usize {
8
}
}
let oracle = OracleEmbedder;
let weights = kimetsu_core::config::BrokerWeights::default();
let m_rg1_text = "prefer ripgrep for searching source code";
let m_rg2_text = "rg is the fastest way to locate patterns";
let conn = rusqlite::Connection::open_in_memory().expect("in-memory");
crate::schema::initialize(&conn).expect("init schema");
insert_memory_with_embedding(&conn, "m_rg1", m_rg1_text, &oracle);
insert_memory_with_embedding(&conn, "m_rg2", m_rg2_text, &oracle);
let bundle_embedding = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "search source patterns".to_string(),
budget_tokens: 20_000,
max_capsules: 1, ..Default::default()
},
&[],
&oracle,
)
.expect("retrieve with oracle embedder");
let emb_in_capsules = bundle_embedding
.capsules
.iter()
.filter(|c| {
c.expansion_handle == "memory:m_rg1" || c.expansion_handle == "memory:m_rg2"
})
.count();
assert_eq!(
emb_in_capsules,
1,
"embedding-MMR must collapse cosine=1.0 paraphrases: with max_capsules=1 \
only ONE should be included; capsule handles: {:?}; excluded: {:?}",
bundle_embedding
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>(),
bundle_embedding
.excluded
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
let emb_in_excluded = bundle_embedding
.excluded
.iter()
.filter(|c| {
c.expansion_handle == "memory:m_rg1" || c.expansion_handle == "memory:m_rg2"
})
.count();
assert_eq!(
emb_in_excluded,
1,
"the second near-duplicate must be in excluded under embedding-MMR; \
excluded handles: {:?}",
bundle_embedding
.excluded
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
let conn2 = rusqlite::Connection::open_in_memory().expect("in-memory 2");
crate::schema::initialize(&conn2).expect("init schema 2");
insert_memory_with_embedding(&conn2, "m_rg1", m_rg1_text, &oracle);
insert_memory_with_embedding(&conn2, "m_rg2", m_rg2_text, &oracle);
let bundle_lean = retrieve_context_with_embedder(
&conn2,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "search source patterns".to_string(),
budget_tokens: 20_000,
max_capsules: 2, ..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve with NoopEmbedder");
let lean_in_capsules = bundle_lean
.capsules
.iter()
.filter(|c| {
c.expansion_handle == "memory:m_rg1" || c.expansion_handle == "memory:m_rg2"
})
.count();
assert_eq!(
lean_in_capsules,
2,
"Jaccard-only path must NOT collapse the two paraphrases (different words, \
low token overlap → both survive MMR with max_capsules=2); capsule handles: {:?}",
bundle_lean
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
}
#[test]
fn content_tokens_strips_stopwords_keeps_topical_words() {
let got = content_tokens("Tell me about kimetsu, what's the idea of the repo");
assert_eq!(got, vec!["kimetsu", "idea", "repo"]);
}
#[test]
fn light_stem_strips_one_inflection_suffix() {
assert_eq!(light_stem("benchmarked"), "benchmark");
assert_eq!(light_stem("benchmarking"), "benchmark");
assert_eq!(light_stem("repos"), "repo");
assert_eq!(light_stem("does"), "does");
assert_eq!(light_stem("toml"), "toml");
}
#[test]
fn stemmed_query_matches_inflected_corpus_through_floor() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let insert = |id: &str, text: &str| {
let norm = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score, embedding, embedding_model
)
VALUES (?1, 'global_user', 'fact', ?2, ?3, 0.9, NULL, '{}',
'2026-06-01T00:00:00Z', 0, 0.0, NULL, NULL)",
rusqlite::params![id, text, norm],
)
.expect("insert memory");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, 'fact', 'global_user')",
rusqlite::params![id, text],
)
.expect("insert fts");
};
insert(
"m_bench",
"kimetsu benchmark runs go through the kbench binary and the Terminal-Bench driver",
);
insert(
"m_doctor",
"kimetsu doctor version-skew check parses process start times on Windows via CIM",
);
insert(
"m_gc",
"kimetsu runs auto-GC on run creation; keep the env guard at the trigger site",
);
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&kimetsu_core::config::BrokerWeights::default(),
ContextRequest {
stage: "localization".to_string(),
query: "Can you find out how kimetsu is benchmarked?".to_string(),
budget_tokens: 2000,
max_capsules: 2,
min_lexical_coverage: 0.5,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve");
let handles: Vec<_> = bundle
.capsules
.iter()
.map(|c| c.expansion_handle.as_str())
.collect();
assert!(
handles.contains(&"memory:m_bench"),
"stemmed 'benchmarked' must surface the benchmark memory; got {handles:?}"
);
assert!(
!handles.contains(&"memory:m_doctor") && !handles.contains(&"memory:m_gc"),
"off-topic memories sharing only 'kimetsu' must stay below the floor; got {handles:?}"
);
}
#[test]
fn weighted_coverage_ignores_zero_idf_tokens() {
let content = vec![
"kimetsu".to_string(),
"idea".to_string(),
"repo".to_string(),
];
let mut idf = HashMap::new();
idf.insert("kimetsu".to_string(), 0.0);
idf.insert("idea".to_string(), 1.386);
idf.insert("repo".to_string(), 0.693);
let cov = weighted_coverage(
&content,
&idf,
"global:fact - the git repo and kimetsu brain",
);
assert!((cov - 0.333).abs() < 0.01, "got {cov}");
let cov_topical =
weighted_coverage(&content, &idf, "global:fact - the core idea of kimetsu");
assert!(cov_topical > 0.6, "got {cov_topical}");
}
#[test]
fn escape_like_neutralizes_wildcards() {
assert_eq!(escape_like("a_b%c"), "a\\_b\\%c");
assert_eq!(escape_like("plain"), "plain");
}
#[test]
fn lexical_floor_drops_offtopic_memories_sharing_project_name() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let insert = |id: &str, text: &str| {
let norm = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score, embedding, embedding_model
)
VALUES (?1, 'global_user', 'fact', ?2, ?3, 0.9, NULL, '{}',
'2026-06-01T00:00:00Z', 0, 0.0, NULL, NULL)",
rusqlite::params![id, text, norm],
)
.expect("insert memory");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, 'fact', 'global_user')",
rusqlite::params![id, text],
)
.expect("insert fts");
};
insert(
"m1",
"When implementing a setup command that calls init_project, tests must call \
git_init_boundary before setup_cmd so ProjectPaths discover resolves to the temp \
dir instead of climbing to the real parent git repo including the user brain at kimetsu",
);
insert(
"m2",
"A member crate with default embeddings silently turned embeddings on for the entire \
cargo test workspace build graph because cargo unifies features; kimetsu-chat \
retrieval tests failed",
);
insert(
"m3",
"In toml 0.9 use toml from_str to parse a TOML document into a Value not str parse; \
implementing config get and set in kimetsu-cli",
);
let query = "Tell me about kimetsu, what's the idea of the repo".to_string();
let weights = kimetsu_core::config::BrokerWeights::default();
let handles = |bundle: &ContextBundle| {
bundle
.capsules
.iter()
.map(|c| c.expansion_handle.clone())
.collect::<Vec<_>>()
};
let no_floor = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: query.clone(),
budget_tokens: 2000,
max_capsules: 8,
min_lexical_coverage: 0.0,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve without floor");
let before = handles(&no_floor);
assert!(
before.contains(&"memory:m2".to_string()) && before.contains(&"memory:m3".to_string()),
"sanity: without the floor the pure-project-name memories should surface; got {before:?}"
);
let floored = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query,
budget_tokens: 2000,
max_capsules: 8,
min_lexical_coverage: 0.5,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve with floor");
let after = handles(&floored);
assert!(
!after.contains(&"memory:m2".to_string()) && !after.contains(&"memory:m3".to_string()),
"the lexical floor must drop memories whose only match is the corpus-ubiquitous \
project name; surviving: {after:?}"
);
}
#[test]
fn lexical_floor_keeps_ontopic_memory() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let insert = |id: &str, text: &str| {
let norm = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score, embedding, embedding_model
)
VALUES (?1, 'global_user', 'fact', ?2, ?3, 0.9, NULL, '{}',
'2026-06-01T00:00:00Z', 0, 0.0, NULL, NULL)",
rusqlite::params![id, text, norm],
)
.expect("insert memory");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, 'fact', 'global_user')",
rusqlite::params![id, text],
)
.expect("insert fts");
};
insert(
"d1",
"The distiller runs at session end and harvests durable lessons from the transcript",
);
insert(
"n1",
"Unrelated note about git rebase and squashing commits",
);
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&kimetsu_core::config::BrokerWeights::default(),
ContextRequest {
stage: "localization".to_string(),
query: "how does the distiller work".to_string(),
budget_tokens: 2000,
min_lexical_coverage: 0.5,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve");
assert!(
bundle
.capsules
.iter()
.any(|c| c.expansion_handle == "memory:d1"),
"on-topic memory covering the rare query word must survive the floor; got: {:?}",
bundle
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
}
#[cfg(feature = "embeddings")]
#[test]
fn min_semantic_score_floor_drops_off_topic_queries() {
struct DirectionalEmbedder {
marker: &'static str,
}
impl embeddings::Embedder for DirectionalEmbedder {
fn embed(&self, text: &str) -> Result<Vec<f32>, embeddings::EmbedderError> {
let mut v = vec![0.0f32; 8];
if text.contains(self.marker) {
v[0] = 1.0;
} else {
v[1] = 1.0;
}
Ok(v)
}
fn model_id(&self) -> &str {
"directional-d8"
}
fn dim(&self) -> usize {
8
}
}
let emb = DirectionalEmbedder { marker: "TOPIC_A" };
let conn = rusqlite::Connection::open_in_memory().expect("in-memory");
crate::schema::initialize(&conn).expect("init schema");
insert_memory_with_embedding(&conn, "m_b", "cookie recipe chocolate baking TOPIC_B", &emb);
let weights = kimetsu_core::config::BrokerWeights::default();
let bundle_off = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "TOPIC_A unrelated phosphorescent".to_string(),
budget_tokens: 4000,
min_semantic_score: 0.1, ..Default::default()
},
&[],
&emb,
)
.expect("retrieve off-topic");
assert!(
bundle_off.capsules.is_empty(),
"off-topic query (cosine=0 < floor=0.1) must produce zero capsules; \
got: {:?}",
bundle_off
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
let conn2 = rusqlite::Connection::open_in_memory().expect("in-memory 2");
crate::schema::initialize(&conn2).expect("init schema 2");
insert_memory_with_embedding(
&conn2,
"m_b2",
"cookie recipe chocolate TOPIC_B baking"
.to_string()
.as_str(),
&emb,
);
let bundle_on = retrieve_context_with_embedder(
&conn2,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "cookie chocolate TOPIC_B".to_string(),
budget_tokens: 4000,
min_semantic_score: 0.1,
..Default::default()
},
&[],
&emb,
)
.expect("retrieve on-topic");
assert!(
bundle_on
.capsules
.iter()
.any(|c| c.expansion_handle == "memory:m_b2"),
"on-topic query (cosine=1.0 ≥ floor) must surface m_b2; \
got capsules: {:?}",
bundle_on
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
let conn3 = rusqlite::Connection::open_in_memory().expect("in-memory 3");
crate::schema::initialize(&conn3).expect("init schema 3");
insert_memory_with_embedding(
&conn3,
"m_b3",
"cookie chocolate TOPIC_B recipe".to_string().as_str(),
&emb,
);
let bundle_noop_floor = retrieve_context_with_embedder(
&conn3,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "cookie chocolate TOPIC_A".to_string(),
budget_tokens: 4000,
min_semantic_score: 0.0, ..Default::default()
},
&[],
&emb,
)
.expect("retrieve noop floor");
assert!(
bundle_noop_floor
.capsules
.iter()
.any(|c| c.expansion_handle == "memory:m_b3"),
"with floor=0.0 (disabled), off-topic-cosine memory must still surface via FTS; \
got: {:?}",
bundle_noop_floor
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
}
#[cfg(feature = "embeddings")]
#[test]
fn d1f_token_economy_fewer_capsules_signal_preserved() {
struct OracleTopicEmbedder;
impl embeddings::Embedder for OracleTopicEmbedder {
fn embed(&self, text: &str) -> Result<Vec<f32>, embeddings::EmbedderError> {
let mut v = vec![0.0f32; 8];
if text.contains("TOPIC_A") {
v[0] = 1.0; } else {
v[1] = 1.0; }
Ok(v)
}
fn model_id(&self) -> &str {
"oracle-topic-d8"
}
fn dim(&self) -> usize {
8
}
}
let oracle = OracleTopicEmbedder;
let setup = |conn: &rusqlite::Connection| {
for (mid, text) in [
("m_dup1", "TOPIC_A prefer ripgrep for searching"),
("m_dup2", "TOPIC_A rg is the fastest searcher"),
("m_dup3", "TOPIC_A use rg tool to find patterns"),
(
"m_relevant",
"TOPIC_A critical lesson about search performance",
),
("m_noise1", "chocolate cookie baking TOPIC_B recipe"),
("m_noise2", "gardening tulip planting TOPIC_B spring"),
] {
insert_memory_with_embedding(conn, mid, text, &oracle);
}
};
let weights = kimetsu_core::config::BrokerWeights::default();
let conn_lean = rusqlite::Connection::open_in_memory().expect("in-memory lean");
crate::schema::initialize(&conn_lean).expect("init schema lean");
setup(&conn_lean);
let bundle_lean = retrieve_context_with_embedder(
&conn_lean,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "TOPIC_A search performance".to_string(),
budget_tokens: 20_000,
min_semantic_score: 0.0, ..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve lean");
let lean_count = bundle_lean
.capsules
.iter()
.filter(|c| c.expansion_handle.starts_with("memory:"))
.count();
let conn_emb = rusqlite::Connection::open_in_memory().expect("in-memory emb");
crate::schema::initialize(&conn_emb).expect("init schema emb");
setup(&conn_emb);
let bundle_emb = retrieve_context_with_embedder(
&conn_emb,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "TOPIC_A search performance".to_string(),
budget_tokens: 20_000,
min_semantic_score: 0.5, ..Default::default()
},
&[],
&oracle,
)
.expect("retrieve with embeddings");
let emb_count = bundle_emb
.capsules
.iter()
.filter(|c| c.expansion_handle.starts_with("memory:"))
.count();
assert!(
emb_count < lean_count,
"D1e must reduce capsule count: embedding path {emb_count} must be \
< lean path {lean_count}. Embedding capsules: {:?}",
bundle_emb
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
assert!(
bundle_emb
.capsules
.iter()
.any(|c| c.expansion_handle == "memory:m_relevant"),
"m_relevant must survive D1e selection (signal preserved); \
embedding capsules: {:?}",
bundle_emb
.capsules
.iter()
.map(|c| &c.expansion_handle)
.collect::<Vec<_>>()
);
let lean_tokens: u32 = bundle_lean.capsules.iter().map(|c| c.token_estimate).sum();
let emb_tokens: u32 = bundle_emb.capsules.iter().map(|c| c.token_estimate).sum();
assert!(
emb_tokens < lean_tokens,
"D1e must reduce token usage: emb={emb_tokens} must be < lean={lean_tokens}"
);
}
#[test]
fn lean_noop_embedder_uses_fts_then_recency_unchanged() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
for (mid, text) in [
("m_x", "use git rebase to clean history"),
("m_y", "grep finds text quickly"),
] {
let normalized = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score
)
VALUES (?1, 'global_user', 'fact', ?2, ?3, 1.0, NULL, '{}',
'2026-01-01T00:00:00Z', 0, 0.0)",
rusqlite::params![mid, text, normalized],
)
.expect("insert");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope) VALUES (?1, ?2, 'fact', 'global_user')",
rusqlite::params![mid, text],
)
.expect("insert fts");
}
let weights = kimetsu_core::config::BrokerWeights::default();
let bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: "grep text".to_string(),
budget_tokens: 4000,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("retrieve with NoopEmbedder must not panic");
let handles: Vec<&str> = bundle
.capsules
.iter()
.filter_map(|c| c.expansion_handle.strip_prefix("memory:"))
.collect();
assert!(
handles.contains(&"m_y"),
"m_y must surface via FTS on lean path; got {handles:?}"
);
}
#[test]
fn classify_task_maps_each_kind_deterministically() {
assert_eq!(
classify_task("fix the panic in the parser"),
TaskKind::Debug,
"contains 'fix' and 'panic'"
);
assert_eq!(
classify_task("there is a crash in auth when calling login"),
TaskKind::Debug,
"contains 'crash'"
);
assert_eq!(
classify_task("debug the failing test"),
TaskKind::Debug,
"contains 'debug' and 'fail'"
);
assert_eq!(
classify_task("investigate why retrieval is slow"),
TaskKind::Investigation,
"contains 'investigate' and 'why'"
);
assert_eq!(
classify_task("analyze the root cause of the latency"),
TaskKind::Investigation,
"contains 'analyze' and 'root cause'"
);
assert_eq!(
classify_task("refactor the auth module"),
TaskKind::Refactor,
"contains 'refactor'"
);
assert_eq!(
classify_task("rename the config struct"),
TaskKind::Refactor,
"contains 'rename'"
);
assert_eq!(
classify_task("simplify the retry handling logic"),
TaskKind::Refactor,
"contains 'simplify'"
);
assert_eq!(
classify_task("document the API endpoints"),
TaskKind::Docs,
"contains 'document'"
);
assert_eq!(
classify_task("update the readme with new instructions"),
TaskKind::Docs,
"contains 'readme'"
);
assert_eq!(
classify_task("add a docstring to the main function"),
TaskKind::Docs,
"contains 'docstring'"
);
assert_eq!(
classify_task("add a dark mode toggle"),
TaskKind::Feature,
"no debug/refactor/docs/investigate keyword"
);
assert_eq!(
classify_task("implement the new caching layer"),
TaskKind::Feature,
"no debug/refactor/docs/investigate keyword"
);
assert_eq!(
classify_task("build the export pipeline"),
TaskKind::Feature,
"no debug/refactor/docs/investigate keyword"
);
}
#[test]
fn classify_task_respects_precedence_order() {
assert_eq!(
classify_task("fix and refactor the login module"),
TaskKind::Debug,
"Debug > Refactor"
);
assert_eq!(
classify_task("investigate and refactor the cache layer"),
TaskKind::Investigation,
"Investigation > Refactor"
);
assert_eq!(
classify_task("investigate the docs and document the API"),
TaskKind::Investigation,
"Investigation > Docs"
);
assert_eq!(
classify_task("refactor and add docs"),
TaskKind::Refactor,
"Refactor > Docs"
);
assert_eq!(
classify_task("fix the bug and investigate the regression"),
TaskKind::Debug,
"Debug > Investigation"
);
}
#[test]
fn weights_for_task_kind_renormalizes_to_unit_sum() {
let base = StageWeights {
relevance: 0.50,
confidence: 0.20,
freshness: 0.20,
scope: 0.10,
};
let original_sum = base.relevance + base.confidence + base.freshness + base.scope;
for kind in [
TaskKind::Debug,
TaskKind::Refactor,
TaskKind::Investigation,
TaskKind::Docs,
] {
let w = weights_for_task_kind(base.clone(), kind);
let new_sum = w.relevance + w.confidence + w.freshness + w.scope;
assert!(
(new_sum - original_sum).abs() < 1e-4,
"weights_for_task_kind({kind:?}) sum {new_sum} differs from {original_sum}"
);
}
}
#[test]
fn weights_for_task_kind_feature_is_unchanged() {
let base = StageWeights {
relevance: 0.40,
confidence: 0.30,
freshness: 0.20,
scope: 0.10,
};
let w = weights_for_task_kind(base.clone(), TaskKind::Feature);
assert!((w.relevance - base.relevance).abs() < f32::EPSILON);
assert!((w.confidence - base.confidence).abs() < f32::EPSILON);
assert!((w.freshness - base.freshness).abs() < f32::EPSILON);
assert!((w.scope - base.scope).abs() < f32::EPSILON);
}
#[test]
fn weights_for_task_kind_debug_up_freshness_fraction() {
let base = StageWeights {
relevance: 0.50,
confidence: 0.20,
freshness: 0.20,
scope: 0.10,
};
let debug_w = weights_for_task_kind(base.clone(), TaskKind::Debug);
assert!(
debug_w.freshness > base.freshness,
"Debug must increase freshness fraction: {debug_w:?}"
);
}
#[test]
fn weights_for_task_kind_refactor_up_scope_fraction() {
let base = StageWeights {
relevance: 0.50,
confidence: 0.20,
freshness: 0.20,
scope: 0.10,
};
let refactor_w = weights_for_task_kind(base.clone(), TaskKind::Refactor);
assert!(
refactor_w.scope > base.scope,
"Refactor must increase scope fraction: {refactor_w:?}"
);
}
#[test]
fn task_kind_feature_is_retrieval_neutral() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
for (mid, db_kind, text) in [
("m1", "failure_pattern", "linker not found error in build"),
("m2", "convention", "use snake_case for all identifiers"),
("m3", "fact", "the cache is invalidated on every deploy"),
] {
let normalized = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score
)
VALUES (?1, 'project', ?2, ?3, ?4, 1.0, NULL, '{}',
'2026-01-01T00:00:00Z', 0, 0.0)",
rusqlite::params![mid, db_kind, text, normalized],
)
.expect("insert memory");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, ?3, 'project')",
rusqlite::params![mid, text, db_kind],
)
.expect("insert fts");
}
let weights = kimetsu_core::config::BrokerWeights::default();
let query = "cache convention failure".to_string();
let baseline = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: query.clone(),
budget_tokens: 4000,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("baseline retrieve");
let feature = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: query.clone(),
budget_tokens: 4000,
task_kind: TaskKind::Feature,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("feature retrieve");
let baseline_ids: Vec<&str> = baseline
.capsules
.iter()
.map(|c| c.expansion_handle.as_str())
.collect();
let feature_ids: Vec<&str> = feature
.capsules
.iter()
.map(|c| c.expansion_handle.as_str())
.collect();
assert_eq!(
baseline_ids, feature_ids,
"task_kind=Feature must produce identical retrieval to default; \
baseline={baseline_ids:?} feature={feature_ids:?}"
);
let baseline_scores: Vec<f32> = baseline.capsules.iter().map(|c| c.score).collect();
let feature_scores: Vec<f32> = feature.capsules.iter().map(|c| c.score).collect();
for (b, f) in baseline_scores.iter().zip(feature_scores.iter()) {
assert!(
(b - f).abs() < 1e-5,
"scores must be identical: baseline={b} feature={f}"
);
}
}
#[test]
fn debug_surfaces_more_failure_pattern_than_docs() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
for (i, text) in [
"auth token expired causes login failure",
"auth service crash on null pointer",
"auth regression after upgrade breaks sessions",
"auth error when certificate is invalid",
]
.iter()
.enumerate()
{
let mid = format!("mfp{i}");
let normalized = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score
)
VALUES (?1, 'project', 'failure_pattern', ?2, ?3, 1.0, NULL, '{}',
'2026-01-01T00:00:00Z', 0, 0.0)",
rusqlite::params![mid, text, normalized],
)
.expect("insert failure_pattern");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, 'failure_pattern', 'project')",
rusqlite::params![mid, text],
)
.expect("insert fts");
}
for (i, (db_kind, text)) in [
("convention", "auth module uses bearer tokens by convention"),
("convention", "auth scopes are documented in the API guide"),
("fact", "auth service runs on port 8443 in production"),
("fact", "auth uses JWT with RS256 signing for all tokens"),
]
.iter()
.enumerate()
{
let mid = format!("mconv{i}");
let normalized = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score
)
VALUES (?1, 'project', ?2, ?3, ?4, 1.0, NULL, '{}',
'2026-01-01T00:00:00Z', 0, 0.0)",
rusqlite::params![mid, db_kind, text, normalized],
)
.expect("insert convention/fact");
conn.execute(
"INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, ?3, 'project')",
rusqlite::params![mid, text, db_kind],
)
.expect("insert fts");
}
let weights = kimetsu_core::config::BrokerWeights::default();
let query = "auth token failure".to_string();
let debug_bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: query.clone(),
budget_tokens: 4000,
max_capsules: 4,
task_kind: TaskKind::Debug,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("debug retrieve");
let docs_bundle = retrieve_context_with_embedder(
&conn,
"/fake-repo",
&weights,
ContextRequest {
stage: "localization".to_string(),
query: query.clone(),
budget_tokens: 4000,
max_capsules: 4,
task_kind: TaskKind::Docs,
..Default::default()
},
&[],
&embeddings::NoopEmbedder,
)
.expect("docs retrieve");
let count_failure_pattern = |bundle: &ContextBundle| -> usize {
bundle
.capsules
.iter()
.filter(|c| capsule_matches_kind(c, "failure_pattern"))
.count()
};
let debug_fp = count_failure_pattern(&debug_bundle);
let docs_fp = count_failure_pattern(&docs_bundle);
assert!(
debug_fp > docs_fp,
"Debug must surface strictly more failure_pattern capsules than Docs: \
debug_fp={debug_fp} docs_fp={docs_fp}\n\
Debug capsules: {:?}\n\
Docs capsules: {:?}",
debug_bundle
.capsules
.iter()
.map(|c| format!("{}:{}", c.kind, &c.summary[..c.summary.len().min(60)]))
.collect::<Vec<_>>(),
docs_bundle
.capsules
.iter()
.map(|c| format!("{}:{}", c.kind, &c.summary[..c.summary.len().min(60)]))
.collect::<Vec<_>>(),
);
}
fn init_db_with_memory(memory_id: &str, text: &str) -> rusqlite::Connection {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
crate::schema::initialize(&conn).expect("init schema");
let normalized = kimetsu_core::memory::normalize_memory_text(text);
conn.execute(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text, confidence,
source_event_id, provenance_snapshot_json, created_at,
use_count, usefulness_score
)
VALUES (?1, 'project', 'fact', ?2, ?3, 1.0, NULL, '{}',
'2026-01-01T00:00:00Z', 0, 0.0)",
rusqlite::params![memory_id, text, normalized],
)
.expect("insert memory");
conn
}
#[test]
fn resolve_capsule_memory_returns_full_text() {
let conn = init_db_with_memory("test-mem-id", "Use rg over grep for speed");
let repo_root = std::path::Path::new("/fake-repo");
let result =
resolve_capsule(&conn, repo_root, "memory:test-mem-id").expect("should resolve");
assert_eq!(result, "Use rg over grep for speed");
}
#[test]
fn resolve_capsule_memory_missing_id_returns_err() {
let conn = init_db_with_memory("real-id", "some text");
let repo_root = std::path::Path::new("/fake-repo");
let err = resolve_capsule(&conn, repo_root, "memory:nonexistent-id")
.expect_err("should error for missing memory");
assert!(
err.to_string().contains("no active memory"),
"error message should mention missing: {err}"
);
}
#[test]
fn resolve_capsule_file_returns_bounded_content() {
let dir = make_test_dir("f2_file_resolve");
let content = "hello from the file\n";
std::fs::write(dir.join("notes.txt"), content).expect("write");
let result = resolve_capsule(
&rusqlite::Connection::open_in_memory().expect("open"),
&dir,
"file:notes.txt",
)
.expect("should resolve file");
assert!(result.contains("hello from the file"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn resolve_capsule_file_caps_large_file() {
let dir = make_test_dir("f2_file_cap");
let big = "A".repeat(FILE_EXPAND_CAP_BYTES * 3);
std::fs::write(dir.join("big.txt"), &big).expect("write");
let result = resolve_capsule(
&rusqlite::Connection::open_in_memory().expect("open"),
&dir,
"file:big.txt",
)
.expect("should resolve large file");
assert!(
result.len() <= FILE_EXPAND_CAP_BYTES + 200,
"result should be bounded: got {} bytes",
result.len()
);
assert!(
result.contains("truncated"),
"truncation marker should be present"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn resolve_capsule_unknown_handle_returns_err() {
let conn = rusqlite::Connection::open_in_memory().expect("open");
let err = resolve_capsule(&conn, std::path::Path::new("/r"), "blob:abc123")
.expect_err("should error");
assert!(
err.to_string().contains("unrecognised handle"),
"got: {err}"
);
}
#[test]
fn resolve_capsule_malformed_handle_returns_err() {
let conn = rusqlite::Connection::open_in_memory().expect("open");
let err = resolve_capsule(&conn, std::path::Path::new("/r"), "justnocolon")
.expect_err("should error");
assert!(
err.to_string().contains("unrecognised handle"),
"got: {err}"
);
}
#[test]
fn resolve_capsule_run_handle_returns_deferred_err() {
let conn = rusqlite::Connection::open_in_memory().expect("open");
let err = resolve_capsule(&conn, std::path::Path::new("/r"), "run:some-run-id")
.expect_err("run: should be deferred err");
assert!(err.to_string().contains("not yet supported"), "got: {err}");
}
#[test]
fn resolve_capsule_file_rejects_absolute_path() {
let conn = rusqlite::Connection::open_in_memory().expect("open");
let err = resolve_capsule(&conn, std::path::Path::new("/r"), "file:/etc/passwd")
.expect_err("should reject absolute path");
assert!(err.to_string().contains("absolute path"), "got: {err}");
}
fn make_capsule(summary: &str, score: f32) -> ContextCapsule {
ContextCapsule {
id: new_id().to_string(),
kind: "memory".to_string(),
summary: summary.to_string(),
token_estimate: 10,
expansion_handle: format!("memory:{}", new_id()),
provenance: vec![],
confidence: 1.0,
freshness: 1.0,
relevance: 1.0,
scope_weight: 1.0,
score,
}
}
#[test]
fn rerank_capsules_reorders_by_query_overlap() {
use crate::embeddings::StubReranker;
let query = "rust async tokio";
let high_overlap = make_capsule("rust async tokio runtime", 0.0);
let low_overlap = make_capsule("python django framework", 0.0);
let capsules = vec![low_overlap.clone(), high_overlap.clone()];
let ranked = rerank_capsules(query, capsules, &StubReranker, 0.0, 0);
assert_eq!(ranked.len(), 2, "both capsules should survive (floor=0)");
assert!(
ranked[0].summary.contains("rust"),
"rust capsule must be first, got: {:?}",
ranked[0].summary
);
assert!(
ranked[0].score > 0.05,
"score must be overwritten by reranker: {}",
ranked[0].score
);
assert!(
ranked[0].score > ranked[1].score,
"high overlap must score higher: {} vs {}",
ranked[0].score,
ranked[1].score
);
}
#[test]
fn rerank_capsules_floor_drops_zero_overlap() {
use crate::embeddings::StubReranker;
let query = "rust async tokio";
let high = make_capsule("rust async tokio runtime", 0.0);
let zero = make_capsule("completely unrelated document xyz", 0.0);
let capsules = vec![high, zero];
let ranked = rerank_capsules(query, capsules, &StubReranker, 0.3, 0);
assert_eq!(ranked.len(), 1, "zero-overlap capsule must be dropped");
assert!(
ranked[0].summary.contains("rust"),
"only rust capsule should survive"
);
}
#[test]
fn rerank_capsules_cap_truncates() {
use crate::embeddings::StubReranker;
let query = "alpha beta gamma";
let capsules = vec![
make_capsule("alpha beta gamma delta", 0.0),
make_capsule("alpha beta", 0.0),
make_capsule("alpha", 0.0),
make_capsule("unrelated xyz", 0.0),
];
let ranked = rerank_capsules(query, capsules, &StubReranker, 0.0, 2);
assert_eq!(ranked.len(), 2, "cap=2 must truncate to 2 results");
assert!(
ranked[0].score >= ranked[1].score,
"results must be sorted descending"
);
}
#[test]
fn rerank_capsules_fail_open_preserves_input_order() {
struct FailingReranker;
impl crate::embeddings::Reranker for FailingReranker {
fn rerank(
&self,
_query: &str,
_docs: &[&str],
) -> Result<Vec<f32>, crate::embeddings::EmbedderError> {
Err(crate::embeddings::EmbedderError::EmbedFailed(
"simulated failure".into(),
))
}
fn model_id(&self) -> &str {
"fail-reranker"
}
}
let query = "anything";
let c1 = make_capsule("first capsule", 0.9);
let c2 = make_capsule("second capsule", 0.5);
let c3 = make_capsule("third capsule", 0.1);
let capsules = vec![c1.clone(), c2.clone(), c3.clone()];
let out = rerank_capsules(query, capsules, &FailingReranker, 0.0, 0);
assert_eq!(out.len(), 3, "all capsules must be returned on error");
assert_eq!(out[0].summary, c1.summary, "order must be preserved");
assert_eq!(out[1].summary, c2.summary, "order must be preserved");
assert_eq!(out[2].summary, c3.summary, "order must be preserved");
}
#[test]
fn rerank_capsules_empty_input_returns_empty() {
use crate::embeddings::StubReranker;
let out = rerank_capsules("query", vec![], &StubReranker, 0.0, 0);
assert!(out.is_empty());
}
}