frigg 0.9.1

Frigg gives AI agents local, source-backed code search and navigation without sending whole repositories through every prompt.
Documentation
//! Repository and manifest snapshot persistence.
//!
//! Persists repository roots and manifest snapshot rows that anchor retrieval projections,
//! semantic heads, and MCP freshness checks.

use crate::context_efficiency::{
    ManifestMetadataCacheKey, ManifestMetadataSummary, cached_manifest_metadata_summary,
    store_manifest_metadata_summary,
};
use rusqlite::Transaction;

use super::*;

impl Storage {
    pub fn upsert_repository(
        &self,
        repository_id: &str,
        root_path: &std::path::Path,
        display_name: &str,
    ) -> FriggResult<()> {
        let conn = self.open_current_schema_connection()?;
        upsert_repository_on_connection(&conn, repository_id, root_path, display_name)
    }

    pub fn upsert_manifest(
        &self,
        repository_id: &str,
        snapshot_id: &str,
        entries: &[ManifestEntry],
    ) -> FriggResult<()> {
        let mut conn = self.open_current_schema_connection()?;
        upsert_manifest_on_connection(&mut conn, repository_id, snapshot_id, entries)
    }

    pub fn load_manifest_for_snapshot(&self, snapshot_id: &str) -> FriggResult<Vec<ManifestEntry>> {
        let conn = self.open_current_schema_connection()?;
        load_manifest_entries_for_snapshot(&conn, snapshot_id)
    }

    pub fn load_latest_manifest_for_repository(
        &self,
        repository_id: &str,
    ) -> FriggResult<Option<RepositoryManifestSnapshot>> {
        let conn = self.open_current_schema_connection()?;
        load_latest_manifest_snapshot_for_repository(&conn, repository_id)
    }

    pub fn load_latest_manifest_metadata_for_repository(
        &self,
        repository_id: &str,
    ) -> FriggResult<Option<RepositoryManifestMetadataSnapshot>> {
        let conn = self.open_current_schema_connection()?;
        load_latest_manifest_metadata_snapshot_for_repository(&conn, repository_id)
    }

    #[allow(dead_code)]
    pub(crate) fn load_latest_context_efficiency_manifest_summary_for_repository(
        &self,
        repository_id: &str,
    ) -> FriggResult<Option<ManifestMetadataSummary>> {
        let conn = self.open_current_schema_connection()?;
        let Some(snapshot_id) =
            load_latest_manifest_snapshot_id_for_repository(&conn, repository_id)?
        else {
            return Ok(None);
        };
        let cache_key = ManifestMetadataCacheKey::from_storage_path(
            self.db_path(),
            repository_id,
            &snapshot_id,
        );
        if let Some(summary) = cached_manifest_metadata_summary(&cache_key) {
            return Ok(Some(summary));
        }

        let entries = load_manifest_metadata_entries_for_snapshot(&conn, &snapshot_id)?;
        let snapshot = RepositoryManifestMetadataSnapshot {
            repository_id: repository_id.to_owned(),
            snapshot_id,
            entries,
        };
        let summary = ManifestMetadataSummary::from_snapshot(&snapshot);
        store_manifest_metadata_summary(cache_key, summary.clone());
        Ok(Some(summary))
    }

    pub fn delete_snapshot(&self, snapshot_id: &str) -> FriggResult<()> {
        let snapshot_id = snapshot_id.trim();
        if snapshot_id.is_empty() {
            return Err(FriggError::InvalidInput(
                "snapshot_id must not be empty".to_owned(),
            ));
        }

        let mut conn = self.open_current_schema_connection()?;
        let tx = conn.transaction().map_err(|err| {
            FriggError::Internal(format!(
                "failed to start snapshot delete transaction for '{snapshot_id}': {err}"
            ))
        })?;

        delete_snapshot_rows_in_transaction(&tx, snapshot_id)?;

        tx.commit().map_err(|err| {
            FriggError::Internal(format!(
                "failed to commit snapshot delete for '{snapshot_id}': {err}"
            ))
        })?;

        Ok(())
    }
}

impl StorageSession {
    pub(crate) fn upsert_repository(
        &self,
        repository_id: &str,
        root_path: &std::path::Path,
        display_name: &str,
    ) -> FriggResult<()> {
        upsert_repository_on_connection(&self.conn, repository_id, root_path, display_name)
    }

    pub(crate) fn upsert_manifest(
        &mut self,
        repository_id: &str,
        snapshot_id: &str,
        entries: &[ManifestEntry],
    ) -> FriggResult<()> {
        upsert_manifest_on_connection(&mut self.conn, repository_id, snapshot_id, entries)
    }

    pub(crate) fn load_manifest_for_snapshot(
        &self,
        snapshot_id: &str,
    ) -> FriggResult<Vec<ManifestEntry>> {
        load_manifest_entries_for_snapshot(&self.conn, snapshot_id)
    }

    pub(crate) fn load_latest_manifest_for_repository(
        &self,
        repository_id: &str,
    ) -> FriggResult<Option<RepositoryManifestSnapshot>> {
        load_latest_manifest_snapshot_for_repository(&self.conn, repository_id)
    }

    pub(crate) fn delete_snapshot(&mut self, snapshot_id: &str) -> FriggResult<()> {
        let snapshot_id = snapshot_id.trim();
        if snapshot_id.is_empty() {
            return Err(FriggError::InvalidInput(
                "snapshot_id must not be empty".to_owned(),
            ));
        }

        let tx = self.conn.transaction().map_err(|err| {
            FriggError::Internal(format!(
                "failed to start snapshot delete transaction for '{snapshot_id}': {err}"
            ))
        })?;
        delete_snapshot_rows_in_transaction(&tx, snapshot_id)?;
        tx.commit().map_err(|err| {
            FriggError::Internal(format!(
                "failed to commit snapshot delete for '{snapshot_id}': {err}"
            ))
        })?;
        Ok(())
    }
}

fn upsert_repository_on_connection(
    conn: &Connection,
    repository_id: &str,
    root_path: &std::path::Path,
    display_name: &str,
) -> FriggResult<()> {
    let repository_id = repository_id.trim();
    if repository_id.is_empty() {
        return Err(FriggError::InvalidInput(
            "repository_id must not be empty".to_owned(),
        ));
    }

    let display_name = display_name.trim();
    if display_name.is_empty() {
        return Err(FriggError::InvalidInput(
            "display_name must not be empty".to_owned(),
        ));
    }

    conn.execute(
        r#"
        INSERT INTO repository (repository_id, root_path, display_name, created_at)
        VALUES (?1, ?2, ?3, STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now'))
        ON CONFLICT(repository_id) DO UPDATE SET
            root_path = excluded.root_path,
            display_name = excluded.display_name
        "#,
        (
            repository_id,
            root_path.to_string_lossy().into_owned(),
            display_name,
        ),
    )
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to upsert repository metadata for '{repository_id}': {err}"
        ))
    })?;

    Ok(())
}

fn upsert_manifest_on_connection(
    conn: &mut Connection,
    repository_id: &str,
    snapshot_id: &str,
    entries: &[ManifestEntry],
) -> FriggResult<()> {
    let tx = conn.transaction().map_err(|err| {
        FriggError::Internal(format!(
            "failed to start manifest upsert transaction for snapshot '{snapshot_id}': {err}"
        ))
    })?;

    tx.execute(
        r#"
        INSERT OR IGNORE INTO repository (repository_id, root_path, display_name, created_at)
        VALUES (?1, '/legacy-import', ?1, STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now'))
        "#,
        [repository_id],
    )
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to seed repository stub row for snapshot '{snapshot_id}' in repository '{repository_id}': {err}"
        ))
    })?;

    tx.execute(
        r#"
        INSERT INTO snapshot (snapshot_id, repository_id, kind, revision, created_at)
        VALUES (?1, ?2, 'manifest', NULL, STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now'))
        ON CONFLICT(snapshot_id) DO UPDATE SET
            repository_id = excluded.repository_id
        "#,
        [snapshot_id, repository_id],
    )
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to upsert snapshot metadata for '{snapshot_id}': {err}"
        ))
    })?;

    tx.execute(
        "DELETE FROM file_manifest WHERE snapshot_id = ?1",
        [snapshot_id],
    )
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to clear existing manifest rows for snapshot '{snapshot_id}': {err}"
        ))
    })?;

    let mut ordered_entries = entries.to_vec();
    ordered_entries.sort_by(|left, right| left.path.cmp(&right.path));

    let mut insert_stmt = tx
        .prepare(
            r#"
            INSERT INTO file_manifest (snapshot_id, path, sha256, size_bytes, mtime_ns)
            VALUES (?1, ?2, ?3, ?4, ?5)
            "#,
        )
        .map_err(|err| {
            FriggError::Internal(format!(
                "failed to prepare manifest insert statement for snapshot '{snapshot_id}': {err}"
            ))
        })?;

    for entry in ordered_entries {
        insert_stmt
            .execute((
                snapshot_id,
                entry.path,
                entry.sha256,
                u64_to_i64(entry.size_bytes, "size_bytes")?,
                option_u64_to_option_i64(entry.mtime_ns, "mtime_ns")?,
            ))
            .map_err(|err| {
                FriggError::Internal(format!(
                    "failed to insert manifest row for snapshot '{snapshot_id}': {err}"
                ))
            })?;
    }

    drop(insert_stmt);

    tx.commit().map_err(|err| {
        FriggError::Internal(format!(
            "failed to commit manifest upsert for snapshot '{snapshot_id}': {err}"
        ))
    })?;

    Ok(())
}

pub(super) fn delete_snapshot_rows_in_transaction(
    tx: &Transaction<'_>,
    snapshot_id: &str,
) -> FriggResult<()> {
    let active_semantic_heads: i64 = tx
        .query_row(
            "SELECT COUNT(*) FROM semantic_head WHERE covered_snapshot_id = ?1",
            [snapshot_id],
            |row| row.get(0),
        )
        .map_err(|err| {
            FriggError::Internal(format!(
                "failed to query semantic head coverage for snapshot '{snapshot_id}': {err}"
            ))
        })?;
    if active_semantic_heads > 0 {
        return Err(FriggError::InvalidInput(format!(
            "cannot delete snapshot '{snapshot_id}' because it is still covered by the active live semantic corpus; refresh semantics to a newer snapshot or clear the semantic head first"
        )));
    }

    for (table, description) in [
        ("path_witness_projection", "path witness projection rows"),
        ("test_subject_projection", "test subject projection rows"),
        (
            "entrypoint_surface_projection",
            "entrypoint surface projection rows",
        ),
        (
            "retrieval_projection_head",
            "retrieval projection head rows",
        ),
        ("path_relation_projection", "path relation projection rows"),
        (
            "subtree_coverage_projection",
            "subtree coverage projection rows",
        ),
        (
            "path_surface_term_projection",
            "path surface-term projection rows",
        ),
        (
            "path_anchor_sketch_projection",
            "path anchor sketch projection rows",
        ),
        ("file_manifest", "manifest rows"),
    ] {
        tx.execute(
            &format!("DELETE FROM {table} WHERE snapshot_id = ?1"),
            [snapshot_id],
        )
        .map_err(|err| {
            FriggError::Internal(format!(
                "failed to delete {description} for snapshot '{snapshot_id}': {err}"
            ))
        })?;
    }

    tx.execute("DELETE FROM snapshot WHERE snapshot_id = ?1", [snapshot_id])
        .map_err(|err| {
            FriggError::Internal(format!(
                "failed to delete snapshot metadata for '{snapshot_id}': {err}"
            ))
        })?;

    Ok(())
}