Skip to main content

hanzo_mcp/brain/
algorithms.rs

1//! Pure-CPU algorithm port for the Hanzo MCP Rust runtime.
2//!
3//! Mirrors `@hanzo/bot-memory` (TS), `hanzo_memory.algorithms` (Python),
4//! `bot-go/pkg/brain` (Go), and `bot-cpp/include/hanzo/brain/algorithms.hpp`
5//! (C++). A `~/.hanzo/brain/brain.db` written by any runtime is read by
6//! every other without translation.
7//!
8//! The node-side canonical home lives in
9//! `hanzoai/node/hanzo-libs/hanzo-brain/src/algorithms.rs`; this file is
10//! the byte-equivalent mirror for the standalone MCP server.
11
12use std::collections::{HashMap, HashSet};
13
14use super::store::SearchHit;
15
16// ── Fusion ────────────────────────────────────────────────────────────
17
18/// BEIR-tuned default RRF k (Elasticsearch 2024 grid search).
19pub const RRF_K_DEFAULT: f64 = 20.0;
20
21/// Reciprocal Rank Fusion (Cormack et al. 2009). Normalizes the top result to ~1.
22pub fn rrf_fuse(lists: Vec<Vec<SearchHit>>, limit: usize, k: f64) -> Vec<SearchHit> {
23    let k = if k == 0.0 { RRF_K_DEFAULT } else { k };
24    let mut scores: HashMap<String, f64> = HashMap::new();
25    let mut meta: HashMap<String, SearchHit> = HashMap::new();
26    let num = lists.len() as f64;
27    for list in lists {
28        for (rank, hit) in list.into_iter().enumerate() {
29            *scores.entry(hit.slug.clone()).or_insert(0.0) += 1.0 / (k + rank as f64 + 1.0);
30            meta.entry(hit.slug.clone()).or_insert(hit);
31        }
32    }
33    if scores.is_empty() {
34        return vec![];
35    }
36    let max = num / (k + 1.0);
37    let mut out: Vec<SearchHit> = scores
38        .into_iter()
39        .map(|(slug, s)| {
40            let m = meta.remove(&slug).unwrap();
41            let norm = if max > 0.0 { (s / max).min(1.0) } else { 0.0 };
42            SearchHit { slug, score: norm, excerpt: m.excerpt, source: "fused".into() }
43        })
44        .collect();
45    out.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
46    if out.len() > limit {
47        out.truncate(limit);
48    }
49    out
50}
51
52/// Relative Score Fusion (Weaviate v1.24). Preserves score magnitude.
53pub fn rsf_fuse(lists: Vec<Vec<SearchHit>>, limit: usize, weights: Option<Vec<f64>>) -> Vec<SearchHit> {
54    let n = lists.len();
55    let w = weights.unwrap_or_else(|| vec![if n == 0 { 0.0 } else { 1.0 / n as f64 }; n]);
56    if w.len() != n {
57        // gracefully fall back to uniform
58        return rsf_fuse(lists, limit, Some(vec![if n == 0 { 0.0 } else { 1.0 / n as f64 }; n]));
59    }
60    let mut scores: HashMap<String, f64> = HashMap::new();
61    let mut meta: HashMap<String, SearchHit> = HashMap::new();
62    for (i, list) in lists.into_iter().enumerate() {
63        if list.is_empty() {
64            continue;
65        }
66        let mut lo = f64::INFINITY;
67        let mut hi = f64::NEG_INFINITY;
68        for h in &list {
69            if h.score < lo { lo = h.score; }
70            if h.score > hi { hi = h.score; }
71        }
72        let span = hi - lo;
73        for h in list {
74            let norm = if span > 0.0 { (h.score - lo) / span } else { 1.0 };
75            *scores.entry(h.slug.clone()).or_insert(0.0) += w[i] * norm;
76            meta.entry(h.slug.clone()).or_insert(h);
77        }
78    }
79    let mut out: Vec<SearchHit> = scores
80        .into_iter()
81        .map(|(slug, s)| {
82            let m = meta.remove(&slug).unwrap();
83            SearchHit { slug, score: s, excerpt: m.excerpt, source: "fused".into() }
84        })
85        .collect();
86    out.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
87    if out.len() > limit {
88        out.truncate(limit);
89    }
90    out
91}
92
93/// Query characteristics used to pick adaptive RRF k / weights.
94#[derive(Debug, Clone, Copy)]
95pub struct QueryCharacteristics {
96    pub token_count: usize,
97    pub is_phrase: bool,
98    pub is_boolean: bool,
99}
100
101/// Cheap query characterizer; no external NLP.
102pub fn characterize(query: &str) -> QueryCharacteristics {
103    let t = query.trim();
104    let is_phrase = (t.starts_with('"') && t.ends_with('"') && t.len() > 2)
105        || (t.starts_with('\'') && t.ends_with('\'') && t.len() > 2);
106    let mut is_boolean = false;
107    for kw in ["AND", "OR", "NOT"] {
108        // word-boundary check
109        for m in t.match_indices(kw) {
110            let before_ok = m.0 == 0 || !t.as_bytes()[m.0 - 1].is_ascii_alphanumeric();
111            let after_idx = m.0 + kw.len();
112            let after_ok = after_idx == t.len()
113                || !t.as_bytes()[after_idx].is_ascii_alphanumeric();
114            if before_ok && after_ok {
115                is_boolean = true;
116                break;
117            }
118        }
119        if is_boolean { break; }
120    }
121    if !is_boolean {
122        // detect "-term" negation
123        let mut prev_ws = false;
124        for c in t.chars() {
125            if c == '-' && prev_ws { is_boolean = true; break; }
126            prev_ws = c.is_whitespace();
127        }
128    }
129    let token_count = t.split_whitespace().count();
130    QueryCharacteristics { token_count, is_phrase, is_boolean }
131}
132
133/// Pick an adaptive k value for RRF based on query shape.
134pub fn select_rrf_k(q: QueryCharacteristics) -> i32 {
135    if q.is_phrase { return 10; }
136    if q.is_boolean { return 15; }
137    if q.token_count <= 2 { return 15; }
138    if q.token_count >= 10 { return 40; }
139    20
140}
141
142/// FTS / semantic weights for fusion, always summing to 1.0.
143#[derive(Debug, Clone, Copy)]
144pub struct FusionWeights { pub fts: f64, pub semantic: f64 }
145
146/// Pick adaptive FTS / semantic weights from query shape.
147pub fn select_weights(q: QueryCharacteristics) -> FusionWeights {
148    if q.is_phrase   { return FusionWeights { fts: 0.8, semantic: 0.2 }; }
149    if q.is_boolean  { return FusionWeights { fts: 0.7, semantic: 0.3 }; }
150    if q.token_count <= 2  { return FusionWeights { fts: 0.65, semantic: 0.35 }; }
151    if q.token_count >= 10 { return FusionWeights { fts: 0.3, semantic: 0.7 }; }
152    FusionWeights { fts: 0.5, semantic: 0.5 }
153}
154
155// ── Rerank (MMR) ─────────────────────────────────────────────────────
156
157/// Cosine similarity. Returns 0 on dimension mismatch or zero norm.
158pub fn cosine(a: &[f64], b: &[f64]) -> f64 {
159    if a.len() != b.len() { return 0.0; }
160    let mut dot = 0.0;
161    let mut na = 0.0;
162    let mut nb = 0.0;
163    for i in 0..a.len() {
164        dot += a[i] * b[i];
165        na += a[i] * a[i];
166        nb += b[i] * b[i];
167    }
168    let d = na.sqrt() * nb.sqrt();
169    if d == 0.0 { 0.0 } else { dot / d }
170}
171
172/// MMR input: a hit with an optional embedding for diversity scoring.
173#[derive(Debug, Clone)]
174pub struct MmrInput {
175    pub hit: SearchHit,
176    pub embedding: Option<Vec<f64>>,
177}
178
179/// Greedy MMR rerank (Carbonell & Goldstein 1998).
180pub fn mmr_rerank(hits: Vec<MmrInput>, lambda: f64, limit: usize) -> Vec<MmrInput> {
181    let limit = if limit == 0 { hits.len() } else { limit };
182    let (mut embedded, orphans): (Vec<_>, Vec<_>) =
183        hits.into_iter().partition(|h| h.embedding.as_ref().map(|e| !e.is_empty()).unwrap_or(false));
184    let mut selected: Vec<MmrInput> = Vec::with_capacity(limit);
185    while selected.len() < limit && !embedded.is_empty() {
186        let mut best_idx: Option<usize> = None;
187        let mut best_score = f64::NEG_INFINITY;
188        for (i, c) in embedded.iter().enumerate() {
189            let rel = c.hit.score;
190            let mut max_sim = 0.0_f64;
191            for s in &selected {
192                let sim = cosine(
193                    c.embedding.as_ref().unwrap(),
194                    s.embedding.as_ref().unwrap(),
195                );
196                if sim > max_sim { max_sim = sim; }
197            }
198            let mmr = lambda * rel - (1.0 - lambda) * max_sim;
199            if mmr > best_score {
200                best_score = mmr;
201                best_idx = Some(i);
202            }
203        }
204        match best_idx {
205            Some(i) => selected.push(embedded.remove(i)),
206            None => break,
207        }
208    }
209    for o in orphans {
210        if selected.len() >= limit { break; }
211        selected.push(o);
212    }
213    selected
214}
215
216// ── Dedup ────────────────────────────────────────────────────────────
217
218fn chain_of(slug: &str) -> String {
219    let mut s = slug.to_string();
220    // strip "#chunk-N"
221    if let Some(idx) = s.rfind("#chunk-") {
222        if s[idx + 7..].chars().all(|c| c.is_ascii_digit()) {
223            s.truncate(idx);
224        }
225    }
226    // strip "::N"
227    if let Some(idx) = s.rfind("::") {
228        if s[idx + 2..].chars().all(|c| c.is_ascii_digit()) {
229            s.truncate(idx);
230        }
231    }
232    s
233}
234
235/// Keep the top-N best-scored hits per chain.
236pub fn dedup_hits(hits: Vec<SearchHit>, per_chain: usize) -> Vec<SearchHit> {
237    let per_chain = if per_chain == 0 { 1 } else { per_chain };
238    let mut buckets: HashMap<String, Vec<SearchHit>> = HashMap::new();
239    for h in hits {
240        buckets.entry(chain_of(&h.slug)).or_default().push(h);
241    }
242    let mut out = Vec::new();
243    for mut lst in buckets.into_values() {
244        lst.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
245        out.extend(lst.into_iter().take(per_chain));
246    }
247    out.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
248    out
249}
250
251// ── Script detection ─────────────────────────────────────────────────
252
253fn is_cjk(cp: u32) -> bool {
254    (0x4E00..=0x9FFF).contains(&cp)
255        || (0x3400..=0x4DBF).contains(&cp)
256        || (0x3040..=0x30FF).contains(&cp)
257        || (0xAC00..=0xD7AF).contains(&cp)
258}
259
260fn is_emoji(cp: u32) -> bool {
261    (0x2600..=0x27BF).contains(&cp) || (0x1F300..=0x1FAFF).contains(&cp)
262}
263
264/// True when any CJK codepoint is present.
265pub fn has_cjk(s: &str) -> bool { s.chars().any(|c| is_cjk(c as u32)) }
266
267/// True when any emoji codepoint is present.
268pub fn has_emoji(s: &str) -> bool { s.chars().any(|c| is_emoji(c as u32)) }
269
270/// Detected script counts + fractions + primary.
271#[derive(Debug, Clone)]
272pub struct ScriptReport {
273    pub primary: String,
274    pub fractions: HashMap<String, f64>,
275    pub has_cjk: bool,
276    pub has_emoji: bool,
277}
278
279/// Classify a string's primary script.
280pub fn detect_script(s: &str) -> ScriptReport {
281    let keys = ["latin", "cjk", "emoji", "cyrillic", "arabic", "hebrew", "greek", "devanagari", "other"];
282    let mut counts: HashMap<String, usize> = keys.iter().map(|k| (k.to_string(), 0)).collect();
283    let mut total = 0usize;
284    for c in s.chars() {
285        let cp = c as u32;
286        if (0x0030..=0x0039).contains(&cp) { continue; }
287        if let Some(k) = classify_cp(cp) {
288            *counts.entry(k.to_string()).or_insert(0) += 1;
289            total += 1;
290        }
291    }
292    let mut primary = "other".to_string();
293    let mut max = 0usize;
294    for &k in &keys {
295        if counts[k] > max {
296            max = counts[k];
297            primary = k.to_string();
298        }
299    }
300    let mut fractions: HashMap<String, f64> = HashMap::new();
301    for &k in &keys {
302        let v = counts[k];
303        fractions.insert(k.to_string(), if total > 0 { v as f64 / total as f64 } else { 0.0 });
304    }
305    let has_cjk = counts["cjk"] > 0;
306    let has_emoji = counts["emoji"] > 0;
307    ScriptReport { primary, fractions, has_cjk, has_emoji }
308}
309
310fn classify_cp(cp: u32) -> Option<&'static str> {
311    if is_cjk(cp) { return Some("cjk"); }
312    if is_emoji(cp) { return Some("emoji"); }
313    if (0x0041..=0x005A).contains(&cp) || (0x0061..=0x007A).contains(&cp) || (0x00C0..=0x024F).contains(&cp) {
314        return Some("latin");
315    }
316    if (0x0370..=0x03FF).contains(&cp) { return Some("greek"); }
317    if (0x0400..=0x04FF).contains(&cp) { return Some("cyrillic"); }
318    if (0x0590..=0x05FF).contains(&cp) { return Some("hebrew"); }
319    if (0x0600..=0x06FF).contains(&cp) { return Some("arabic"); }
320    if (0x0900..=0x097F).contains(&cp) { return Some("devanagari"); }
321    if cp <= 0x002F
322        || (0x003A..=0x0040).contains(&cp)
323        || (0x005B..=0x0060).contains(&cp)
324        || (0x007B..=0x007E).contains(&cp)
325    {
326        return None;
327    }
328    Some("other")
329}
330
331// ── FTS helpers ──────────────────────────────────────────────────────
332
333/// Split CJK runs into 2-character grams; Latin words pass through intact.
334pub fn cjk_bigrams(text: &str) -> Vec<String> {
335    let mut out = Vec::new();
336    let mut cjk_buf = String::new();
337    let mut latin_buf = String::new();
338    let flush_cjk = |buf: &mut String, out: &mut Vec<String>| {
339        if buf.is_empty() { return; }
340        let runes: Vec<char> = buf.chars().collect();
341        if runes.len() == 1 {
342            out.push(runes[0].to_string());
343        } else {
344            for i in 0..runes.len() - 1 {
345                out.push(runes[i..i + 2].iter().collect::<String>());
346            }
347        }
348        buf.clear();
349    };
350    let flush_latin = |buf: &mut String, out: &mut Vec<String>| {
351        if !buf.is_empty() {
352            out.push(buf.clone());
353            buf.clear();
354        }
355    };
356    for c in text.chars() {
357        if is_cjk(c as u32) {
358            flush_latin(&mut latin_buf, &mut out);
359            cjk_buf.push(c);
360        } else if c.is_whitespace() {
361            flush_cjk(&mut cjk_buf, &mut out);
362            flush_latin(&mut latin_buf, &mut out);
363        } else {
364            flush_cjk(&mut cjk_buf, &mut out);
365            latin_buf.push(c);
366        }
367    }
368    flush_cjk(&mut cjk_buf, &mut out);
369    flush_latin(&mut latin_buf, &mut out);
370    out
371}
372
373/// Emit length-3 grams over emoji runs.
374pub fn emoji_trigrams(text: &str) -> Vec<String> {
375    let chars: Vec<char> = text.chars().collect();
376    let mut out = Vec::new();
377    for i in 0..chars.len() {
378        if !is_emoji(chars[i] as u32) { continue; }
379        let mut s = String::new();
380        s.push(chars[i]);
381        if i + 1 < chars.len() { s.push(chars[i + 1]); }
382        if i + 2 < chars.len() { s.push(chars[i + 2]); }
383        out.push(s);
384    }
385    out
386}
387
388/// Decomposed websearch query.
389#[derive(Debug, Clone, Default, PartialEq, Eq)]
390pub struct ParsedQuery {
391    pub required: Vec<String>,
392    pub excluded: Vec<String>,
393    pub optional: Vec<Vec<String>>,
394    pub phrases: Vec<String>,
395}
396
397/// Parse a websearch-style query into a structured form.
398pub fn parse_websearch(query: &str) -> ParsedQuery {
399    let tokens = tokenize_with_phrases(query);
400    let mut out = ParsedQuery::default();
401    let mut i = 0;
402    while i < tokens.len() {
403        let (kind, val) = &tokens[i];
404        if kind == "phrase" {
405            out.phrases.push(val.clone());
406            out.required.push(val.clone());
407            i += 1;
408            continue;
409        }
410        if i + 1 < tokens.len() && tokens[i + 1].0 == "word" && tokens[i + 1].1 == "OR" {
411            let mut group = vec![val.clone()];
412            let mut j = i + 1;
413            while j + 1 < tokens.len() && tokens[j].0 == "word" && tokens[j].1 == "OR" {
414                group.push(tokens[j + 1].1.clone());
415                j += 2;
416            }
417            out.optional.push(group);
418            i = j;
419            continue;
420        }
421        if val.starts_with('-') && val.len() > 1 {
422            out.excluded.push(val[1..].to_string());
423            i += 1;
424            continue;
425        }
426        out.required.push(val.clone());
427        i += 1;
428    }
429    out
430}
431
432fn tokenize_with_phrases(q: &str) -> Vec<(String, String)> {
433    let mut out = Vec::new();
434    let chars: Vec<char> = q.chars().collect();
435    let mut i = 0;
436    while i < chars.len() {
437        if chars[i].is_whitespace() { i += 1; continue; }
438        if chars[i] == '"' {
439            let mut j = i + 1;
440            while j < chars.len() && chars[j] != '"' { j += 1; }
441            out.push(("phrase".to_string(), chars[i + 1..j].iter().collect()));
442            i = if j < chars.len() { j + 1 } else { j };
443            continue;
444        }
445        let mut j = i;
446        while j < chars.len() && !chars[j].is_whitespace() { j += 1; }
447        out.push(("word".to_string(), chars[i..j].iter().collect()));
448        i = j;
449    }
450    out
451}
452
453/// Render a parsed query as a SQLite FTS5 MATCH expression.
454pub fn to_fts5_match(p: &ParsedQuery) -> String {
455    let mut parts: Vec<String> = Vec::new();
456    for r in &p.required { parts.push(quote_fts5(r)); }
457    for group in &p.optional {
458        let alts: Vec<String> = group.iter().map(|g| quote_fts5(g)).collect();
459        parts.push(format!("({})", alts.join(" OR ")));
460    }
461    let mut s = parts.join(" AND ");
462    for e in &p.excluded {
463        s.push_str(&format!(" NOT {}", quote_fts5(e)));
464    }
465    s.trim().to_string()
466}
467
468fn quote_fts5(term: &str) -> String {
469    let simple = !term.is_empty() && term.chars().all(|c| c.is_alphanumeric() || c == '_');
470    if !simple || term.contains(' ') {
471        let escaped = term.replace('"', "\"\"");
472        format!("\"{}\"", escaped)
473    } else {
474        term.to_string()
475    }
476}
477
478// ── Embed registry + MRL ─────────────────────────────────────────────
479
480/// Embedding model profile.
481#[derive(Debug, Clone)]
482pub struct EmbeddingModel {
483    pub slug: String,
484    pub dim: usize,
485    pub mrl_dims: Vec<usize>,
486    pub prefix_query: Option<String>,
487    pub prefix_passage: Option<String>,
488    pub family: Option<String>,
489}
490
491fn embed_registry() -> &'static std::sync::Mutex<HashMap<String, EmbeddingModel>> {
492    use std::sync::OnceLock;
493    static REG: OnceLock<std::sync::Mutex<HashMap<String, EmbeddingModel>>> = OnceLock::new();
494    REG.get_or_init(|| {
495        let mut m = HashMap::new();
496        m.insert("ollama:nomic-embed-text".into(), EmbeddingModel {
497            slug: "ollama:nomic-embed-text".into(), dim: 768,
498            mrl_dims: vec![128, 256, 512, 768],
499            prefix_query: None, prefix_passage: None,
500            family: Some("nomic".into()),
501        });
502        m.insert("intfloat/e5-large-v2".into(), EmbeddingModel {
503            slug: "intfloat/e5-large-v2".into(), dim: 1024,
504            mrl_dims: vec![],
505            prefix_query: Some("query: ".into()), prefix_passage: Some("passage: ".into()),
506            family: Some("e5".into()),
507        });
508        m.insert("openai:text-embedding-3-small".into(), EmbeddingModel {
509            slug: "openai:text-embedding-3-small".into(), dim: 1536,
510            mrl_dims: vec![256, 512, 768, 1024, 1536],
511            prefix_query: None, prefix_passage: None,
512            family: Some("openai".into()),
513        });
514        m.insert("openai:text-embedding-3-large".into(), EmbeddingModel {
515            slug: "openai:text-embedding-3-large".into(), dim: 3072,
516            mrl_dims: vec![256, 512, 1024, 2048, 3072],
517            prefix_query: None, prefix_passage: None,
518            family: Some("openai".into()),
519        });
520        std::sync::Mutex::new(m)
521    })
522}
523
524/// Register or replace an embedding model.
525pub fn register_embedding_model(m: EmbeddingModel) {
526    embed_registry().lock().unwrap().insert(m.slug.clone(), m);
527}
528
529/// Look up an embedding model by slug.
530pub fn get_embedding_model(slug: &str) -> Option<EmbeddingModel> {
531    embed_registry().lock().unwrap().get(slug).cloned()
532}
533
534/// List all known embedding models.
535pub fn list_embedding_models() -> Vec<EmbeddingModel> {
536    embed_registry().lock().unwrap().values().cloned().collect()
537}
538
539/// Apply asymmetric query / passage prefixes when the model requires them.
540pub fn prefix_for(m: &EmbeddingModel, task: &str, text: &str) -> String {
541    if task == "symmetric" || (m.prefix_query.is_none() && m.prefix_passage.is_none()) {
542        return text.to_string();
543    }
544    match task {
545        "query" => format!("{}{}", m.prefix_query.clone().unwrap_or_default(), text),
546        _ => format!("{}{}", m.prefix_passage.clone().unwrap_or_default(), text),
547    }
548}
549
550/// L2-normalize a vector in place.
551pub fn l2_normalize(v: &mut [f64]) {
552    let mut s = 0.0_f64;
553    for x in v.iter() { s += x * x; }
554    let n = s.sqrt();
555    if n == 0.0 { return; }
556    for x in v.iter_mut() { *x /= n; }
557}
558
559/// Matryoshka truncate + re-normalize.
560pub fn mrl_truncate(embedding: &[f64], target_dim: usize) -> Vec<f64> {
561    assert!(target_dim > 0, "target_dim must be positive");
562    let mut copy = if target_dim >= embedding.len() {
563        embedding.to_vec()
564    } else {
565        embedding[..target_dim].to_vec()
566    };
567    l2_normalize(&mut copy);
568    copy
569}
570
571/// Coarse dim for two-stage retrieval (~1/8 of native).
572pub fn coarse_dim(m: &EmbeddingModel) -> usize {
573    if m.mrl_dims.is_empty() { return m.dim; }
574    let target = (m.dim as f64) / 8.0;
575    for &d in &m.mrl_dims {
576        if (d as f64) >= target { return d; }
577    }
578    *m.mrl_dims.last().unwrap_or(&m.dim)
579}
580
581// ── Temporal ─────────────────────────────────────────────────────────
582
583/// Render a UUIDv7 floor for the given epoch-ms timestamp.
584pub fn v7_floor(epoch_ms: i64) -> String { v7(epoch_ms, false) }
585
586/// Render a UUIDv7 ceiling for the given epoch-ms timestamp.
587pub fn v7_ceiling(epoch_ms: i64) -> String { v7(epoch_ms, true) }
588
589fn v7(epoch_ms: i64, ceiling: bool) -> String {
590    let ts = if epoch_ms < 0 { 0u64 } else { epoch_ms as u64 };
591    let hex = format!("{:012x}", ts);
592    let hex = &hex[hex.len() - 12..];
593    let (th, tl) = (&hex[..8], &hex[8..12]);
594    if ceiling {
595        format!("{}-{}-7fff-bfff-ffffffffffff", th, tl)
596    } else {
597        format!("{}-{}-7000-8000-000000000000", th, tl)
598    }
599}
600
601// ── Captions ─────────────────────────────────────────────────────────
602
603#[derive(Debug, Clone)]
604pub struct CaptionSegment {
605    pub start_secs: f64,
606    pub end_secs: f64,
607    pub text: String,
608    pub speaker: Option<String>,
609}
610
611/// Render WebVTT.
612pub fn render_vtt(segs: &[CaptionSegment]) -> String {
613    let mut s = String::from("WEBVTT\n\n");
614    for (i, sg) in segs.iter().enumerate() {
615        s.push_str(&format!("{}\n", i + 1));
616        s.push_str(&format!("{} --> {}\n", fmt_time(sg.start_secs, '.'), fmt_time(sg.end_secs, '.')));
617        match &sg.speaker {
618            Some(sp) => s.push_str(&format!("<v {}>{}</v>\n\n", sp, sg.text)),
619            None => s.push_str(&format!("{}\n\n", sg.text)),
620        }
621    }
622    s
623}
624
625/// Render SubRip (SRT).
626pub fn render_srt(segs: &[CaptionSegment]) -> String {
627    let mut s = String::new();
628    for (i, sg) in segs.iter().enumerate() {
629        s.push_str(&format!("{}\n", i + 1));
630        s.push_str(&format!("{} --> {}\n", fmt_time(sg.start_secs, ','), fmt_time(sg.end_secs, ',')));
631        match &sg.speaker {
632            Some(sp) => s.push_str(&format!("{}: {}\n\n", sp, sg.text)),
633            None => s.push_str(&format!("{}\n\n", sg.text)),
634        }
635    }
636    s
637}
638
639/// Render NIST RTTM.
640pub fn render_rttm(segs: &[CaptionSegment], uri: &str) -> String {
641    let uri = if uri.is_empty() { "audio" } else { uri };
642    let mut s = String::new();
643    for sg in segs {
644        if let Some(sp) = &sg.speaker {
645            let dur = sg.end_secs - sg.start_secs;
646            s.push_str(&format!(
647                "SPEAKER {} 1 {:.3} {:.3} <NA> <NA> {} <NA> <NA>\n",
648                uri, sg.start_secs, dur, sp,
649            ));
650        }
651    }
652    s
653}
654
655fn fmt_time(secs: f64, ms_sep: char) -> String {
656    let ms = ((secs - secs.floor()) * 1000.0) as u32;
657    let total = secs.floor() as u32;
658    let h = total / 3600;
659    let m = (total % 3600) / 60;
660    let s = total % 60;
661    format!("{:02}:{:02}:{:02}{}{:03}", h, m, s, ms_sep, ms)
662}
663
664// ── Tokenizer ────────────────────────────────────────────────────────
665
666/// Fast BPE-style token estimate.
667pub fn estimate_tokens(text: &str) -> usize {
668    let mut total = 0usize;
669    let mut ascii_run = String::new();
670    let flush = |buf: &mut String, total: &mut usize| {
671        if buf.is_empty() { return; }
672        for w in buf.split_whitespace() {
673            *total += std::cmp::max(1, (w.chars().count() + 3) / 4);
674        }
675        buf.clear();
676    };
677    for c in text.chars() {
678        if is_cjk(c as u32) || is_emoji(c as u32) {
679            flush(&mut ascii_run, &mut total);
680            total += 1;
681        } else {
682            ascii_run.push(c);
683        }
684    }
685    flush(&mut ascii_run, &mut total);
686    total
687}
688
689/// Truncate to fit a token budget via binary search.
690pub fn truncate_to_tokens(text: &str, max: usize) -> String {
691    if estimate_tokens(text) <= max { return text.to_string(); }
692    let chars: Vec<char> = text.chars().collect();
693    let (mut lo, mut hi) = (0usize, chars.len());
694    while lo < hi {
695        let mid = (lo + hi + 1) / 2;
696        let s: String = chars[..mid].iter().collect();
697        if estimate_tokens(&s) <= max { lo = mid; } else { hi = mid - 1; }
698    }
699    chars[..lo].iter().collect()
700}
701
702// ── Eval ─────────────────────────────────────────────────────────────
703
704/// Per-query evaluation row.
705#[derive(Debug, Clone)]
706pub struct QueryEval {
707    pub predicted: Vec<String>,
708    pub relevant: HashMap<String, i32>,
709}
710
711fn rel_set(q: &QueryEval) -> HashSet<&String> {
712    q.relevant.iter().filter_map(|(k, v)| if *v > 0 { Some(k) } else { None }).collect()
713}
714
715pub fn reciprocal_rank(q: &QueryEval) -> f64 {
716    let rel = rel_set(q);
717    for (i, p) in q.predicted.iter().enumerate() {
718        if rel.contains(p) { return 1.0 / (i + 1) as f64; }
719    }
720    0.0
721}
722
723pub fn mean_reciprocal_rank(qs: &[QueryEval]) -> f64 {
724    if qs.is_empty() { return 0.0; }
725    qs.iter().map(reciprocal_rank).sum::<f64>() / qs.len() as f64
726}
727
728pub fn recall_at_k(q: &QueryEval, k: usize) -> f64 {
729    let rel = rel_set(q);
730    if rel.is_empty() { return 0.0; }
731    let hits = q.predicted.iter().take(k).filter(|p| rel.contains(p)).count();
732    hits as f64 / rel.len() as f64
733}
734
735pub fn precision_at_k(q: &QueryEval, k: usize) -> f64 {
736    let head: Vec<_> = q.predicted.iter().take(k).collect();
737    if head.is_empty() { return 0.0; }
738    let rel = rel_set(q);
739    let hits = head.iter().filter(|p| rel.contains(**p)).count();
740    hits as f64 / head.len() as f64
741}
742
743pub fn ndcg_at_k(q: &QueryEval, k: usize) -> f64 {
744    let dcg = |slugs: &[&String]| {
745        let mut s = 0.0;
746        for (i, slug) in slugs.iter().enumerate() {
747            let g = q.relevant.get(*slug).copied().unwrap_or(0) as f64;
748            s += (2f64.powf(g) - 1.0) / (((i + 2) as f64).log2());
749        }
750        s
751    };
752    let mut sorted: Vec<(&String, i32)> = q.relevant.iter().map(|(k, v)| (k, *v)).collect();
753    sorted.sort_by(|a, b| b.1.cmp(&a.1));
754    let ideal: Vec<&String> = sorted.iter().take(k).map(|(k, _)| *k).collect();
755    let idcg = dcg(&ideal);
756    let head: Vec<&String> = q.predicted.iter().take(k).collect();
757    let actual = dcg(&head);
758    if idcg == 0.0 { 0.0 } else { actual / idcg }
759}
760
761// ── Spatial ──────────────────────────────────────────────────────────
762
763const EARTH_KM: f64 = 6371.0088;
764
765pub fn haversine_km(lat_a: f64, lng_a: f64, lat_b: f64, lng_b: f64) -> f64 {
766    let d_lat = (lat_b - lat_a).to_radians();
767    let d_lng = (lng_b - lng_a).to_radians();
768    let r1 = lat_a.to_radians();
769    let r2 = lat_b.to_radians();
770    let x = (d_lat / 2.0).sin().powi(2)
771        + (d_lng / 2.0).sin().powi(2) * r1.cos() * r2.cos();
772    2.0 * EARTH_KM * x.sqrt().asin()
773}
774
775#[derive(Debug, Clone, Copy)]
776pub struct BBox { pub min_lat: f64, pub min_lng: f64, pub max_lat: f64, pub max_lng: f64 }
777
778pub fn bbox_around(lat: f64, lng: f64, radius_km: f64) -> BBox {
779    let d_lat = radius_km / 111.0;
780    let d_lng = radius_km / (111.0 * lat.to_radians().cos());
781    BBox { min_lat: lat - d_lat, min_lng: lng - d_lng, max_lat: lat + d_lat, max_lng: lng + d_lng }
782}
783
784pub fn in_box(lat: f64, lng: f64, b: BBox) -> bool {
785    lat >= b.min_lat && lat <= b.max_lat && lng >= b.min_lng && lng <= b.max_lng
786}
787
788// ── HTTP Range ───────────────────────────────────────────────────────
789
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
791pub enum RangeOutcome {
792    Ok { start: i64, end: i64 },
793    Unsatisfiable,
794    Invalid,
795}
796
797pub fn parse_range(header: &str, total: i64) -> RangeOutcome {
798    if !header.starts_with("bytes=") { return RangeOutcome::Invalid; }
799    let spec = header[6..].split(',').next().unwrap_or("").trim();
800    if spec.is_empty() { return RangeOutcome::Invalid; }
801    if let Some(rest) = spec.strip_prefix('-') {
802        match rest.parse::<i64>() {
803            Ok(suffix) if suffix > 0 => {
804                let start = (total - suffix).max(0);
805                return RangeOutcome::Ok { start, end: total - 1 };
806            }
807            _ => return RangeOutcome::Invalid,
808        }
809    }
810    let parts: Vec<&str> = spec.splitn(2, '-').collect();
811    let start = match parts[0].parse::<i64>() { Ok(v) => v, Err(_) => return RangeOutcome::Invalid };
812    let end = if parts.len() == 2 && !parts[1].is_empty() {
813        match parts[1].parse::<i64>() { Ok(v) => v, Err(_) => return RangeOutcome::Invalid }
814    } else {
815        total - 1
816    };
817    if start > end || start >= total { return RangeOutcome::Unsatisfiable; }
818    RangeOutcome::Ok { start, end: end.min(total - 1) }
819}
820
821pub fn content_range(start: i64, end: i64, total: i64) -> String {
822    format!("bytes {}-{}/{}", start, end, total)
823}
824
825// ── Wallet address ───────────────────────────────────────────────────
826
827const BASE58: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
828const VERSION_V1: u8 = 0x01;
829
830fn digest32(data: &[u8]) -> [u8; 32] {
831    // BLAKE3 — wallet addresses are content-addressable; every brain runtime
832    // (TS @noble/hashes/blake3, Python blake3, Go lukechampine.com/blake3,
833    // C++ vendored reference impl) hashes with BLAKE3 so the output is
834    // byte-equivalent across all five.
835    blake3::hash(data).into()
836}
837
838pub fn encode_address(public_key: &[u8], prefix: Option<&str>) -> Result<String, &'static str> {
839    if public_key.len() != 32 { return Err("public key must be 32 bytes"); }
840    let prefix = prefix.unwrap_or("hanzo");
841    let h = digest32(public_key);
842    let mut versioned = Vec::with_capacity(21);
843    versioned.push(VERSION_V1);
844    versioned.extend_from_slice(&h[..20]);
845    let cs = digest32(&versioned);
846    let mut payload = versioned.clone();
847    payload.extend_from_slice(&cs[..4]);
848    Ok(format!("{}:{}", prefix, base58_encode(&payload)))
849}
850
851#[derive(Debug, Clone)]
852pub struct DecodedAddress {
853    pub prefix: String,
854    pub version: u8,
855    pub hash: [u8; 20],
856}
857
858pub fn decode_address(addr: &str) -> Result<DecodedAddress, &'static str> {
859    let colon = match addr.find(':') { Some(i) => i, None => return Err("address: missing prefix") };
860    let body = &addr[colon + 1..];
861    let decoded = base58_decode(body)?;
862    if decoded.len() != 25 { return Err("address: wrong length"); }
863    let mut hash = [0u8; 20];
864    hash.copy_from_slice(&decoded[1..21]);
865    let expected = digest32(&decoded[..21]);
866    if decoded[21..25] != expected[..4] { return Err("address: bad checksum"); }
867    Ok(DecodedAddress { prefix: addr[..colon].to_string(), version: decoded[0], hash })
868}
869
870fn base58_encode(b: &[u8]) -> String {
871    if b.is_empty() { return String::new(); }
872    let zeros = b.iter().take_while(|&&c| c == 0).count();
873    let bytes_to_int = |b: &[u8]| -> Vec<u8> { b.to_vec() };
874    let mut n = bytes_to_int(b);
875    let mut out = String::new();
876    loop {
877        let mut remainder: u32 = 0;
878        let mut new = Vec::with_capacity(n.len());
879        let mut started = false;
880        for &byte in &n {
881            let cur = remainder * 256 + byte as u32;
882            let q = cur / 58;
883            remainder = cur % 58;
884            if started || q > 0 { new.push(q as u8); started = true; }
885        }
886        out.insert(0, BASE58.as_bytes()[remainder as usize] as char);
887        if new.is_empty() { break; }
888        n = new;
889    }
890    for _ in 0..zeros { out.insert(0, BASE58.as_bytes()[0] as char); }
891    out
892}
893
894fn base58_decode(s: &str) -> Result<Vec<u8>, &'static str> {
895    if s.is_empty() { return Ok(vec![]); }
896    let zeros = s.chars().take_while(|c| *c == BASE58.chars().next().unwrap()).count();
897    let mut n: Vec<u8> = vec![0];
898    for c in s.chars() {
899        let idx = BASE58.find(c).ok_or("base58: invalid char")? as u32;
900        let mut carry = idx;
901        for b in n.iter_mut() {
902            carry += (*b as u32) * 58;
903            *b = (carry & 0xff) as u8;
904            carry >>= 8;
905        }
906        while carry > 0 { n.push((carry & 0xff) as u8); carry >>= 8; }
907    }
908    let mut out = vec![0u8; zeros];
909    let mut rev: Vec<u8> = n.iter().rev().copied().collect();
910    while rev.first().copied() == Some(0) && rev.len() > 1 { rev.remove(0); }
911    out.extend_from_slice(&rev);
912    Ok(out)
913}
914
915// ── Graph maintenance ────────────────────────────────────────────────
916
917#[derive(Debug, Clone)]
918pub struct WeightedEdge {
919    pub source: String,
920    pub target: String,
921    pub weight: f64,
922}
923
924pub fn normalize_edges(edges: &[WeightedEdge]) -> Vec<WeightedEdge> {
925    if edges.is_empty() { return vec![]; }
926    let lo = edges.iter().map(|e| e.weight).fold(f64::INFINITY, f64::min);
927    let hi = edges.iter().map(|e| e.weight).fold(f64::NEG_INFINITY, f64::max);
928    let span = hi - lo;
929    edges.iter().map(|e| WeightedEdge {
930        source: e.source.clone(),
931        target: e.target.clone(),
932        weight: if span > 0.0 { (e.weight - lo) / span } else { 1.0 },
933    }).collect()
934}
935
936pub fn snn_score(edges: &[WeightedEdge], k: usize) -> Vec<WeightedEdge> {
937    let mut adj: HashMap<String, Vec<(String, f64)>> = HashMap::new();
938    for e in edges {
939        adj.entry(e.source.clone()).or_default().push((e.target.clone(), e.weight));
940        adj.entry(e.target.clone()).or_default().push((e.source.clone(), e.weight));
941    }
942    let mut nbrs: HashMap<String, HashSet<String>> = HashMap::new();
943    for (node, lst) in adj.iter_mut() {
944        lst.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
945        let set: HashSet<String> = lst.iter().take(k).map(|(t, _)| t.clone()).collect();
946        nbrs.insert(node.clone(), set);
947    }
948    edges.iter().map(|e| {
949        let a = nbrs.get(&e.source).cloned().unwrap_or_default();
950        let b = nbrs.get(&e.target).cloned().unwrap_or_default();
951        let inter = a.intersection(&b).count();
952        let union = a.len() + b.len() - inter;
953        let w = if union > 0 { inter as f64 / union as f64 } else { 0.0 };
954        WeightedEdge { source: e.source.clone(), target: e.target.clone(), weight: w }
955    }).collect()
956}
957
958pub fn pfnet_infinity(edges: &[WeightedEdge]) -> Vec<WeightedEdge> {
959    let mut adj: HashMap<String, HashMap<String, f64>> = HashMap::new();
960    for e in edges {
961        adj.entry(e.source.clone()).or_default().entry(e.target.clone())
962            .and_modify(|v| { if e.weight > *v { *v = e.weight; } })
963            .or_insert(e.weight);
964    }
965    let mut keep = Vec::new();
966    for e in edges {
967        let mut dominated = false;
968        if let Some(srcs) = adj.get(&e.source) {
969            for (x, w_ux) in srcs {
970                if x == &e.target { continue; }
971                if let Some(w_xv) = adj.get(x).and_then(|m| m.get(&e.target)) {
972                    let path = (*w_ux).min(*w_xv);
973                    if path > e.weight { dominated = true; break; }
974                }
975            }
976        }
977        if !dominated { keep.push(e.clone()); }
978    }
979    keep
980}
981
982pub fn louvain(edges: &[WeightedEdge], passes: usize) -> HashMap<String, i32> {
983    let passes = if passes == 0 { 10 } else { passes };
984    let mut nodes: HashSet<String> = HashSet::new();
985    for e in edges {
986        nodes.insert(e.source.clone());
987        nodes.insert(e.target.clone());
988    }
989    let mut community: HashMap<String, i32> = HashMap::new();
990    for (i, n) in nodes.iter().enumerate() {
991        community.insert(n.clone(), i as i32);
992    }
993    let mut adj: HashMap<String, Vec<(String, f64)>> = HashMap::new();
994    let mut total = 0.0;
995    for e in edges {
996        adj.entry(e.source.clone()).or_default().push((e.target.clone(), e.weight));
997        adj.entry(e.target.clone()).or_default().push((e.source.clone(), e.weight));
998        total += e.weight;
999    }
1000    let mut deg: HashMap<String, f64> = HashMap::new();
1001    for (n, lst) in &adj {
1002        let d: f64 = lst.iter().map(|(_, w)| w).sum();
1003        deg.insert(n.clone(), d);
1004    }
1005    let m = total;
1006    for _ in 0..passes {
1007        let mut improved = false;
1008        let order: Vec<String> = nodes.iter().cloned().collect();
1009        for n in &order {
1010            let cur = community[n];
1011            let mut w_to: HashMap<i32, f64> = HashMap::new();
1012            for (nb, w) in adj.get(n).cloned().unwrap_or_default() {
1013                let c = community[&nb];
1014                *w_to.entry(c).or_insert(0.0) += w;
1015            }
1016            let mut best = cur;
1017            let mut best_gain = 0.0;
1018            let kn = deg.get(n).copied().unwrap_or(0.0);
1019            for (c, wnc) in &w_to {
1020                if *c == cur { continue; }
1021                let sigma_tot: f64 = community.iter()
1022                    .filter(|(other, comm)| *comm == c && other.as_str() != n.as_str())
1023                    .map(|(other, _)| deg.get(other).copied().unwrap_or(0.0))
1024                    .sum();
1025                let gain = wnc - (kn * sigma_tot) / (2.0 * m).max(1e-9);
1026                if gain > best_gain { best_gain = gain; best = *c; }
1027            }
1028            if best != cur {
1029                community.insert(n.clone(), best);
1030                improved = true;
1031            }
1032        }
1033        if !improved { break; }
1034    }
1035    // compact ids
1036    let mut id_map: HashMap<i32, i32> = HashMap::new();
1037    let mut next = 0i32;
1038    for c in community.values() {
1039        id_map.entry(*c).or_insert_with(|| { let v = next; next += 1; v });
1040    }
1041    for (_, c) in community.iter_mut() {
1042        *c = id_map[c];
1043    }
1044    community
1045}
1046
1047// ── Inference: slug + runtime config + link types ───────────────────
1048
1049const KNOWN_PROVIDERS: &[&str] = &[
1050    "ollama", "openai", "openrouter", "llamacpp",
1051    "anthropic", "google", "azure", "groq", "together", "mock",
1052];
1053
1054#[derive(Debug, Clone, PartialEq, Eq)]
1055pub struct ParsedSlug { pub provider: String, pub model: String }
1056
1057pub fn parse_slug(slug: &str, default_provider: &str) -> ParsedSlug {
1058    let colon = match slug.find(':') {
1059        Some(i) => i,
1060        None => return ParsedSlug { provider: default_provider.into(), model: slug.into() },
1061    };
1062    let head = &slug[..colon];
1063    if KNOWN_PROVIDERS.iter().any(|&k| k == head) {
1064        return ParsedSlug { provider: head.into(), model: slug[colon + 1..].into() };
1065    }
1066    ParsedSlug { provider: default_provider.into(), model: slug.into() }
1067}
1068
1069pub fn format_slug(p: &ParsedSlug) -> String { format!("{}:{}", p.provider, p.model) }
1070
1071#[derive(Debug, Default)]
1072pub struct RuntimeConfig {
1073    pub defaults: HashMap<String, String>,
1074    pub env: HashMap<String, String>,
1075    overrides: HashMap<String, String>,
1076}
1077
1078impl RuntimeConfig {
1079    pub fn new(defaults: HashMap<String, String>, env: HashMap<String, String>) -> Self {
1080        Self { defaults, env, overrides: HashMap::new() }
1081    }
1082    pub fn get(&self, key: &str) -> Option<String> {
1083        if let Some(v) = self.overrides.get(key) { return Some(v.clone()); }
1084        if let Some(v) = self.env.get(key) { return Some(v.clone()); }
1085        self.defaults.get(key).cloned()
1086    }
1087    pub fn source(&self, key: &str) -> &'static str {
1088        if self.overrides.contains_key(key) { return "db_override"; }
1089        if self.env.contains_key(key) { return "env"; }
1090        if self.defaults.contains_key(key) { return "default"; }
1091        "absent"
1092    }
1093    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
1094        self.overrides.insert(key.into(), value.into());
1095    }
1096    pub fn clear(&mut self, key: &str) { self.overrides.remove(key); }
1097}
1098
1099pub const LINK_TYPES: &[&str] = &[
1100    "mentions", "founded", "invested_in", "advises", "works_at",
1101    "attended", "authored", "cites", "succeeded_by", "located_in", "related",
1102];
1103
1104/// Zero-LLM rule-based link classifier.
1105pub fn classify_link_rule(evidence: &str) -> &'static str {
1106    let e = evidence.to_lowercase();
1107    if e.contains("founded") { return "founded"; }
1108    if let Some(idx) = e.find("invested") {
1109        if e[idx..].contains("in") { return "invested_in"; }
1110    }
1111    if e.contains("advisor") || e.contains("advises") || e.contains("advising") { return "advises"; }
1112    if e.contains("works at") || e.contains("works for") || e.contains("work at") || e.contains("work for") { return "works_at"; }
1113    if e.contains("attended") { return "attended"; }
1114    if e.contains("wrote") || e.contains("authored") { return "authored"; }
1115    if e.contains("cites") || e.contains("cite ") { return "cites"; }
1116    if e.contains("succeeded by") { return "succeeded_by"; }
1117    if e.contains("located in") { return "located_in"; }
1118    "mentions"
1119}
1120
1121// ── Tests ────────────────────────────────────────────────────────────
1122
1123#[cfg(test)]
1124mod tests {
1125    use super::*;
1126    use crate::brain::store::SearchHit;
1127
1128    fn hit(slug: &str, score: f64) -> SearchHit {
1129        SearchHit { slug: slug.into(), score, excerpt: slug.into(), source: "keyword".into() }
1130    }
1131
1132    #[test]
1133    fn test_rrf_top_one() {
1134        let r = rrf_fuse(vec![vec![hit("a", 1.0), hit("b", 0.5)]], 10, 0.0);
1135        assert_eq!(r[0].slug, "a");
1136        assert!((r[0].score - 1.0).abs() < 0.01);
1137    }
1138
1139    #[test]
1140    fn test_rsf_preserves_magnitude() {
1141        let r = rsf_fuse(vec![vec![hit("a", 100.0), hit("b", 50.0)], vec![hit("a", 1.0), hit("c", 0.5)]], 10, None);
1142        assert_eq!(r[0].slug, "a");
1143        assert_eq!(r.len(), 3);
1144    }
1145
1146    #[test]
1147    fn test_select_rrf_k() {
1148        assert_eq!(select_rrf_k(characterize("\"hello world\"")), 10);
1149        assert_eq!(select_rrf_k(characterize("foo AND bar")), 15);
1150        assert_eq!(select_rrf_k(characterize("rust")), 15);
1151        assert_eq!(select_rrf_k(characterize("a b c d e f g h i j")), 40);
1152    }
1153
1154    #[test]
1155    fn test_select_weights() {
1156        let short = select_weights(characterize("rust"));
1157        assert!(short.fts > short.semantic);
1158        let long = select_weights(characterize("how do retrieval augmented generation systems typically work in production scale"));
1159        assert!(long.semantic > long.fts);
1160    }
1161
1162    #[test]
1163    fn test_cosine() {
1164        assert!((cosine(&[1.0, 0.0], &[1.0, 0.0]) - 1.0).abs() < 1e-6);
1165        assert!(cosine(&[1.0, 0.0], &[0.0, 1.0]).abs() < 1e-6);
1166    }
1167
1168    #[test]
1169    fn test_mmr() {
1170        let hits = vec![
1171            MmrInput { hit: hit("a", 0.9), embedding: Some(vec![1.0, 0.0]) },
1172            MmrInput { hit: hit("b", 0.85), embedding: Some(vec![1.0, 0.01]) },
1173            MmrInput { hit: hit("c", 0.6), embedding: Some(vec![0.0, 1.0]) },
1174        ];
1175        let out = mmr_rerank(hits, 0.2, 2);
1176        assert_eq!(out[0].hit.slug, "a");
1177        assert_eq!(out[1].hit.slug, "c");
1178    }
1179
1180    #[test]
1181    fn test_dedup() {
1182        let out = dedup_hits(vec![
1183            hit("page/foo#chunk-0", 0.5),
1184            hit("page/foo#chunk-1", 0.8),
1185            hit("page/bar", 0.6),
1186        ], 1);
1187        assert_eq!(out.len(), 2);
1188    }
1189
1190    #[test]
1191    fn test_detect_script() {
1192        assert_eq!(detect_script("こんにちは世界").primary, "cjk");
1193        assert_eq!(detect_script("Hello world").primary, "latin");
1194        assert_eq!(detect_script("Привет").primary, "cyrillic");
1195    }
1196
1197    #[test]
1198    fn test_cjk_bigrams() {
1199        let out = cjk_bigrams("hello 世界 こんにちは");
1200        assert!(out.contains(&"hello".to_string()));
1201        assert!(out.contains(&"世界".to_string()));
1202        assert!(out.contains(&"こん".to_string()));
1203    }
1204
1205    #[test]
1206    fn test_emoji_trigrams_emit() {
1207        let out = emoji_trigrams("hi 🚀🌌🌟");
1208        assert!(!out.is_empty());
1209    }
1210
1211    #[test]
1212    fn test_parse_websearch() {
1213        let p = parse_websearch("\"hello world\" foo OR bar -baz qux");
1214        assert_eq!(p.phrases, vec!["hello world"]);
1215        assert_eq!(p.optional[0], vec!["foo", "bar"]);
1216        assert_eq!(p.excluded, vec!["baz"]);
1217        assert!(p.required.contains(&"qux".to_string()));
1218    }
1219
1220    #[test]
1221    fn test_to_fts5_match() {
1222        let sql = to_fts5_match(&parse_websearch("apple OR orange -spoil"));
1223        assert!(sql.contains("apple OR orange"));
1224        assert!(sql.contains("NOT spoil"));
1225    }
1226
1227    #[test]
1228    fn test_embed_registry() {
1229        assert_eq!(get_embedding_model("ollama:nomic-embed-text").unwrap().dim, 768);
1230        assert_eq!(get_embedding_model("openai:text-embedding-3-small").unwrap().dim, 1536);
1231    }
1232
1233    #[test]
1234    fn test_prefix_for() {
1235        let e5 = get_embedding_model("intfloat/e5-large-v2").unwrap();
1236        assert_eq!(prefix_for(&e5, "query", "x"), "query: x");
1237        assert_eq!(prefix_for(&e5, "passage", "x"), "passage: x");
1238        let nomic = get_embedding_model("ollama:nomic-embed-text").unwrap();
1239        assert_eq!(prefix_for(&nomic, "query", "x"), "x");
1240    }
1241
1242    #[test]
1243    fn test_mrl_truncate() {
1244        let v: Vec<f64> = (1..=8).map(|x| x as f64).collect();
1245        let t = mrl_truncate(&v, 4);
1246        assert_eq!(t.len(), 4);
1247        let norm: f64 = t.iter().map(|x| x * x).sum::<f64>().sqrt();
1248        assert!((norm - 1.0).abs() < 1e-6);
1249    }
1250
1251    #[test]
1252    fn test_coarse_dim() {
1253        let m = get_embedding_model("openai:text-embedding-3-large").unwrap();
1254        let cd = coarse_dim(&m);
1255        assert!((256..=512).contains(&cd));
1256    }
1257
1258    #[test]
1259    fn test_l2_zero_vector() {
1260        let mut v = vec![0.0; 3];
1261        l2_normalize(&mut v);
1262        assert_eq!(v, vec![0.0; 3]);
1263    }
1264
1265    #[test]
1266    fn test_v7_floor_ceiling_order() {
1267        let t = 1_700_000_000_000i64;
1268        assert!(v7_floor(t) < v7_ceiling(t));
1269    }
1270
1271    #[test]
1272    fn test_captions() {
1273        let segs = vec![
1274            CaptionSegment { start_secs: 0.0, end_secs: 1.5, text: "hi".into(), speaker: Some("S0".into()) },
1275            CaptionSegment { start_secs: 1.5, end_secs: 3.0, text: "world".into(), speaker: Some("S1".into()) },
1276        ];
1277        assert!(render_vtt(&segs).starts_with("WEBVTT"));
1278        assert!(render_srt(&segs).contains("00:00:00,000 --> 00:00:01,500"));
1279        assert!(render_rttm(&segs, "").starts_with("SPEAKER"));
1280    }
1281
1282    #[test]
1283    fn test_estimate_tokens() {
1284        assert!(estimate_tokens("hi there friend") > estimate_tokens("hi"));
1285        assert_eq!(estimate_tokens("こんにちは"), 5);
1286    }
1287
1288    #[test]
1289    fn test_truncate_to_tokens() {
1290        let long = "alpha ".repeat(100);
1291        let t = truncate_to_tokens(&long, 20);
1292        assert!(estimate_tokens(&t) <= 20);
1293    }
1294
1295    fn qe() -> QueryEval {
1296        let mut r = HashMap::new();
1297        r.insert("c".into(), 1);
1298        r.insert("d".into(), 1);
1299        QueryEval { predicted: vec!["a", "b", "c", "d"].into_iter().map(String::from).collect(), relevant: r }
1300    }
1301
1302    #[test]
1303    fn test_reciprocal_rank() {
1304        assert!((reciprocal_rank(&qe()) - 1.0 / 3.0).abs() < 1e-6);
1305    }
1306
1307    #[test]
1308    fn test_recall_at_k() {
1309        let q = qe();
1310        assert_eq!(recall_at_k(&q, 2), 0.0);
1311        assert!((recall_at_k(&q, 4) - 1.0).abs() < 1e-6);
1312    }
1313
1314    #[test]
1315    fn test_precision_at_k() {
1316        assert!((precision_at_k(&qe(), 4) - 0.5).abs() < 1e-6);
1317    }
1318
1319    #[test]
1320    fn test_ndcg_graded() {
1321        let mut r = HashMap::new();
1322        r.insert("a".into(), 3);
1323        r.insert("b".into(), 1);
1324        let g = QueryEval { predicted: vec!["a", "b"].into_iter().map(String::from).collect(), relevant: r };
1325        assert!(ndcg_at_k(&g, 2) > 0.9);
1326    }
1327
1328    #[test]
1329    fn test_mrr() {
1330        assert!(mean_reciprocal_rank(&[qe()]) > 0.0);
1331    }
1332
1333    #[test]
1334    fn test_haversine() {
1335        assert!(haversine_km(0.0, 0.0, 0.0, 0.0).abs() < 1e-6);
1336        let d = haversine_km(40.7128, -74.006, 34.0522, -118.2437);
1337        assert!((d - 3935.0).abs() < 50.0);
1338    }
1339
1340    #[test]
1341    fn test_bbox() {
1342        let center = (37.77, -122.42);
1343        let box_ = bbox_around(center.0, center.1, 10.0);
1344        assert!(in_box(center.0, center.1, box_));
1345        assert!(!in_box(0.0, 0.0, box_));
1346    }
1347
1348    #[test]
1349    fn test_range_parsing() {
1350        assert_eq!(parse_range("bytes=0-99", 1000), RangeOutcome::Ok { start: 0, end: 99 });
1351        assert_eq!(parse_range("bytes=-100", 1000), RangeOutcome::Ok { start: 900, end: 999 });
1352        assert_eq!(parse_range("bytes=2000-3000", 1000), RangeOutcome::Unsatisfiable);
1353        assert_eq!(content_range(0, 99, 1000), "bytes 0-99/1000");
1354    }
1355
1356    #[test]
1357    fn test_address_round_trip() {
1358        let pk: Vec<u8> = (0..32u8).collect();
1359        let addr = encode_address(&pk, None).unwrap();
1360        assert!(addr.starts_with("hanzo:"));
1361        let dec = decode_address(&addr).unwrap();
1362        assert_eq!(dec.prefix, "hanzo");
1363        assert_eq!(dec.version, 1);
1364    }
1365
1366    #[test]
1367    fn test_address_bad_checksum() {
1368        assert!(decode_address("hanzo:11111111111111111111111111").is_err());
1369    }
1370
1371    #[test]
1372    fn test_normalize_edges() {
1373        let out = normalize_edges(&[
1374            WeightedEdge { source: "a".into(), target: "b".into(), weight: 10.0 },
1375            WeightedEdge { source: "b".into(), target: "c".into(), weight: 5.0 },
1376        ]);
1377        assert!((out[0].weight - 1.0).abs() < 1e-6);
1378        assert!(out[1].weight.abs() < 1e-6);
1379    }
1380
1381    #[test]
1382    fn test_snn_bounds() {
1383        let edges = vec![
1384            WeightedEdge { source: "a".into(), target: "b".into(), weight: 0.9 },
1385            WeightedEdge { source: "a".into(), target: "c".into(), weight: 0.8 },
1386            WeightedEdge { source: "b".into(), target: "c".into(), weight: 0.7 },
1387        ];
1388        for e in snn_score(&edges, 2) {
1389            assert!((0.0..=1.0).contains(&e.weight));
1390        }
1391    }
1392
1393    #[test]
1394    fn test_pfnet_drops_dominated() {
1395        let edges = vec![
1396            WeightedEdge { source: "a".into(), target: "b".into(), weight: 0.9 },
1397            WeightedEdge { source: "b".into(), target: "c".into(), weight: 0.9 },
1398            WeightedEdge { source: "a".into(), target: "c".into(), weight: 0.5 },
1399        ];
1400        for e in pfnet_infinity(&edges) {
1401            assert!(!(e.source == "a" && e.target == "c"));
1402        }
1403    }
1404
1405    #[test]
1406    fn test_louvain_size() {
1407        let edges = vec![
1408            WeightedEdge { source: "a".into(), target: "b".into(), weight: 1.0 },
1409            WeightedEdge { source: "b".into(), target: "c".into(), weight: 1.0 },
1410            WeightedEdge { source: "a".into(), target: "c".into(), weight: 1.0 },
1411        ];
1412        let c = louvain(&edges, 0);
1413        assert_eq!(c.len(), 3);
1414    }
1415
1416    #[test]
1417    fn test_parse_slug() {
1418        let p = parse_slug("openai:gpt-4o", "ollama");
1419        assert_eq!(p, ParsedSlug { provider: "openai".into(), model: "gpt-4o".into() });
1420        let p = parse_slug("qwen3:8b", "ollama");
1421        assert_eq!(p, ParsedSlug { provider: "ollama".into(), model: "qwen3:8b".into() });
1422        assert_eq!(format_slug(&ParsedSlug { provider: "openai".into(), model: "gpt-4o".into() }), "openai:gpt-4o");
1423    }
1424
1425    #[test]
1426    fn test_runtime_config_precedence() {
1427        let mut rc = RuntimeConfig::new(
1428            HashMap::from([("K".into(), "default".into())]),
1429            HashMap::from([("K".into(), "env".into())]),
1430        );
1431        assert_eq!(rc.get("K").as_deref(), Some("env"));
1432        rc.set("K", "override");
1433        assert_eq!(rc.get("K").as_deref(), Some("override"));
1434        assert_eq!(rc.source("K"), "db_override");
1435        rc.clear("K");
1436        assert_eq!(rc.get("K").as_deref(), Some("env"));
1437    }
1438
1439    #[test]
1440    fn test_classify_link_rule() {
1441        assert_eq!(classify_link_rule("Alice founded Acme"), "founded");
1442        assert_eq!(classify_link_rule("Alice invested in Acme"), "invested_in");
1443        assert_eq!(classify_link_rule("worked together"), "mentions");
1444    }
1445}