lantern 0.3.0

Local-first, provenance-aware semantic search for agent activity
Documentation
use std::fs;

use lantern::ingest::ingest_path;
use lantern::query_success::{apply_query_success, get_query_success_count, record_query_success};
use lantern::search::{SearchOptions, search};
use lantern::store::Store;
use tempfile::tempdir;

fn setup_store_with(files: &[(&str, &str)]) -> (tempfile::TempDir, Store) {
    let root = tempdir().unwrap();
    let mut store = Store::initialize(&root.path().join("store")).unwrap();
    let data = root.path().join("data");
    fs::create_dir_all(&data).unwrap();
    for (name, body) in files {
        fs::write(data.join(name), body).unwrap();
    }
    ingest_path(&mut store, &data).unwrap();
    (root, store)
}

#[test]
fn new_chunks_default_to_neutral_query_success_count() {
    let (_root, store) = setup_store_with(&[("a.md", "Lanterns glow in the dark forest.")]);
    let hits = search(&store, "lantern", SearchOptions::default()).unwrap();
    assert_eq!(hits.len(), 1);
    assert_eq!(hits[0].query_success_count, 0);
}

#[test]
fn record_query_success_increments_and_is_visible_on_next_search() {
    let (_root, store) = setup_store_with(&[("a.md", "Lanterns glow in the dark forest.")]);

    let chunk_id = {
        let hits = search(&store, "lantern", SearchOptions::default()).unwrap();
        hits[0].chunk_id.clone()
    };

    assert_eq!(record_query_success(&store, &chunk_id).unwrap(), 1);
    assert_eq!(record_query_success(&store, &chunk_id).unwrap(), 2);
    assert_eq!(get_query_success_count(&store, &chunk_id).unwrap(), Some(2));

    let hits = search(&store, "lantern", SearchOptions::default()).unwrap();
    let hit = hits.iter().find(|h| h.chunk_id == chunk_id).unwrap();
    assert_eq!(hit.query_success_count, 2);
}

#[test]
fn apply_query_success_returns_updated_confidence_snapshot() {
    let (_root, store) = setup_store_with(&[("a.md", "Lanterns glow in the dark forest.")]);

    let chunk_id = {
        let hits = search(&store, "lantern", SearchOptions::default()).unwrap();
        hits[0].chunk_id.clone()
    };

    let report = apply_query_success(&store, &chunk_id).unwrap();
    assert_eq!(report.chunk_id, chunk_id);
    assert_eq!(report.count, 1);
    assert!(
        (0.0..=1.0).contains(&report.confidence),
        "confidence should stay in range: {:?}",
        report.confidence_breakdown
    );
    assert!(
        report.confidence_breakdown.query_success_factor > 0.0,
        "expected positive query-success factor in report: {:?}",
        report.confidence_breakdown
    );
}

#[test]
fn query_success_raises_confidence_relative_to_neutral_peer() {
    let (_root, store) = setup_store_with(&[
        ("a.md", "Lanterns glow in the dark forest."),
        ("b.md", "Lanterns are useful in old mines as well."),
    ]);

    // Pin the baseline so freshness/access components are identical across
    // chunks and the query-success signal is the only differentiator going
    // into the search. Reading chunk ids directly (rather than via a first
    // search) avoids the search-bump that would otherwise push both chunks'
    // `last_accessed_at` to "now", saturating freshness to 1.0 and hiding
    // the lift behind a clamp.
    store
        .conn()
        .execute(
            "UPDATE chunks
             SET timestamp_unix = 1,
                 last_accessed_at = NULL,
                 access_count = 0,
                 access_decay_at = NULL",
            [],
        )
        .unwrap();
    let chunk_ids: Vec<String> = store
        .conn()
        .prepare("SELECT id FROM chunks ORDER BY id")
        .unwrap()
        .query_map([], |row| row.get(0))
        .unwrap()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    assert_eq!(chunk_ids.len(), 2, "expected exactly two chunks");
    let success_id = chunk_ids[0].clone();
    let other_id = chunk_ids[1].clone();

    apply_query_success(&store, &success_id).unwrap();
    assert_eq!(
        get_query_success_count(&store, &success_id).unwrap(),
        Some(1)
    );
    assert_eq!(get_query_success_count(&store, &other_id).unwrap(), Some(0));

    let hits = search(&store, "lantern", SearchOptions::default()).unwrap();
    let success = hits.iter().find(|h| h.chunk_id == success_id).unwrap();
    let other = hits.iter().find(|h| h.chunk_id == other_id).unwrap();

    assert_eq!(success.query_success_count, 1);
    assert_eq!(other.query_success_count, 0);
    assert!(
        success.confidence > other.confidence,
        "query-success should strictly raise confidence above a neutral peer \
         (success={}, other={})",
        success.confidence,
        other.confidence,
    );
    // The breakdown must explain the lift: the successful chunk's
    // query_success_factor should be strictly positive, and the neutral
    // peer's should remain at the documented neutral default of 0.
    assert!(
        success.confidence_breakdown.query_success_factor > 0.0,
        "query_success_factor should be > 0 after a recorded success: {:?}",
        success.confidence_breakdown,
    );
    assert_eq!(
        other.confidence_breakdown.query_success_factor, 0.0,
        "neutral peer must keep the query_success_factor=0 default: {:?}",
        other.confidence_breakdown,
    );
}

#[test]
fn record_query_success_errors_for_missing_chunk() {
    let (_root, store) = setup_store_with(&[("a.md", "just a seed chunk")]);
    let err = record_query_success(&store, "does-not-exist").unwrap_err();
    assert!(
        err.to_string().contains("no chunk"),
        "unexpected error: {err}"
    );
    assert_eq!(
        get_query_success_count(&store, "does-not-exist").unwrap(),
        None,
    );
}