semantic-memory 0.5.13

Local-first hybrid semantic search (SQLite + FTS5 + usearch 2.25) with bitemporal truth and typed receipts
Documentation
#![cfg(feature = "rl-routing")]
#![allow(clippy::expect_used)]

use semantic_memory::rl_routing::{
    is_trained, record_routing_outcome, route_with_policy, RoutingOutcome, RoutingPolicy,
};
use semantic_memory::routing::{QueryProfile, RetrievalRouter, RoutingDecision};
use semantic_memory::{MemoryConfig, MemoryStore, MockEmbedder};

fn open_store(config: MemoryConfig) -> MemoryStore {
    let dimensions = config.embedding.dimensions;
    MemoryStore::open_with_embedder(config, Box::new(MockEmbedder::new(dimensions)))
        .expect("open routing-policy test store")
}

fn same_stages(left: &RoutingDecision, right: &RoutingDecision) -> bool {
    left.bm25_coarse == right.bm25_coarse
        && left.vector_medium == right.vector_medium
        && left.rerank_fine == right.rerank_fine
        && left.graph_expansion == right.graph_expansion
        && left.decoder == right.decoder
        && left.discord == right.discord
        && left.no_retrieval == right.no_retrieval
}

#[tokio::test]
async fn trained_policy_persists_and_routes_identically_after_restart() {
    let dir = tempfile::tempdir().expect("temp dir");
    let config = MemoryConfig {
        base_dir: dir.path().to_path_buf(),
        ..Default::default()
    };
    let store = open_store(config.clone());

    let profile = QueryProfile::from_query("compare rust vs python performance");
    let heuristic = RetrievalRouter::default().route(&profile);
    assert!(heuristic.rerank_fine);

    let mut policy = RoutingPolicy::default();
    for _ in 0..11 {
        record_routing_outcome(&mut policy, &profile, &heuristic, RoutingOutcome::Bad);
    }
    assert!(is_trained(&policy));
    assert!(policy.last_updated.is_some());

    let before_restart = route_with_policy(&policy, &profile);
    assert!(
        !same_stages(&before_restart, &heuristic),
        "training should change at least one routing stage"
    );
    store
        .save_routing_policy(&policy)
        .await
        .expect("persist routing policy");
    drop(store);

    let restarted = open_store(config);
    let loaded = restarted
        .load_routing_policy()
        .await
        .expect("load routing policy")
        .expect("persisted routing policy");
    assert_eq!(loaded.trained_examples, policy.trained_examples);
    for (stage, expected) in &policy.weights {
        let actual = loaded.weights.get(stage).expect("persisted stage weight");
        assert!(
            (actual - expected).abs() < 1e-12,
            "weight changed for {stage}: expected {expected}, loaded {actual}"
        );
    }
    assert_eq!(loaded.last_updated, policy.last_updated);

    let after_restart = route_with_policy(&loaded, &profile);
    assert!(same_stages(&before_restart, &after_restart));
}