code-kb-core 1.0.3

Core library for code-kb AST fact querying, slicing, and progressive disclosure
Documentation
use code_kb_core::{
    Workspace, compute_blast_radius_scoped, find_callee_signatures, find_julie_extract_binary,
    find_references_scoped, get_symbol_by_name, open_read_only, safe_tempdir, scan_workspace,
};
use std::fs;

fn scanned_repo(files: &[(&str, &str)]) -> (tempfile::TempDir, std::path::PathBuf) {
    find_julie_extract_binary().expect("julie-extract binary must be present for tests");
    let temp_dir = safe_tempdir();
    let root = temp_dir.path().to_path_buf();
    for (path, content) in files {
        let full = root.join(path);
        fs::create_dir_all(full.parent().unwrap()).unwrap();
        fs::write(full, content).unwrap();
    }
    let db_path = root.join(".code-kb").join("artifact.db");
    scan_workspace(&Workspace::new(root), &db_path, true).expect("scan failed");
    (temp_dir, db_path)
}

const SAME_NAME_ACROSS_DIRECTORIES: &[(&str, &str)] = &[
    ("pkg/net/run.py", "def run():\n    return 1\n"),
    (
        "pkg/net/client.py",
        "from .run import run\n\ndef client():\n    return run()\n",
    ),
    ("pkg/db/run.py", "def run():\n    return 2\n"),
    (
        "pkg/db/job.py",
        "from .run import run\n\ndef job():\n    return run()\n",
    ),
    ("pkg/db/loose.py", "def loose():\n    return run()\n"),
];

const RECEIVER_TYPES: &[(&str, &str)] = &[(
    "src/a.rs",
    "pub struct A;\nimpl A {\n    pub fn go(&self) -> u8 { 1 }\n}\npub struct B;\nimpl B {\n    pub fn go(&self) -> u8 { 2 }\n}\npub fn use_both(a: &A, b: &B) -> u8 {\n    a.go() + b.go()\n}\n",
)];

fn caller_names(refs: &[code_kb_core::ReferenceSite]) -> Vec<String> {
    let mut names: Vec<String> = refs.iter().map(|r| r.from_symbol_name.clone()).collect();
    names.sort();
    names.dedup();
    names
}

#[test]
fn callers_prefer_the_candidate_in_the_same_directory() {
    let (_repo, db) = scanned_repo(SAME_NAME_ACROSS_DIRECTORIES);
    let conn = open_read_only(&db).unwrap();

    let net =
        find_references_scoped(&conn, "run", "callers", 20, false, Some("pkg/net/run.py")).unwrap();
    let db_ =
        find_references_scoped(&conn, "run", "callers", 20, false, Some("pkg/db/run.py")).unwrap();

    assert_eq!(caller_names(&net), ["client"]);
    assert_eq!(caller_names(&db_), ["job", "loose"]);
}

#[test]
fn callee_signatures_prefer_the_candidate_in_the_same_directory() {
    let (_repo, db) = scanned_repo(SAME_NAME_ACROSS_DIRECTORIES);
    let conn = open_read_only(&db).unwrap();
    let client = get_symbol_by_name(&conn, "client", Some("pkg/net/client.py"))
        .unwrap()
        .unwrap();

    let sigs = find_callee_signatures(&conn, "client", &client.symbol_id, 10, false).unwrap();

    assert!(
        sigs.iter().any(|s| s.contains("pkg/net/run.py")),
        "{sigs:?}"
    );
    assert!(
        !sigs.iter().any(|s| s.contains("pkg/db/run.py")),
        "{sigs:?}"
    );
}

#[test]
fn blast_radius_prefers_the_candidate_in_the_same_directory() {
    let (_repo, db) = scanned_repo(SAME_NAME_ACROSS_DIRECTORIES);
    let conn = open_read_only(&db).unwrap();

    let radius =
        compute_blast_radius_scoped(&conn, &["run"], Some("pkg/net/run.py"), &[], 3, 50).unwrap();

    let mut impacted: Vec<&str> = radius
        .impacted_symbols
        .iter()
        .map(|s| s.name.as_str())
        .collect();
    impacted.sort();
    assert_eq!(impacted, ["client"]);
}

#[test]
fn callers_use_the_receiver_variable_type_to_pick_the_method() {
    let (_repo, db) = scanned_repo(RECEIVER_TYPES);
    let conn = open_read_only(&db).unwrap();

    let a_go = find_references_scoped(&conn, "A::go", "callers", 20, false, None).unwrap();
    let b_go = find_references_scoped(&conn, "B::go", "callers", 20, false, None).unwrap();

    assert_eq!(a_go.len(), 1, "{a_go:?}");
    assert_eq!(a_go[0].from_symbol_name, "use_both");
    assert_eq!(a_go[0].start_line, Some(10));
    assert_eq!(b_go.len(), 1, "{b_go:?}");
    assert_eq!(b_go[0].from_symbol_name, "use_both");
}