lantern 0.2.3

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

use lantern::feedback::{Feedback, get_feedback_score, record_feedback};
use lantern::ingest::ingest_path;
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_feedback_score() {
    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].feedback_score, 0);
}

#[test]
fn record_feedback_updates_score_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_feedback(&store, &chunk_id, Feedback::Up).unwrap(), 1);
    assert_eq!(record_feedback(&store, &chunk_id, Feedback::Up).unwrap(), 2);
    assert_eq!(
        record_feedback(&store, &chunk_id, Feedback::Down).unwrap(),
        1
    );
    assert_eq!(
        record_feedback(&store, &chunk_id, Feedback::Custom(5)).unwrap(),
        6
    );

    assert_eq!(get_feedback_score(&store, &chunk_id).unwrap(), Some(6));

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

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

    // Make the baseline non-saturated so the feedback signal has room to move.
    store
        .conn()
        .execute(
            "UPDATE chunks SET timestamp_unix = 1, last_accessed_at = NULL, access_count = 0",
            [],
        )
        .unwrap();

    let first_pass = search(&store, "lantern", SearchOptions::default()).unwrap();
    let rated_id = first_pass[0].chunk_id.clone();
    let other_id = first_pass[1].chunk_id.clone();

    for _ in 0..2 {
        record_feedback(&store, &rated_id, Feedback::Up).unwrap();
    }

    assert_eq!(get_feedback_score(&store, &rated_id).unwrap(), Some(2));

    let after = search(&store, "lantern", SearchOptions::default()).unwrap();
    let rated = after.iter().find(|h| h.chunk_id == rated_id).unwrap();
    let other = after.iter().find(|h| h.chunk_id == other_id).unwrap();

    assert_eq!(rated.feedback_score, 2);
    assert_eq!(other.feedback_score, 0);
    assert!(
        rated.confidence >= other.confidence,
        "feedback should not reduce a chunk's confidence"
    );
}

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

    // Match the positive-side test's baseline: ancient source timestamps with
    // no prior access, so the feedback signal is the only per-chunk
    // differentiator going into the second search.
    store
        .conn()
        .execute(
            "UPDATE chunks SET timestamp_unix = 1, last_accessed_at = NULL, access_count = 0",
            [],
        )
        .unwrap();

    let first_pass = search(&store, "lantern", SearchOptions::default()).unwrap();
    let rated_id = first_pass[0].chunk_id.clone();
    let other_id = first_pass[1].chunk_id.clone();

    for _ in 0..2 {
        record_feedback(&store, &rated_id, Feedback::Down).unwrap();
    }

    assert_eq!(get_feedback_score(&store, &rated_id).unwrap(), Some(-2));

    let after = search(&store, "lantern", SearchOptions::default()).unwrap();
    let rated = after.iter().find(|h| h.chunk_id == rated_id).unwrap();
    let other = after.iter().find(|h| h.chunk_id == other_id).unwrap();

    assert_eq!(rated.feedback_score, -2);
    assert_eq!(other.feedback_score, 0);
    assert!(
        rated.confidence < other.confidence,
        "negative feedback should strictly lower confidence below the neutral peer \
         (rated={}, other={})",
        rated.confidence,
        other.confidence,
    );
}

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