nornir 0.1.0

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
Documentation
//! Benchmark harness — project-agnostic.
//!
//! Provides a uniform run/result envelope plus an append-only JSONL
//! history. Repo-specific metrics live in [`BenchResult::metrics`] as
//! a free-form JSON object, so one envelope serves any project.

pub mod api;
pub mod assets;
pub mod history;

use serde::{Deserialize, Serialize};

/// A single benchmark result. Repo-specific metrics live in `metrics`
/// so both holger (`holger_ops_sec` / `nexus_ops_sec`) and znippy
/// (`compress_mbs` / `decompress_mbs` / `files`) can share the same
/// run envelope.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchResult {
    pub name: String,
    #[serde(flatten)]
    pub metrics: serde_json::Map<String, serde_json::Value>,
}

/// Pass/fail outcome of a single test that ran alongside the bench.
///
/// `tests` lives next to `results` on a [`BenchRun`] so the no-regression
/// gate can fail a release both for scalar drops *and* for any test
/// flipping red. Optional fields (`duration_ms`, `message`) carry the
/// extra detail when the runner provides it (criterion / `cargo test`
/// JSON output etc.).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestOutcome {
    pub name: String,
    pub passed: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub duration_ms: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

/// One full bench run — one line in `bench_history.jsonl`.
///
/// Backward-compat fields:
/// - `version`, `machine`, `cores` are `#[serde(default)]` so legacy
///   entries (holger 2026-05-27 lines lacking machine/version/cores;
///   znippy lines lacking machine) deserialize cleanly. Going forward
///   all newly written runs must populate them — `history::append`
///   rejects empty `machine`.
/// - `date` (YYYY-MM-DD string) is retained for legacy compatibility.
///   New writers should also populate `timestamp` (RFC 3339, UTC) which
///   becomes the canonical time column in the Iceberg `bench_runs`
///   table once the warehouse phase lands.
/// - `tests` defaults to empty so existing JSONL parses; new runs are
///   expected to populate it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchRun {
    pub date: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timestamp: Option<String>,
    #[serde(default)]
    pub version: String,
    #[serde(default)]
    pub machine: String,
    #[serde(default)]
    pub cores: u32,
    pub results: Vec<BenchResult>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tests: Vec<TestOutcome>,
}

impl BenchRun {
    pub fn find(&self, name: &str) -> Option<&BenchResult> {
        self.results.iter().find(|r| r.name == name)
    }

    /// True iff every test in `tests` passed. An empty `tests` vec is
    /// treated as "no tests run" → returns true.
    pub fn all_tests_passed(&self) -> bool {
        self.tests.iter().all(|t| t.passed)
    }

    /// Convenience: list the names of any failed tests.
    pub fn failed_tests(&self) -> Vec<&str> {
        self.tests
            .iter()
            .filter(|t| !t.passed)
            .map(|t| t.name.as_str())
            .collect()
    }
}