src/docs/mod.rs
//! 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).
//!
//! ## Where the code lives now
//!
//! The warehouse-free ASSEMBLY half — the `nornir:gen:*` marker engine and
//! every section renderer (`sections`), the managed-docs layout (`layout`),
//! the book assembler (`book`), `bench_status`, `test_inventory`, `hooks`,
//! `pages`, the typst `svg` charts, `apidoc` and `ai_context` — lives in the
//! [`nornir_docgen`] edda leaf (which re-exports [`nornir_docbook`]'s render
//! engine as its `export`). This module re-exports it all **byte-for-byte
//! path-unchanged**, exactly like the existing `docs::export` shim, so every
//! `docs::…` call site (the CLI, the MCP server, the release gates, the tests)
//! resolves as before.
//!
//! What genuinely stays here is Mímir: [`warehouse`] (the Iceberg `doc_exports`
//! table), [`search`] (the tantivy docs index) and [`release_book`] — they read
//! the warehouse. The `depgraph` section's network half (clone missing sibling
//! path-dep repos) is injected into [`Ctx`] via
//! [`Ctx::with_path_dep_cloner`] — nornir's callers pass
//! [`crate::security::clone_missing_path_dep_siblings`] through
//! [`crate::security::docs_path_dep_cloner`]; the leaf itself stays
//! network-free.
use std::path::Path;
use anyhow::{Result, anyhow};
use crate::bench::BenchRun;
pub const GENERATED_MARKER: &str = "<!-- nornir:generated -->";
pub use nornir_docgen::sections;
pub use sections::{Ctx, FileReport, assemble_file, check_file};
pub use bench_status::{
HarnessProbe, PREVIEW_TOKEN, is_preview, probe_harness, suspect_reasons, suspects,
};
pub use nornir_docgen::bench_status;
pub use nornir_docgen::test_inventory;
pub use test_inventory::{TestInventory, scan as scan_tests, scan_opts as scan_tests_opts};
pub use hooks::{InstallReport, install_hooks, pre_commit_script};
pub use nornir_docgen::hooks;
pub use layout::{
DocCheckRow, DocsRenderCfg, MANAGED_DOCS, RenderReport, RepoLayout,
check_all as render_check_all, check_all_status, check_doc as render_check_doc, check_outcome,
init_repo,
};
pub use nornir_docgen::layout;
/// The `nornir:claim:` gate — RE-EXPORTED from the engine, not owned here.
///
/// ⚠ MOVED OUT 2026-08-27, and the move is the fix. This module used to own
/// `claims.rs` and wrap six engine functions with it. That gated NORNIR's road
/// and nothing else, and nornir is not the only road: `dwarves` depends on
/// `nornir-docgen` directly and calls [`layout::render_all`],
/// [`layout::render_sources_in_place`], [`book::build_book`],
/// [`book::build_books`] and [`book::build_book_svg_pages`] raw from
/// `dwarves/crates/dwarves/src/docs/mod.rs` — and `gate_doc_claims` had ZERO
/// occurrences in the whole dwarves tree. So the FORGE, the thing that actually
/// burns the books, took the ungated road, and a `dwarves docs render` on a
/// falsified claim was measured writing the artifact with exit 0 and stamping
/// `⚠ GENERATED … DO NOT EDIT` over the contradicted sentence.
///
/// The gate now lives on the engine's own entry points, so BOTH roads pass
/// through one writer, and the six wrappers that used to live here are GONE —
/// a wrapper that adds nothing is a second copy of the rule (L5). Every
/// `docs::render_doc` / `render_all` / `render_sources_in_place` /
/// `build_book*` call site in nornir resolves to the gated engine function by
/// re-export, unchanged.
pub use nornir_docgen::claims;
pub use claims::{Claim, ClaimKind, gate_repo as gate_doc_claims};
pub use layout::{render_all, render_doc, render_sources_in_place};
pub mod warehouse;
pub use warehouse::{
DocExport, DocHistoryRow, ExportFilter, history_outcome as history_export_outcome,
list_doc_exports, list_doc_exports_async, record_doc_export, record_doc_export_async,
};
pub mod search;
pub use search::{
build_docs_index, discover_doc_files, docs_index_dir, restore_docs_index, search_docs,
snapshot_docs_index,
};
pub use nornir_docgen::pages;
pub use pages::{PAGES_BRANCH, PagesBuild, force_push_cmd, index_html, stage_pages_branch};
#[cfg(feature = "docs-export")]
pub mod export;
#[cfg(feature = "docs-export")]
pub use export::{DocFormat, ExportMeta, export as export_bytes, export_repo};
#[cfg(feature = "docs-export")]
pub use nornir_docgen::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 use book::{
BookRoute, BuiltBook, Chapter, classify, collect_chapters, default_routing, load_routing,
resolve_version,
};
#[cfg(feature = "docs-export")]
pub use nornir_docgen::book;
/// The book builders — gated inside the engine now (see the [`claims`] note
/// above), so these are plain re-exports and no longer wrappers. The book binds
/// every `.nornir/*.md` source into one publication, so a false claimed sentence
/// in any of them refuses the whole build.
#[cfg(feature = "docs-export")]
pub use book::{build_book, build_book_svg_pages, build_books};
#[cfg(feature = "docs-export")]
pub use nornir_docgen::apidoc;
#[cfg(feature = "docs-export")]
pub use ai_context::{
AiContextOpts, AiContextOutput, BuildStats, GenMode, build as build_ai_context,
};
#[cfg(feature = "docs-export")]
pub use nornir_docgen::ai_context;
/// 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 — but only when a bench run is available
/// to name the version. A repo with no recorded bench run (e.g. nornir itself,
/// which ships no `nornir-bench.rs`) still passes the gate via its generated
/// README; the bench run is not a docs-freshness prerequisite.
pub fn assemble_and_check(repo_root: &Path, run: Option<&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(());
}
// Hand-written README: assert the headline names the release version, but
// only when a bench run gives us a version to look for. No run ⇒ nothing to
// assert here (freshness of the prose is checked by `check_file` above).
if let Some(run) = run {
let needle = format!("v{}", run.version);
if !text.contains(&needle) {
return Err(anyhow!(
"README.md does not mention {needle} — update headline before release"
));
}
}
Ok(())
}