nornir 0.4.19

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
Documentation
//! Documentation assembly.
//!
//! nornir owns generated docs: README.md, CLAUDE.md, CHANGELOG.md.
//! Files (or sections) carrying the marker `<!-- nornir:generated -->`
//! are rewritten in full on each release from:
//!
//!  1. the per-repo TOML in `workspace_holger/release/`
//!  2. the latest [`BenchRun`]
//!  3. the bench history file
//!
//! Hand-written docs without the marker are only spot-checked by
//! [`assemble_and_check`] (version string + headline must match).

use std::path::Path;

use anyhow::{anyhow, Context, Result};

use crate::bench::BenchRun;

pub const GENERATED_MARKER: &str = "<!-- nornir:generated -->";

pub mod sections;
pub use sections::{assemble_file, check_file, Ctx, FileReport};

pub mod test_inventory;
pub use test_inventory::{scan as scan_tests, scan_opts as scan_tests_opts, TestInventory};

pub mod layout;
pub use layout::{
    check_all as render_check_all, check_doc as render_check_doc, init_repo,
    render_all, render_doc, render_sources_in_place, DocsRenderCfg, RenderReport, RepoLayout,
    MANAGED_DOCS,
};

pub mod warehouse;
pub use warehouse::{
    list_doc_exports, list_doc_exports_async, record_doc_export, record_doc_export_async,
    DocExport, ExportFilter,
};

pub mod search;
pub use search::{
    build_docs_index, discover_doc_files, docs_index_dir, restore_docs_index, search_docs,
    snapshot_docs_index,
};

#[cfg(feature = "docs-export")]
pub mod export;
#[cfg(feature = "docs-export")]
pub use export::{export as export_bytes, export_repo, DocFormat, ExportMeta};

#[cfg(feature = "docs-export")]
pub mod svg;

#[cfg(feature = "docs-export")]
pub mod release_book;
#[cfg(feature = "docs-export")]
pub use release_book::{BookOptions, build_release_book, build_release_book_markdown};

#[cfg(feature = "docs-export")]
pub mod book;
#[cfg(feature = "docs-export")]
pub use book::{build_book, collect_chapters, resolve_version, Chapter};

/// Render the README for `repo_name` from a bench run + description.
pub fn render_readme(repo_name: &str, description: &str, run: &BenchRun) -> String {
    let best = best_metric(run);
    let mut out = String::new();
    out.push_str(&format!("# {repo_name}\n\n"));
    out.push_str(description.trim());
    out.push_str("\n\n");
    out.push_str(&format!(
        "**{best:.0} ops/sec · {cores} cores · v{ver}**\n\n",
        cores = run.cores,
        ver = run.version
    ));
    out.push_str("## Current benchmarks\n\n");
    out.push_str("| name | metric | value |\n|------|--------|-------|\n");
    for r in &run.results {
        for (k, v) in &r.metrics {
            if let Some(f) = v.as_f64() {
                out.push_str(&format!("| {} | {} | {:.2} |\n", r.name, k, f));
            }
        }
    }
    out.push_str("\n");
    out.push_str(GENERATED_MARKER);
    out.push('\n');
    out
}

/// Write `README.md` (full rewrite — only call on files that carry the
/// generated marker, or are managed wholesale by nornir).
pub fn write_readme(repo_root: &Path, body: &str) -> Result<()> {
    let path = repo_root.join("README.md");
    std::fs::write(&path, body).with_context(|| format!("write {}", path.display()))?;
    Ok(())
}

/// Append one CHANGELOG entry.
pub fn append_changelog(repo_root: &Path, version: &str, date: &str, summary: &str) -> Result<()> {
    let path = repo_root.join("CHANGELOG.md");
    let prior = std::fs::read_to_string(&path).unwrap_or_default();
    let entry = format!("## v{version}{date}\n\n{}\n\n", summary.trim());
    let new = if prior.starts_with("# ") {
        let (head, rest) = prior.split_once('\n').unwrap_or((&prior, ""));
        format!("{head}\n\n{entry}{rest}")
    } else {
        format!("# Changelog\n\n{entry}{prior}")
    };
    std::fs::write(&path, new).with_context(|| format!("write {}", path.display()))?;
    Ok(())
}

/// Gate 7: docs are in sync with the run. For generated files, rewrite
/// and verify. For hand-written README.md (no marker), require that
/// `v<version>` appears in the file.
pub fn assemble_and_check(repo_root: &Path, run: &BenchRun) -> Result<()> {
    let readme = repo_root.join("README.md");
    let text = std::fs::read_to_string(&readme).unwrap_or_default();
    if text.contains(GENERATED_MARKER) {
        return Ok(());
    }
    let needle = format!("v{}", run.version);
    if !text.contains(&needle) {
        return Err(anyhow!(
            "README.md does not mention {needle} — update headline before release"
        ));
    }
    Ok(())
}

fn best_metric(run: &BenchRun) -> f64 {
    let mut best = 0.0f64;
    for r in &run.results {
        for (_, v) in &r.metrics {
            if let Some(f) = v.as_f64() {
                if f > best {
                    best = f;
                }
            }
        }
    }
    best
}