remem-ai 0.5.153

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use anyhow::Result;
use rusqlite::{params, Connection};

use super::types::ProjectLeakReport;

pub(super) fn check_project_leak(conn: &Connection) -> Result<ProjectLeakReport> {
    let projects = top_projects(conn)?;
    if projects.len() < 2 {
        return Ok(ProjectLeakReport {
            total_tested: 0,
            total_hits: 0,
            project_hits: 0,
            global_overlay_hits: 0,
            leaked: 0,
            leak_rate: 0.0,
        });
    }

    let mut total_tested = 0;
    let mut total_hits = 0;
    let mut project_hits = 0;
    let mut global_overlay_hits = 0;
    let mut leaked = 0;

    for project in &projects {
        for entity in project_entities(conn, project)? {
            for memory_id in
                crate::retrieval::entity::search_by_entity(conn, &entity, Some(project), 20)?
            {
                let (memory_project, scope): (String, String) = conn.query_row(
                    "SELECT project, scope FROM memories WHERE id = ?1",
                    params![memory_id],
                    |row| Ok((row.get(0)?, row.get(1)?)),
                )?;
                total_hits += 1;
                if scope == "global" {
                    global_overlay_hits += 1;
                } else if !crate::project_id::project_matches(Some(&memory_project), project) {
                    leaked += 1;
                } else {
                    project_hits += 1;
                }
            }
            total_tested += 1;
        }
    }

    let leak_rate = if total_hits > 0 {
        leaked as f64 / total_hits as f64
    } else {
        0.0
    };
    Ok(ProjectLeakReport {
        total_tested,
        total_hits,
        project_hits,
        global_overlay_hits,
        leaked,
        leak_rate,
    })
}

fn top_projects(conn: &Connection) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT project, COUNT(*) as cnt FROM memories
         WHERE status = 'active' AND project != ''
         GROUP BY project ORDER BY cnt DESC LIMIT 5",
    )?;
    let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
    Ok(rows.flatten().collect())
}

fn project_entities(conn: &Connection, project: &str) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT DISTINCT e.canonical_name FROM memory_entities me
         JOIN entities e ON e.id = me.entity_id
         JOIN memories m ON m.id = me.memory_id
         WHERE m.project = ?1 AND m.status = 'active'
         LIMIT 3",
    )?;
    let rows = stmt.query_map(params![project], |row| row.get::<_, String>(0))?;
    Ok(rows.flatten().collect())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn setup_schema(conn: &Connection) -> Result<()> {
        conn.execute_batch(
            "CREATE TABLE memories (
                id INTEGER PRIMARY KEY,
                project TEXT NOT NULL,
                memory_type TEXT NOT NULL DEFAULT 'discovery',
                status TEXT NOT NULL DEFAULT 'active',
                scope TEXT NOT NULL DEFAULT 'project',
                branch TEXT,
                expires_at_epoch INTEGER,
                updated_at_epoch INTEGER NOT NULL DEFAULT 0
            );
            CREATE TABLE entities (
                id INTEGER PRIMARY KEY,
                canonical_name TEXT NOT NULL UNIQUE COLLATE NOCASE
            );
            CREATE TABLE memory_entities (
                memory_id INTEGER NOT NULL,
                entity_id INTEGER NOT NULL,
                UNIQUE(memory_id, entity_id)
            );",
        )?;
        Ok(())
    }

    fn link_entity(conn: &Connection, memory_id: i64, entity_id: i64, name: &str) -> Result<()> {
        conn.execute(
            "INSERT OR IGNORE INTO entities (id, canonical_name) VALUES (?1, ?2)",
            params![entity_id, name],
        )?;
        conn.execute(
            "INSERT INTO memory_entities (memory_id, entity_id) VALUES (?1, ?2)",
            params![memory_id, entity_id],
        )?;
        Ok(())
    }

    #[test]
    fn project_leak_counts_same_project_global_memory_as_overlay() -> Result<()> {
        let conn = Connection::open_in_memory()?;
        setup_schema(&conn)?;
        conn.execute(
            "INSERT INTO memories (id, project, scope, status) VALUES (?1, ?2, ?3, 'active')",
            params![1_i64, "proj-a", "project"],
        )?;
        conn.execute(
            "INSERT INTO memories (id, project, scope, status) VALUES (?1, ?2, ?3, 'active')",
            params![2_i64, "proj-b", "project"],
        )?;
        conn.execute(
            "INSERT INTO memories (id, project, scope, status) VALUES (?1, ?2, ?3, 'active')",
            params![3_i64, "proj-a", "global"],
        )?;
        link_entity(&conn, 1, 1, "SharedEntity")?;
        link_entity(&conn, 2, 2, "OtherEntity")?;
        link_entity(&conn, 3, 1, "SharedEntity")?;

        let report = check_project_leak(&conn)?;

        assert_eq!(report.total_hits, 3);
        assert_eq!(report.project_hits, 2);
        assert_eq!(report.global_overlay_hits, 1);
        assert_eq!(report.leaked, 0);
        Ok(())
    }
}