frigg 0.4.2

Local-first MCP server for code understanding.
Documentation
pub(super) use std::path::{Path, PathBuf};
pub(super) use std::{env, fs};

pub(super) use super::super::{
    DEFAULT_VECTOR_DIMENSIONS, EntrypointSurfaceProjection, MIGRATIONS, ManifestEntry,
    PROVENANCE_STORAGE_DB_FILE, PROVENANCE_STORAGE_DIR, PathAnchorSketchProjection,
    PathRelationProjection, PathSurfaceTermProjection, PathWitnessProjection,
    RetrievalProjectionBundle, RetrievalProjectionHeadRecord, SQLITE_VEC_REQUIRED_VERSION,
    SemanticChunkEmbeddingRecord, Storage, SubtreeCoverageProjection, TestSubjectProjection,
    VECTOR_TABLE_NAME, encode_f32_vector, ensure_provenance_db_parent_dir,
    ensure_sqlite_vec_pinned_version,
    initialize_vector_store_on_connection_with_detected_capability, open_connection,
    reset_semantic_read_trace, resolve_provenance_db_path, set_schema_version,
    snapshot_semantic_read_trace, table_exists,
    verify_vector_store_on_connection_with_detected_capability,
};
pub(super) use crate::domain::{FriggError, FriggResult, PathClass, SourceClass};
pub(super) use rusqlite::Connection;
pub(super) use serde_json::json;
pub(super) use uuid::Uuid;

pub(super) fn temp_db_path(test_name: &str) -> PathBuf {
    env::temp_dir().join(format!(
        "frigg-storage-{test_name}-{}.sqlite3",
        Uuid::now_v7()
    ))
}

pub(super) fn temp_workspace_root(test_name: &str) -> PathBuf {
    env::temp_dir().join(format!(
        "frigg-storage-workspace-{test_name}-{}",
        Uuid::now_v7()
    ))
}

pub(super) fn open_test_connection(path: &Path) -> FriggResult<Connection> {
    Connection::open(path).map_err(|err| {
        FriggError::Internal(format!(
            "failed to open sqlite db for test assertions: {err}"
        ))
    })
}

pub(super) fn initialize_v3_storage_schema(path: &Path) -> FriggResult<()> {
    let mut conn = open_test_connection(path)?;
    conn.execute_batch(
        r#"
            CREATE TABLE schema_version (
              id INTEGER PRIMARY KEY CHECK (id = 1),
              version INTEGER NOT NULL,
              updated_at TEXT NOT NULL
            );
            "#,
    )
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to create schema_version table for v3 migration test: {err}"
        ))
    })?;

    let tx = conn.transaction().map_err(|err| {
        FriggError::Internal(format!(
            "failed to start v3 migration seed transaction for tests: {err}"
        ))
    })?;
    for migration in MIGRATIONS
        .iter()
        .take_while(|migration| migration.version <= 3)
    {
        tx.execute_batch(migration.sql).map_err(|err| {
            FriggError::Internal(format!(
                "failed to seed migration v{} for v3 migration test: {err}",
                migration.version
            ))
        })?;
    }
    set_schema_version(&tx, 3)?;
    tx.commit().map_err(|err| {
        FriggError::Internal(format!(
            "failed to commit v3 schema seed transaction for tests: {err}"
        ))
    })?;

    Ok(())
}

pub(super) fn count_rows(conn: &Connection, table_name: &str) -> FriggResult<i64> {
    let query = format!("SELECT COUNT(*) FROM {table_name}");
    conn.query_row(&query, [], |row| row.get(0)).map_err(|err| {
        FriggError::Internal(format!(
            "failed to count rows in sqlite table '{table_name}': {err}"
        ))
    })
}

pub(super) fn index_exists(conn: &Connection, index_name: &str) -> FriggResult<bool> {
    conn.query_row(
        "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?1)",
        [index_name],
        |row| row.get::<_, i64>(0),
    )
    .map(|exists| exists != 0)
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to inspect sqlite index '{index_name}': {err}"
        ))
    })
}

pub(super) fn explain_query_plan(conn: &Connection, query: &str) -> FriggResult<Vec<String>> {
    let explain_sql = format!("EXPLAIN QUERY PLAN {query}");
    let mut statement = conn.prepare(&explain_sql).map_err(|err| {
        FriggError::Internal(format!(
            "failed to prepare explain query plan statement: {err}"
        ))
    })?;
    let rows = statement
        .query_map([], |row| row.get::<_, String>(3))
        .map_err(|err| {
            FriggError::Internal(format!(
                "failed to execute explain query plan statement: {err}"
            ))
        })?;
    rows.collect::<Result<Vec<_>, _>>().map_err(|err| {
        FriggError::Internal(format!(
            "failed to decode explain query plan details: {err}"
        ))
    })
}

pub(super) fn cleanup_db(path: &Path) {
    let _ = fs::remove_file(path);
}

pub(super) fn cleanup_workspace(path: &Path) {
    let _ = fs::remove_dir_all(path);
}

#[cfg(unix)]
pub(super) fn create_dir_symlink(target: &Path, link: &Path) -> FriggResult<()> {
    std::os::unix::fs::symlink(target, link).map_err(FriggError::Io)?;
    Ok(())
}

pub(super) fn create_sqlite_vec_like_table(
    conn: &Connection,
    dimensions: usize,
) -> FriggResult<()> {
    conn.execute_batch(&format!(
        "CREATE TABLE {VECTOR_TABLE_NAME} (embedding float[{dimensions}] NOT NULL);"
    ))
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to seed sqlite-vec-like table for transition tests: {err}"
        ))
    })?;

    Ok(())
}

#[allow(clippy::too_many_arguments)]
pub(super) fn semantic_record(
    chunk_id: &str,
    repository_id: &str,
    snapshot_id: &str,
    path: &str,
    language: &str,
    chunk_index: usize,
    start_line: usize,
    end_line: usize,
    provider: &str,
    model: &str,
    trace_id: Option<&str>,
    content_hash_blake3: &str,
    content_text: &str,
    embedding: &[f32],
) -> SemanticChunkEmbeddingRecord {
    SemanticChunkEmbeddingRecord {
        chunk_id: chunk_id.to_owned(),
        repository_id: repository_id.to_owned(),
        snapshot_id: snapshot_id.to_owned(),
        path: path.to_owned(),
        language: language.to_owned(),
        chunk_index,
        start_line,
        end_line,
        provider: provider.to_owned(),
        model: model.to_owned(),
        trace_id: trace_id.map(ToOwned::to_owned),
        content_hash_blake3: content_hash_blake3.to_owned(),
        content_text: content_text.to_owned(),
        embedding: embedding.to_vec(),
    }
}

pub(super) fn replace_semantic_records(
    storage: &Storage,
    repository_id: &str,
    snapshot_id: &str,
    records: &[SemanticChunkEmbeddingRecord],
) -> FriggResult<()> {
    if records.is_empty() {
        return storage.replace_semantic_embeddings_for_repository(
            repository_id,
            snapshot_id,
            "openai",
            "text-embedding-3-small",
            records,
        );
    }

    let mut grouped =
        std::collections::BTreeMap::<(String, String), Vec<SemanticChunkEmbeddingRecord>>::new();
    for record in records {
        grouped
            .entry((record.provider.clone(), record.model.clone()))
            .or_default()
            .push(record.clone());
    }

    for ((provider, model), group) in grouped {
        storage.replace_semantic_embeddings_for_repository(
            repository_id,
            snapshot_id,
            &provider,
            &model,
            &group,
        )?;
    }

    Ok(())
}

pub(super) fn advance_semantic_records(
    storage: &Storage,
    repository_id: &str,
    previous_snapshot_id: Option<&str>,
    snapshot_id: &str,
    changed_paths: &[String],
    deleted_paths: &[String],
    records: &[SemanticChunkEmbeddingRecord],
) -> FriggResult<()> {
    let provider = records
        .first()
        .map(|record| record.provider.as_str())
        .unwrap_or("openai");
    let model = records
        .first()
        .map(|record| record.model.as_str())
        .unwrap_or("text-embedding-3-small");
    storage.advance_semantic_embeddings_for_repository(
        repository_id,
        previous_snapshot_id,
        snapshot_id,
        provider,
        model,
        changed_paths,
        deleted_paths,
        records,
    )
}

pub(super) fn manifest_entry(
    path: &str,
    sha256: &str,
    size_bytes: u64,
    mtime_ns: Option<u64>,
) -> ManifestEntry {
    ManifestEntry {
        path: path.to_owned(),
        sha256: sha256.to_owned(),
        size_bytes,
        mtime_ns,
    }
}

pub(super) fn path_witness_projection_record(
    _repository_id: &str,
    _snapshot_id: &str,
    path: &str,
    path_class: &str,
    source_class: &str,
    path_terms_json: &str,
    flags_json: &str,
) -> PathWitnessProjection {
    PathWitnessProjection {
        path: path.to_owned(),
        path_class: PathClass::from_label(path_class).expect("valid path_class"),
        source_class: SourceClass::from_label(source_class).expect("valid source_class"),
        file_stem: Path::new(path)
            .file_stem()
            .and_then(|stem| stem.to_str())
            .unwrap_or_default()
            .to_ascii_lowercase(),
        path_terms: serde_json::from_str(path_terms_json).expect("valid path terms json"),
        subtree_root: None,
        family_bits: 0,
        flags_json: flags_json.to_owned(),
        heuristic_version: 1,
    }
}

pub(super) fn test_subject_projection_record(
    _repository_id: &str,
    _snapshot_id: &str,
    test_path: &str,
    subject_path: &str,
    shared_terms_json: &str,
    score_hint: usize,
    flags_json: &str,
) -> TestSubjectProjection {
    TestSubjectProjection {
        test_path: test_path.to_owned(),
        subject_path: subject_path.to_owned(),
        shared_terms: serde_json::from_str(shared_terms_json).expect("valid shared terms json"),
        score_hint,
        flags_json: flags_json.to_owned(),
    }
}

#[allow(clippy::too_many_arguments)]
pub(super) fn entrypoint_surface_projection_record(
    _repository_id: &str,
    _snapshot_id: &str,
    path: &str,
    path_class: &str,
    source_class: &str,
    path_terms_json: &str,
    surface_terms_json: &str,
    flags_json: &str,
) -> EntrypointSurfaceProjection {
    EntrypointSurfaceProjection {
        path: path.to_owned(),
        path_class: PathClass::from_label(path_class).expect("valid path_class"),
        source_class: SourceClass::from_label(source_class).expect("valid source_class"),
        path_terms: serde_json::from_str(path_terms_json).expect("valid path terms json"),
        surface_terms: serde_json::from_str(surface_terms_json).expect("valid surface terms json"),
        flags_json: flags_json.to_owned(),
    }
}