nornir 0.4.28

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
//! Warehouse — append-only columnar store backing nornir's bench/symbol
//! history.
//!
//! The canonical backend is [`iceberg::IcebergWarehouse`] (Apache Iceberg via
//! `iceberg-rust` over the single-file **skade-katalog** redb catalogue).
//! Every derived fact is an Iceberg row keyed by git SHA. A `RemoteWarehouse`
//! (Arrow Flight to `nornir-server`, Phase 5) will reuse the same [`Warehouse`]
//! trait surface; the trait is the durable abstraction.

pub mod iceberg;
pub mod iceberg_schema;
pub mod dep_graph;
pub mod funnel;
pub mod release_events;
pub mod test_results;
pub mod viz_actions;
pub mod agent_model_runs;
pub mod codegen_judge;
pub mod blob_store;
/// Generative-LLM abstraction (EPIC #39): ONE `Generator` trait, three real
/// backends (candle / mistralrs / onnx) behind the `generator(spec)` factory,
/// plus `mock` and an off-by-default `ollama` client. Feeds the bake-off.
pub mod generator;

use std::path::Path;

use anyhow::Result;
use uuid::Uuid;

use crate::bench::BenchRun;
use crate::config::Storage;

/// Storage backend for bench/symbol history. Sync API; the async
/// Flight server wraps a sync impl when it ships.
pub trait Warehouse: Send + Sync {
    fn append_bench_run(&self, repo: &str, run: &BenchRun) -> Result<Uuid>;
    fn query_bench_runs(&self, filter: &BenchFilter) -> Result<Vec<BenchRun>>;
}

#[derive(Debug, Default, Clone)]
pub struct BenchFilter {
    pub repo: Option<String>,
    pub machine: Option<String>,
    pub limit: Option<usize>,
}

impl BenchFilter {
    pub fn for_repo(repo: impl Into<String>) -> Self {
        Self { repo: Some(repo.into()), machine: None, limit: None }
    }
}

/// Open the configured warehouse. Returns a boxed trait object so
/// callers don't bake an impl into their signatures. All local storage
/// kinds resolve to the Iceberg backend; only `remote` is distinct.
pub fn open(storage: &Storage, workspace_root: &Path) -> Result<Box<dyn Warehouse>> {
    match storage.kind.as_str() {
        "" | "local" | "iceberg" => {
            let root = warehouse_root(storage, workspace_root);
            Ok(Box::new(iceberg::IcebergWarehouse::open(&root)?))
        }
        "remote" => anyhow::bail!("remote warehouse not yet implemented (Phase 5)"),
        other => anyhow::bail!("unknown storage.kind: {other}"),
    }
}

/// Open the configured warehouse for **read-only** use, tolerating a live
/// `nornir-server` that already holds the exclusive redb lock on
/// `catalog.redb`. When the catalog is locked, this opens a copied-aside
/// read-only snapshot (logged with a WARNING) instead of hard-failing, so a
/// dev-side `nornir` CLI read (the `docs_fresh` gate, docs render, etc.) can
/// never be wedged by the running server. Mutating callers must use
/// [`open`] — the snapshot is read-only by construction.
pub fn open_read_only(storage: &Storage, workspace_root: &Path) -> Result<Box<dyn Warehouse>> {
    match storage.kind.as_str() {
        "" | "local" | "iceberg" => {
            let root = warehouse_root(storage, workspace_root);
            Ok(Box::new(iceberg::IcebergWarehouse::open_read_only(&root)?))
        }
        "remote" => anyhow::bail!("remote warehouse not yet implemented (Phase 5)"),
        other => anyhow::bail!("unknown storage.kind: {other}"),
    }
}

/// Resolve the on-disk warehouse root for a local storage config.
///
/// Precedence (must match `config::Loaded::warehouse_root` and the server's
/// resolution in `bin/nornir-server.rs`):
///   1. explicit `[storage].local_path` → `<workspace_root>/<local_path>/warehouse`
///      (the repo-/workspace-local home; the recommended default for a CLI),
///   2. otherwise the home-derived `<home>/.nornir/warehouse` default
///      (`config::warehouse_default_root`), which the live server also defaults
///      to — a collision risk, hence reads route through [`open_read_only`].
fn warehouse_root(storage: &Storage, workspace_root: &Path) -> std::path::PathBuf {
    if storage.local_path.is_empty() {
        crate::config::warehouse_default_root()
    } else {
        workspace_root.join(&storage.local_path).join("warehouse")
    }
}