From 3d77e60a02d8286b45829e505291a8892570d3be Mon Sep 17 00:00:00 2001 From: Riz Ashraf Date: Wed, 7 Oct 2026 17:04:52 +0100 Subject: [PATCH] feat(embedding,notes): implement ADR-0056 Blake3 stable hashing and ADR-0057 priority-aware retention --- server/src/embedding.rs | 22 +++++++++++++++------- server/src/handlers/meta.rs | 17 +++++++++++++++-- server/src/handlers/notes.rs | 9 ++++++++- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/server/src/embedding.rs b/server/src/embedding.rs index 7994b9a..317cc1a 100644 --- a/server/src/embedding.rs +++ b/server/src/embedding.rs @@ -152,7 +152,6 @@ fn fallback_embed(text: &str) -> Vec { return vec; } - use std::hash::{Hash, Hasher}; for word in words { let clean: String = word .chars() @@ -163,18 +162,17 @@ fn fallback_embed(text: &str) -> Vec { continue; } - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - clean.hash(&mut hasher); - let h = hasher.finish(); + let hash_bytes = blake3::hash(clean.as_bytes()); + let h = u64::from_le_bytes(hash_bytes.as_bytes()[0..8].try_into().unwrap()); let idx = (h as usize) % DIM; let sign = if (h >> 32) & 1 == 0 { 1.0f32 } else { -1.0f32 }; vec[idx] += sign; let chars: Vec = clean.chars().collect(); for window in chars.windows(3) { - let mut h2 = std::collections::hash_map::DefaultHasher::new(); - window.hash(&mut h2); - let hv = h2.finish(); + let window_str: String = window.iter().collect(); + let h2_bytes = blake3::hash(window_str.as_bytes()); + let hv = u64::from_le_bytes(h2_bytes.as_bytes()[0..8].try_into().unwrap()); let idx2 = (hv as usize) % DIM; let s2 = if (hv >> 32) & 1 == 0 { 0.5f32 } else { -0.5f32 }; vec[idx2] += s2; @@ -378,4 +376,14 @@ mod tests { assert_eq!(embedding.len(), 384); } } + + #[test] + fn test_fallback_embed_deterministic_stability() { + let text = "The quick brown fox jumps over the lazy dog"; + let emb1 = fallback_embed(text); + let emb2 = fallback_embed(text); + assert_eq!(emb1.len(), 384); + assert_eq!(emb1, emb2); + assert!((cosine_similarity(&emb1, &emb2) - 1.0).abs() < 1e-5); + } } diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs index 2ce8aa2..8dad604 100644 --- a/server/src/handlers/meta.rs +++ b/server/src/handlers/meta.rs @@ -665,7 +665,20 @@ impl McpTool for TechDebtHandler { effort_estimate: req.effort_estimate, }); if debts.len() > 300 { - debts.remove(0); + let severity_rank = |sev: Option<&str>| match sev.unwrap_or("").to_lowercase().as_str() { + "critical" => 4, + "high" => 3, + "medium" => 2, + "low" => 1, + _ => 1, + }; + if let Some((idx_to_remove, _)) = debts.iter().enumerate().min_by_key(|(_, d)| { + let status_score = if d.is_resolved { 0 } else { 10 }; + let sev_score = severity_rank(d.severity.as_deref()); + (status_score + sev_score, d.created_at) + }) { + debts.remove(idx_to_remove); + } } }); Ok("Tech debt logged".to_string()) @@ -2827,4 +2840,4 @@ mod tests { .unwrap(); assert!(pre_res.contains("active_tasks")); } -} + diff --git a/server/src/handlers/notes.rs b/server/src/handlers/notes.rs index 6ec454d..1c89eef 100644 --- a/server/src/handlers/notes.rs +++ b/server/src/handlers/notes.rs @@ -108,7 +108,14 @@ impl McpTool for AddSessionSummaryHandler { next_steps: req.next_steps.unwrap_or_default(), }); if summaries.len() > 200 { - summaries.remove(0); + let now = crate::handlers::utils::now_secs(); + if let Some((idx_to_remove, _)) = summaries.iter().enumerate().min_by_key(|(_, s)| { + let is_expired = s.expires_at.map(|exp| exp < now).unwrap_or(false); + let status_score = if is_expired { 0 } else { 1 }; + (status_score, s.timestamp) + }) { + summaries.remove(idx_to_remove); + } } }); Ok("Session summary added".to_string())