talon-core 0.4.2

Core retrieval engine for Talon: hybrid search (BM25 + semantic + reranker), indexing, and graph-aware ranking over markdown corpora.
Documentation
use std::collections::BTreeMap;

use rusqlite::{Connection, params};

use crate::graph::{GraphNode, GraphSnapshot, build_missing_link_suggestions};
use crate::indexing::migrations::run_migrations;

use super::suggest::line_mentions_term;

#[test]
fn suggestions_skip_existing_links_code_and_markdown() -> Result<(), Box<dyn std::error::Error>> {
    let mut conn = Connection::open_in_memory()?;
    run_migrations(&mut conn)?;
    insert_note(
        &conn,
        "Source.md",
        "Inline `Target Note` and [Target Note](https://example.com) are ignored.\n\
         Target Note should be linked here.\n\
         ```\nTarget Note ignored in fence\n```",
    )?;
    let mut snapshot = snapshot_with_targets(["Source.md", "Target.md"]);
    snapshot.edges.push(crate::graph::GraphEdge {
        from_path: "Other.md".into(),
        to_path: "Target.md".into(),
        link_text: "Target".into(),
        weight: 1,
    });

    let suggestions = build_missing_link_suggestions(&conn, &snapshot)?;

    assert_eq!(suggestions.len(), 1);
    assert_eq!(suggestions[0].path, "Source.md");
    assert_eq!(suggestions[0].target, "Target.md");
    assert_eq!(suggestions[0].line, Some(2));
    Ok(())
}

#[test]
fn suggestions_limit_one_per_target_per_source() -> Result<(), Box<dyn std::error::Error>> {
    let mut conn = Connection::open_in_memory()?;
    run_migrations(&mut conn)?;
    insert_note(&conn, "Source.md", "Target Note here.\nTarget Note again.")?;
    let snapshot = snapshot_with_targets(["Source.md", "Target.md"]);

    let suggestions = build_missing_link_suggestions(&conn, &snapshot)?;

    assert_eq!(suggestions.len(), 1);
    Ok(())
}

#[test]
fn suggestions_skip_basename_only_terms() -> Result<(), Box<dyn std::error::Error>> {
    let mut conn = Connection::open_in_memory()?;
    run_migrations(&mut conn)?;
    insert_note(
        &conn,
        "Source.md",
        "Review should not link by filename alone.",
    )?;
    let mut snapshot = snapshot_with_targets(["Source.md", "Review.md"]);
    set_title(&mut snapshot, "Review.md", "Editorial Pass")?;

    let suggestions = build_missing_link_suggestions(&conn, &snapshot)?;

    assert!(suggestions.is_empty());
    Ok(())
}

#[test]
fn suggestions_require_case_signal_for_lowercase_phrases() -> Result<(), Box<dyn std::error::Error>>
{
    let mut conn = Connection::open_in_memory()?;
    run_migrations(&mut conn)?;
    insert_note(
        &conn,
        "Source.md",
        "the silver ledger pattern is discussed here.",
    )?;
    let mut snapshot = snapshot_with_targets(["Source.md", "Silver Ledger.md"]);
    set_title(&mut snapshot, "Silver Ledger.md", "Silver Ledger")?;

    let suggestions = build_missing_link_suggestions(&conn, &snapshot)?;

    assert!(suggestions.is_empty());
    Ok(())
}

#[test]
fn short_single_word_terms_are_not_suggested() {
    let salient_terms = std::collections::BTreeSet::from(["mise".to_string()]);

    assert!(!line_mentions_term("Mise station", "mise", &salient_terms));
}

fn snapshot_with_targets(paths: impl IntoIterator<Item = &'static str>) -> GraphSnapshot {
    GraphSnapshot {
        nodes: paths
            .into_iter()
            .map(|path| (path.to_string(), node(path)))
            .collect::<BTreeMap<_, _>>(),
        ..GraphSnapshot::default()
    }
}

fn node(path: &str) -> GraphNode {
    GraphNode {
        vault_path: path.into(),
        title: if path == "Target.md" {
            "Target Note".into()
        } else {
            path.into()
        },
        aliases: Vec::new(),
        tags: Vec::new(),
        scope: String::new(),
        note_type: None,
        sources: Vec::new(),
        outgoing_degree: 0,
        backlink_degree: 0,
        total_degree: 0,
        structural: false,
        community_id: None,
        community_cohesion: 0.0,
        community_neighbor_count: 0,
        bridge_weight: 0.0,
    }
}

fn set_title(
    snapshot: &mut GraphSnapshot,
    path: &str,
    title: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let Some(node) = snapshot.nodes.get_mut(path) else {
        return Err(format!("missing test node {path}").into());
    };
    node.title = title.into();
    Ok(())
}

fn insert_note(conn: &Connection, path: &str, content: &str) -> Result<(), rusqlite::Error> {
    conn.execute(
        "INSERT INTO notes (
           vault_path, title, tags, aliases, content, frontmatter,
           mtime_ms, size_bytes, hash, docid, active, scope
         ) VALUES (?1, ?1, '[]', '[]', ?2, '', 0, 0, ?1, ?1, 1, '')",
        params![path, content],
    )?;
    Ok(())
}