crates/nornir-docgen/src/ai_context.rs
//! `nornir docs ai-context` — assemble an AI-native context system for a repo.
//!
//! Two markdown files are produced:
//!
//! * `docs/ai-context.md` — the COMPACT primary context. Read this first.
//! Repository identity, AI usage rules, system summary, crate/layer +
//! binary + feature tables, a four-part dependency view, the runtime-wiring
//! / security / invariant / failure-semantics / change-impact maps, the
//! ecosystem capability matrix, CLI + RON-config + env inventories, the
//! test inventory, the task→file map, incomplete-marker scan, reasoning
//! warnings, and an API summary that links into the full companion.
//! * `docs/ai-context-full.md` — the EXHAUSTIVE companion. Not for every
//! task; consult it for exact signatures, complete rustdoc, private
//! implementation detail, and precise source references.
//!
//! Evidence order (also emitted into the compact pack): executable tests →
//! runtime wiring → implementation → configuration/features → rustdoc → current
//! architecture docs → generated summaries → historical docs → inferred intent.
//! Generated summaries never override active code.
//!
//! Two source layers feed the output:
//!
//! 1. **Derived facts** (deterministic, sorted): crate manifests → features,
//! binaries, layer classification, and the internal dependency graph;
//! environment-variable references; TODO/`unimplemented!`/`#[ignore]`
//! markers; per-crate test inventory; git revision + dirty state.
//! 2. **Maintained metadata** — `.nornir/ai-context.toml`, carrying the facts
//! that cannot be derived from code (purpose, wiring/security/invariant/
//! failure/change-impact maps, capability matrix, CLI + config inventories,
//! document status, per-crate layers, budgets). Every file path and test
//! command it cites is validated at generation time — a dangling reference,
//! an unknown status label, or a status contradiction FAILS generation, so
//! the maintained layer cannot silently drift.
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail};
use serde::Deserialize;
use super::apidoc::{self, ApiCrate, ExtractOpts};
use super::sections::Ctx;
/// The maintained-metadata file, relative to the repo root.
pub const MAINTAINED_METADATA_PATH: &str = ".nornir/ai-context.toml";
/// Status labels an AI agent can trust to mean the same thing everywhere. Any
/// `status = "…"` in the maintained metadata must be one of these (case- and
/// space-insensitive) or generation fails.
pub const ALLOWED_STATUSES: &[&str] = &[
"Verified implementation",
"Verified by test",
"Runtime wired",
"Feature-gated",
"Dev-only",
"Test-only",
"Compiled but not wired",
"Partial implementation",
"Historical documentation",
"Superseded documentation",
"Not implemented",
"Unverified",
"Documentation mismatch",
];
/// Default token budgets (chars/4 heuristic) when the metadata omits them.
const DEFAULT_COMPACT_TOKENS: usize = 30_000;
const DEFAULT_FULL_TOKENS: usize = 120_000;
/// Knobs for [`build`].
#[derive(Debug, Clone)]
pub struct AiContextOpts {
/// Include private (`priv`) items too — default: exported surface only.
pub include_private: bool,
/// Append current-status prose docs to the full pack.
pub include_prose: bool,
/// Per-doc-block truncation (chars; 0 = unlimited).
pub max_doc_chars: usize,
/// `development` | `ci` | `release`. Release/CI fail on a dirty tree unless
/// `allow_dirty` is set; development marks the snapshot non-reproducible.
pub mode: GenMode,
/// Explicit override that permits a dirty-tree release/CI generation.
pub allow_dirty: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GenMode {
Development,
Ci,
Release,
}
impl GenMode {
fn label(self) -> &'static str {
match self {
GenMode::Development => "development",
GenMode::Ci => "ci",
GenMode::Release => "release",
}
}
/// Parse from a string (env/CLI); unknown → development.
pub fn parse(s: &str) -> Self {
match s.trim().to_ascii_lowercase().as_str() {
"release" => GenMode::Release,
"ci" => GenMode::Ci,
_ => GenMode::Development,
}
}
fn reproducible_required(self) -> bool {
matches!(self, GenMode::Ci | GenMode::Release)
}
}
impl Default for AiContextOpts {
fn default() -> Self {
Self {
include_private: false,
include_prose: true,
max_doc_chars: 1200,
mode: GenMode::Development,
allow_dirty: false,
}
}
}
/// Counts for the run summary the CLI prints.
#[derive(Debug, Default, Clone, Copy)]
pub struct BuildStats {
pub crates: usize,
pub modules: usize,
pub items: usize,
pub documented: usize,
pub bytes: usize,
pub full_bytes: usize,
/// Approx tokens (chars/4) of each pack, and their budgets.
pub compact_tokens: usize,
pub full_tokens: usize,
pub compact_budget: usize,
pub full_budget: usize,
pub over_budget: bool,
}
/// The multi-file result of a build.
#[derive(Debug)]
pub struct AiContextOutput {
pub compact: String,
pub full: String,
pub stats: BuildStats,
/// Human-readable warnings (budget overruns, undocumented env vars, …).
pub warnings: Vec<String>,
}
// ═══ Maintained metadata (`.nornir/ai-context.toml`) ═════════════════════════
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Maintained {
#[serde(default)]
pub repository: MetaRepository,
#[serde(default)]
pub output: MetaOutput,
#[serde(default)]
pub invariants: Vec<MetaInvariant>,
#[serde(default)]
pub tasks: Vec<MetaTask>,
#[serde(default)]
pub wiring: Vec<MetaWiring>,
#[serde(default)]
pub security: Vec<MetaSecurity>,
#[serde(default)]
pub warnings: Vec<String>,
#[serde(default)]
pub crates: BTreeMap<String, MetaCrate>,
#[serde(default)]
pub documents: Vec<MetaDocument>,
#[serde(default)]
pub capabilities: Vec<MetaCapability>,
#[serde(default)]
pub change_impact: Vec<MetaChangeImpact>,
#[serde(default)]
pub failures: Vec<MetaFailure>,
#[serde(default)]
pub cli: Vec<MetaCli>,
#[serde(default)]
pub config: Vec<MetaConfig>,
#[serde(default)]
pub precedence: Vec<String>,
#[serde(default)]
pub hotspots: Vec<MetaHotspot>,
#[serde(default)]
pub env: Vec<MetaEnv>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaRepository {
#[serde(default)]
pub purpose: String,
#[serde(default)]
pub role: String,
#[serde(default)]
pub non_goals: Vec<String>,
#[serde(default)]
pub system_summary: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaOutput {
#[serde(default)]
pub compact: MetaBudget,
#[serde(default)]
pub full: MetaBudget,
}
impl Default for MetaOutput {
fn default() -> Self {
Self {
compact: MetaBudget {
max_tokens: DEFAULT_COMPACT_TOKENS,
},
full: MetaBudget {
max_tokens: DEFAULT_FULL_TOKENS,
},
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaBudget {
#[serde(default)]
pub max_tokens: usize,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaInvariant {
pub id: String,
pub statement: String,
// `enforced` is the preferred key; `implementation` kept as an alias.
#[serde(default, alias = "implementation")]
pub enforced: Vec<String>,
#[serde(default)]
pub tests: Vec<String>,
#[serde(default)]
pub break_risk: String,
#[serde(default)]
pub status: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaTask {
pub name: String,
#[serde(default)]
pub start: Vec<String>,
#[serde(default)]
pub related: Vec<String>,
#[serde(default)]
pub invariants: Vec<String>,
/// Legacy single test list (rendered as "fast" when fast_tests is empty).
#[serde(default)]
pub tests: Vec<String>,
#[serde(default)]
pub fast_tests: Vec<String>,
#[serde(default)]
pub full_tests: Vec<String>,
#[serde(default)]
pub heavy_tests: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaWiring {
pub capability: String,
#[serde(default)]
pub interface: String,
#[serde(default)]
pub implementation: String,
#[serde(default)]
pub registration: String,
#[serde(default)]
pub configuration: String,
#[serde(default)]
pub tests: String,
#[serde(default)]
pub status: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaSecurity {
pub concern: String,
#[serde(default)]
pub invariant: String,
#[serde(default)]
pub implementation: Vec<String>,
#[serde(default)]
pub tests: Vec<String>,
#[serde(default)]
pub consequence: String,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaCrate {
#[serde(default)]
pub layer: String,
#[serde(default)]
pub note: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaDocument {
pub path: String,
pub status: String,
#[serde(default)]
pub include_compact: bool,
#[serde(default)]
pub include_full: bool,
#[serde(default)]
pub reason: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaCapability {
pub ecosystem: String,
#[serde(default)]
pub protocol_fetch: String,
#[serde(default)]
pub protocol_upload: String,
#[serde(default)]
pub generic_fetch: String,
#[serde(default)]
pub browse: String,
#[serde(default)]
pub native_index: String,
#[serde(default)]
pub writable_backend: String,
#[serde(default)]
pub znippy_backend: String,
#[serde(default)]
pub proxy_upstream: String,
#[serde(default)]
pub limitation: String,
#[serde(default)]
pub evidence: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaChangeImpact {
pub interface: String,
#[serde(default)]
pub defined_in: String,
#[serde(default)]
pub implementers: String,
#[serde(default)]
pub consumers: String,
#[serde(default)]
pub significance: String,
#[serde(default)]
pub tests: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaFailure {
pub operation: String,
#[serde(default)]
pub fatal: String,
#[serde(default)]
pub partial: String,
#[serde(default)]
pub retry: String,
#[serde(default)]
pub cleanup: String,
#[serde(default)]
pub observable: String,
#[serde(default)]
pub evidence: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaCli {
pub binary: String,
pub command: String,
#[serde(default)]
pub flags: String,
#[serde(default)]
pub calls: String,
#[serde(default)]
pub restrictions: String,
#[serde(default)]
pub tests: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaConfig {
pub field: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub default: String,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub sensitive: bool,
#[serde(default)]
pub consumer: String,
#[serde(default)]
pub restrictions: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaHotspot {
pub dependency: String,
#[serde(default)]
pub used_by: String,
#[serde(default)]
pub effect: String,
#[serde(default)]
pub tier_impact: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaEnv {
pub name: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub default: String,
#[serde(default)]
pub sensitive: bool,
/// `runtime` | `build` | `test` | `bench` | `dev`.
#[serde(default)]
pub phase: String,
#[serde(default)]
pub effect: String,
#[serde(default)]
pub evidence: Vec<String>,
}
/// Load `.nornir/ai-context.toml` when present. Parse errors, dangling file
/// references, unknown status labels, and status contradictions are HARD
/// errors — maintained metadata must never drift.
pub fn load_maintained(
repo_root: &Path,
workspace_crates: &BTreeSet<String>,
) -> Result<Option<Maintained>> {
let path = repo_root.join(MAINTAINED_METADATA_PATH);
if !path.is_file() {
return Ok(None);
}
let txt = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
let m: Maintained =
toml::from_str(&txt).with_context(|| format!("parse {}", path.display()))?;
let mut problems = Vec::new();
// 1. Every file-ish reference must exist; every cargo-test command must
// name a real workspace crate.
let check = |refs: &[String], ctx: &str, problems: &mut Vec<String>| {
for r in refs {
if let Some(p) = path_of_reference(r) {
if !repo_root.join(&p).exists() {
problems.push(format!("dangling path — {ctx}: {r}"));
}
} else if let Some(krate) = cargo_test_crate(r) {
if !workspace_crates.contains(&krate) {
problems.push(format!(
"test command names unknown crate `{krate}` — {ctx}: {r}"
));
}
}
}
};
let one = |r: &str, ctx: &str, problems: &mut Vec<String>| {
if let Some(p) = path_of_reference(r) {
if !repo_root.join(&p).exists() {
problems.push(format!("dangling path — {ctx}: {r}"));
}
}
};
for inv in &m.invariants {
check(
&inv.enforced,
&format!("invariant `{}` enforced", inv.id),
&mut problems,
);
check(
&inv.tests,
&format!("invariant `{}` tests", inv.id),
&mut problems,
);
}
for t in &m.tasks {
check(&t.start, &format!("task `{}` start", t.name), &mut problems);
check(
&t.related,
&format!("task `{}` related", t.name),
&mut problems,
);
check(&t.tests, &format!("task `{}` tests", t.name), &mut problems);
check(
&t.fast_tests,
&format!("task `{}` fast_tests", t.name),
&mut problems,
);
check(
&t.full_tests,
&format!("task `{}` full_tests", t.name),
&mut problems,
);
check(
&t.heavy_tests,
&format!("task `{}` heavy_tests", t.name),
&mut problems,
);
}
for s in &m.security {
check(
&s.implementation,
&format!("security `{}` implementation", s.concern),
&mut problems,
);
check(
&s.tests,
&format!("security `{}` tests", s.concern),
&mut problems,
);
}
for w in &m.wiring {
one(
&w.implementation,
&format!("wiring `{}` implementation", w.capability),
&mut problems,
);
one(
&w.registration,
&format!("wiring `{}` registration", w.capability),
&mut problems,
);
one(
&w.tests,
&format!("wiring `{}` tests", w.capability),
&mut problems,
);
}
for c in &m.change_impact {
one(
&c.defined_in,
&format!("change_impact `{}` defined_in", c.interface),
&mut problems,
);
check(
&c.tests,
&format!("change_impact `{}` tests", c.interface),
&mut problems,
);
}
for f in &m.failures {
check(
&f.evidence,
&format!("failure `{}` evidence", f.operation),
&mut problems,
);
}
for c in &m.cli {
check(
&c.tests,
&format!("cli `{} {}` tests", c.binary, c.command),
&mut problems,
);
}
for cap in &m.capabilities {
check(
&cap.evidence,
&format!("capability `{}` evidence", cap.ecosystem),
&mut problems,
);
}
for e in &m.env {
check(
&e.evidence,
&format!("env `{}` evidence", e.name),
&mut problems,
);
}
for d in &m.documents {
one(&d.path, &format!("document `{}`", d.path), &mut problems);
}
// 2. Every status label must be from the allowed set.
let check_status = |s: &str, ctx: &str, problems: &mut Vec<String>| {
if !s.is_empty() && normalize_status(s).is_none() {
problems.push(format!(
"unknown status `{s}` — {ctx} (allowed: {})",
ALLOWED_STATUSES.join(", ")
));
}
};
for w in &m.wiring {
check_status(
&w.status,
&format!("wiring `{}`", w.capability),
&mut problems,
);
}
for inv in &m.invariants {
check_status(
&inv.status,
&format!("invariant `{}`", inv.id),
&mut problems,
);
}
for d in &m.documents {
check_status(&d.status, &format!("document `{}`", d.path), &mut problems);
}
// 3. Status contradictions: a capability marked absent in one place but live
// in another.
problems.extend(status_contradictions(&m));
if !problems.is_empty() {
bail!(
"{}: {} problem(s) — fix the metadata:\n {}",
MAINTAINED_METADATA_PATH,
problems.len(),
problems.join("\n ")
);
}
Ok(Some(m))
}
/// A capability whose status labels conflict in polarity across sections.
fn status_contradictions(m: &Maintained) -> Vec<String> {
let mut seen: BTreeMap<String, BTreeSet<&'static str>> = BTreeMap::new();
let note = |cap: &str, status: &str, seen: &mut BTreeMap<String, BTreeSet<&'static str>>| {
if let Some(pol) = status_polarity(status) {
seen.entry(cap_key(cap)).or_default().insert(pol);
}
};
for w in &m.wiring {
note(&w.capability, &w.status, &mut seen);
}
for inv in &m.invariants {
note(&inv.id, &inv.status, &mut seen);
}
seen.into_iter()
.filter(|(_, pols)| pols.contains("live") && pols.contains("absent"))
.map(|(cap, _)| {
format!("status contradiction — capability `{cap}` is labelled both live (wired/verified) and absent (not implemented/superseded)")
})
.collect()
}
/// "live" for wired/verified/partial, "absent" for not-implemented/superseded/
/// historical/not-wired, None for neutral (unverified/dev/test/feature-gated).
fn status_polarity(status: &str) -> Option<&'static str> {
match normalize_status(status)? {
"Verified implementation"
| "Verified by test"
| "Runtime wired"
| "Partial implementation" => Some("live"),
"Not implemented"
| "Superseded documentation"
| "Historical documentation"
| "Compiled but not wired" => Some("absent"),
_ => None,
}
}
/// Canonicalize a status to its controlled label. A status is `<label>` or
/// `<label><sep><annotation>` (sep = one of `;:,(` or ` — `); the label is the
/// controlled, validated part and an optional trailing annotation is free text.
/// Returns the canonical label, or `None` if the string doesn't START with one.
fn normalize_status(s: &str) -> Option<&'static str> {
leading_status(s).map(|(label, _)| label)
}
/// Split a status into its controlled `(label, annotation)`. Case-insensitive on
/// the label; matches the longest allowed label that is a prefix at a word
/// boundary. `annotation` is the trimmed remainder (may be empty).
fn leading_status(s: &str) -> Option<(&'static str, String)> {
let t = s.trim();
let lower = t.to_ascii_lowercase();
ALLOWED_STATUSES
.iter()
.copied()
.filter(|a| {
let al = a.to_ascii_lowercase();
lower.starts_with(&al)
&& lower[al.len()..]
.chars()
.next()
.map(|c| !c.is_ascii_alphanumeric())
.unwrap_or(true)
})
.max_by_key(|a| a.len())
.map(|label| {
let rest = t[label.len()..]
.trim_start_matches([';', ':', ',', '(', ' ', '—', '-'])
.trim();
(label, rest.to_string())
})
}
fn cap_key(s: &str) -> String {
s.trim().to_ascii_lowercase()
}
/// The validatable path inside a metadata reference, when it looks like one.
/// `server/lib/src/auth.rs:445` → `server/lib/src/auth.rs`. Free text (spaces),
/// commands (`cargo test …`), anchors and URLs return `None`.
fn path_of_reference(r: &str) -> Option<String> {
let r = r.trim();
if r.is_empty() || r.contains(' ') || r.starts_with("http") || !r.contains('/') {
return None;
}
let base = r.split('#').next().unwrap_or(r);
let base = match base.rsplit_once(':') {
Some((head, tail))
if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit() || c == '-') =>
{
head
}
_ => base,
};
Some(base.to_string())
}
/// If `r` is a `cargo test -p <crate> …` command, return `<crate>`.
fn cargo_test_crate(r: &str) -> Option<String> {
let r = r.trim();
if !r.starts_with("cargo ") {
return None;
}
let toks: Vec<&str> = r.split_whitespace().collect();
toks.iter()
.position(|t| *t == "-p")
.and_then(|i| toks.get(i + 1))
.map(|s| s.to_string())
}
// ═══ Derived facts ═══════════════════════════════════════════════════════════
#[derive(Debug, Default)]
struct CrateMeta {
rel_dir: String,
features: BTreeMap<String, Vec<String>>,
bins: Vec<BinMeta>,
has_lib: bool,
/// Workspace-internal dependency edges.
deps: Vec<DepEdge>,
}
#[derive(Debug, Default)]
struct BinMeta {
name: String,
required_features: Vec<String>,
}
#[derive(Debug, Clone)]
struct DepEdge {
to: String,
kind: DepKind,
optional: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum DepKind {
Runtime,
Dev,
Build,
}
impl DepKind {
fn label(self) -> &'static str {
match self {
DepKind::Runtime => "runtime",
DepKind::Dev => "dev-only",
DepKind::Build => "build-only",
}
}
}
/// Parse features + `[[bin]]` + internal dependency edges out of every manifest.
fn scan_manifests(repo_root: &Path) -> BTreeMap<String, CrateMeta> {
let manifests = apidoc::discover_manifests(repo_root);
let mut parsed: Vec<(String, PathBuf, toml::Table)> = Vec::new();
for manifest in &manifests {
let Ok(txt) = std::fs::read_to_string(manifest) else {
continue;
};
let Ok(doc) = txt.parse::<toml::Table>() else {
continue;
};
let Some(name) = doc
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
else {
continue;
};
parsed.push((name.to_string(), manifest.clone(), doc));
}
let names: BTreeSet<String> = parsed.iter().map(|(n, _, _)| n.clone()).collect();
let mut out = BTreeMap::new();
for (name, manifest, doc) in parsed {
let dir = manifest.parent().unwrap_or(repo_root);
let rel_dir = dir
.strip_prefix(repo_root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let mut meta = CrateMeta {
rel_dir: if rel_dir.is_empty() {
".".into()
} else {
rel_dir
},
has_lib: dir.join("src/lib.rs").is_file() || doc.contains_key("lib"),
..Default::default()
};
if let Some(features) = doc.get("features").and_then(|f| f.as_table()) {
for (k, v) in features {
let deps = v
.as_array()
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
meta.features.insert(k.clone(), deps);
}
}
if let Some(bins) = doc.get("bin").and_then(|b| b.as_array()) {
for b in bins {
let Some(bname) = b.get("name").and_then(|n| n.as_str()) else {
continue;
};
let req = b
.get("required-features")
.and_then(|r| r.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
meta.bins.push(BinMeta {
name: bname.to_string(),
required_features: req,
});
}
}
if dir.join("src/main.rs").is_file() && !meta.bins.iter().any(|b| b.name == name) {
meta.bins.push(BinMeta {
name: name.clone(),
required_features: Vec::new(),
});
}
meta.bins.sort_by(|a, b| a.name.cmp(&b.name));
let mut edges: BTreeMap<(String, DepKind), bool> = BTreeMap::new();
collect_deps(&doc, &names, &name, &mut edges);
if let Some(targets) = doc.get("target").and_then(|t| t.as_table()) {
for (_, tv) in targets {
if let Some(tt) = tv.as_table() {
collect_deps(tt, &names, &name, &mut edges);
}
}
}
// Collapse: a runtime edge supersedes a dev/build edge to the same crate.
let runtime: BTreeSet<String> = edges
.keys()
.filter(|(_, k)| *k == DepKind::Runtime)
.map(|(c, _)| c.clone())
.collect();
edges.retain(|(c, k), _| *k == DepKind::Runtime || !runtime.contains(c));
for ((to, kind), optional) in edges {
meta.deps.push(DepEdge { to, kind, optional });
}
meta.deps
.sort_by(|a, b| (&a.to, a.kind as u8).cmp(&(&b.to, b.kind as u8)));
out.insert(name, meta);
}
out
}
/// Pull `[dependencies]`/`[dev-dependencies]`/`[build-dependencies]` edges that
/// point at another workspace crate, deduplicated on (crate, kind).
fn collect_deps(
table: &toml::Table,
names: &BTreeSet<String>,
self_name: &str,
edges: &mut BTreeMap<(String, DepKind), bool>,
) {
for (section, kind) in [
("dependencies", DepKind::Runtime),
("dev-dependencies", DepKind::Dev),
("build-dependencies", DepKind::Build),
] {
let Some(deps) = table.get(section).and_then(|d| d.as_table()) else {
continue;
};
for (dep_key, spec) in deps {
let real = spec
.as_table()
.and_then(|t| t.get("package"))
.and_then(|p| p.as_str())
.unwrap_or(dep_key.as_str());
if real == self_name || !names.contains(real) {
continue;
}
let optional = spec
.as_table()
.and_then(|t| t.get("optional"))
.and_then(|o| o.as_bool())
.unwrap_or(false);
let e = edges.entry((real.to_string(), kind)).or_insert(optional);
*e = *e || optional;
}
}
}
fn layer_of(name: &str, meta: &CrateMeta, maintained: Option<&Maintained>) -> String {
if let Some(m) = maintained {
if let Some(c) = m.crates.get(name) {
if !c.layer.is_empty() {
return c.layer.clone();
}
}
}
let n = name.to_ascii_lowercase();
let d = meta.rel_dir.to_ascii_lowercase();
let has = |s: &str| n.contains(s) || d.contains(s);
if has("mannequin") || has("mock") || n.contains("test") {
"test-infra".into()
} else if has("bench") {
"benchmarks".into()
} else if has("xtask") {
"dev-tooling".into()
} else if has("connector") {
"connectors".into()
} else if has("repository") || has("storage") || has("backend") {
"storage-backends".into()
} else if has("ui") || has("embed") || has("demo") {
"ui".into()
} else if has("airgap") || has("dist") {
"airgap".into()
} else if n.ends_with("-cli") || (!meta.bins.is_empty() && !meta.has_lib) {
"cli".into()
} else if has("traits") || has("common") || has("core") || has("types") {
"contracts".into()
} else {
"library".into()
}
}
fn scan_env_vars(repo_root: &Path) -> BTreeMap<String, BTreeSet<String>> {
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (rel, txt) in rust_sources(repo_root) {
for pat in ["env::var(\"", "env::var_os(\"", "option_env!(\"", "env!(\""] {
let mut rest = txt.as_str();
while let Some(i) = rest.find(pat) {
rest = &rest[i + pat.len()..];
if let Some(end) = rest.find('"') {
let var = &rest[..end];
let ok = !var.is_empty()
&& var.len() < 64
&& var
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_');
let build_noise =
var == "CARGO" || var == "OUT_DIR" || var.starts_with("CARGO_");
if ok && !build_noise {
out.entry(var.to_string()).or_default().insert(rel.clone());
}
rest = &rest[end..];
}
}
}
}
out
}
#[derive(Debug)]
struct TodoHit {
file: String,
line: usize,
kind: &'static str,
text: String,
}
fn scan_todo_markers(repo_root: &Path) -> Vec<TodoHit> {
const KINDS: [(&str, &str); 5] = [
("unimplemented!", "unimplemented"),
("todo!(", "todo-macro"),
("#[ignore", "ignored-test"),
("FIXME", "FIXME"),
("TODO", "TODO"),
];
let mut out = Vec::new();
for (rel, txt) in rust_sources(repo_root) {
for (ln, line) in txt.lines().enumerate() {
for (needle, kind) in KINDS {
if line.contains(needle) {
let text = line.trim();
let text = if text.len() > 120 { &text[..120] } else { text };
out.push(TodoHit {
file: rel.clone(),
line: ln + 1,
kind,
text: text.to_string(),
});
break;
}
}
}
}
out.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
out
}
#[derive(Debug, Default)]
struct TestMeta {
integration_files: Vec<String>,
test_fns: usize,
ignored: usize,
}
fn scan_tests(
repo_root: &Path,
manifests: &BTreeMap<String, CrateMeta>,
) -> BTreeMap<String, TestMeta> {
let mut out = BTreeMap::new();
for (name, meta) in manifests {
let dir = if meta.rel_dir == "." {
repo_root.to_path_buf()
} else {
repo_root.join(&meta.rel_dir)
};
let mut tm = TestMeta::default();
let tests_dir = dir.join("tests");
if tests_dir.is_dir() {
let mut files: Vec<_> = std::fs::read_dir(&tests_dir)
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "rs"))
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
files.sort();
tm.integration_files = files;
}
let sub_dirs: Vec<PathBuf> = manifests
.values()
.filter(|m| {
m.rel_dir != meta.rel_dir
&& m.rel_dir != "."
&& repo_root.join(&m.rel_dir).starts_with(&dir)
})
.map(|m| repo_root.join(&m.rel_dir))
.collect();
for (rel, txt) in rust_sources(&dir) {
let abs = dir.join(&rel);
if sub_dirs.iter().any(|s| abs.starts_with(s)) {
continue;
}
tm.test_fns += txt.matches("#[test]").count() + txt.matches("#[tokio::test").count();
tm.ignored += txt.matches("#[ignore").count();
}
out.insert(name.clone(), tm);
}
out
}
fn rust_sources(root: &Path) -> Vec<(String, String)> {
let mut files = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&dir) else {
continue;
};
for e in rd.flatten() {
let p = e.path();
let name = e.file_name().to_string_lossy().to_string();
if p.is_dir() {
if !matches!(
name.as_str(),
"target" | ".git" | "node_modules" | "vendor" | "dist" | ".venv"
) {
stack.push(p);
}
} else if p.extension().is_some_and(|x| x == "rs") {
if let Ok(txt) = std::fs::read_to_string(&p) {
let rel = p
.strip_prefix(root)
.unwrap_or(&p)
.to_string_lossy()
.to_string();
files.push((rel, txt));
}
}
}
}
files.sort_by(|a, b| a.0.cmp(&b.0));
files
}
fn git_identity(repo_root: &Path) -> Option<(String, bool)> {
// Pure-gix (via the `nornir-git` leaf): short HEAD sha + dirty flag, no
// `git rev-parse` / `git status` subprocess.
let sha = nornir_git::gitops::short_head_sha(repo_root).ok()?;
let dirty = nornir_git::gitops::is_dirty(repo_root).unwrap_or(false);
Some((sha, dirty))
}
// ═══ Build ═══════════════════════════════════════════════════════════════════
/// Assemble the AI-context system for `repo_root`.
pub fn build(repo_root: &Path, ctx: &Ctx, opts: &AiContextOpts) -> Result<AiContextOutput> {
let crates = apidoc::extract(
repo_root,
&ExtractOpts {
include_private: opts.include_private,
max_doc_chars: opts.max_doc_chars,
},
)?;
let manifests = scan_manifests(repo_root);
let workspace_crates: BTreeSet<String> = manifests.keys().cloned().collect();
let maintained = load_maintained(repo_root, &workspace_crates)?;
let env_vars = scan_env_vars(repo_root);
let todos = scan_todo_markers(repo_root);
let tests = scan_tests(repo_root, &manifests);
let git = git_identity(repo_root);
let dirty = git.as_ref().map(|(_, d)| *d).unwrap_or(false);
if opts.mode.reproducible_required() && dirty && !opts.allow_dirty {
bail!(
"refusing {} generation against a dirty working tree (snapshot would be \
non-reproducible); commit first or pass --allow-dirty",
opts.mode.label()
);
}
let repo_name = repo_root
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "repo".into());
let mut stats = BuildStats::default();
for c in &crates {
stats.crates += 1;
for m in &c.modules {
if m.items.is_empty() && m.doc.is_empty() {
continue;
}
stats.modules += 1;
for it in &m.items {
stats.items += 1;
if !it.doc.is_empty() {
stats.documented += 1;
}
}
}
}
let budgets = maintained.as_ref().map(|m| &m.output);
stats.compact_budget = budgets
.map(|b| b.compact.max_tokens)
.filter(|&t| t > 0)
.unwrap_or(DEFAULT_COMPACT_TOKENS);
stats.full_budget = budgets
.map(|b| b.full.max_tokens)
.filter(|&t| t > 0)
.unwrap_or(DEFAULT_FULL_TOKENS);
let mut warnings = Vec::new();
// ══ COMPACT ══
let mut out = String::new();
out.push_str(&format!("# {repo_name} — AI context pack (compact)\n\n"));
emit_banner_compact(&mut out, &repo_name, opts.mode, git.as_ref());
emit_snapshot(&mut out, opts.mode, git.as_ref(), maintained.is_some());
emit_identity(
&mut out,
&repo_name,
&crates,
&manifests,
maintained.as_ref(),
);
emit_usage_rules(&mut out);
emit_system_summary(&mut out, &crates, maintained.as_ref());
emit_crates_table(&mut out, &crates, &manifests, maintained.as_ref());
emit_binaries(&mut out, &manifests);
emit_workspace_depgraph(&mut out, &manifests);
emit_hotspots(&mut out, maintained.as_ref());
emit_high_coupling(&mut out, &manifests);
emit_features(&mut out, &manifests);
emit_wiring(&mut out, maintained.as_ref());
emit_capabilities(&mut out, maintained.as_ref());
emit_change_impact(&mut out, maintained.as_ref());
emit_failures(&mut out, maintained.as_ref());
emit_security(&mut out, maintained.as_ref());
emit_invariants(&mut out, maintained.as_ref());
emit_cli(&mut out, maintained.as_ref());
emit_config(&mut out, maintained.as_ref());
emit_precedence(&mut out, maintained.as_ref());
emit_env_vars(&mut out, &env_vars, maintained.as_ref(), &mut warnings);
emit_tests(&mut out, &tests);
emit_tasks(&mut out, maintained.as_ref());
emit_todos(&mut out, &todos);
emit_status_legend(&mut out);
emit_warnings(&mut out, maintained.as_ref());
emit_documents(&mut out, maintained.as_ref());
emit_api_summary(&mut out, &crates);
emit_final_index(
&mut out,
&crates,
&manifests,
maintained.as_ref(),
&env_vars,
);
// ══ FULL ══
let mut full = String::new();
full.push_str(&format!(
"# {repo_name} — AI context pack (full API reference)\n\n"
));
emit_banner_full(&mut full, &repo_name, opts.mode, git.as_ref());
emit_snapshot(&mut full, opts.mode, git.as_ref(), maintained.is_some());
emit_full_backlinks(&mut full);
let anchors = emit_api_reference(&mut full, &crates, repo_root);
if opts.include_prose {
emit_prose_current(&mut full, repo_root, ctx, maintained.as_ref());
}
// Validate cross-file anchors.
for c in &crates {
let slug = format!("crate-{}", slugify(&c.name));
if out.contains(&format!("ai-context-full.md#{slug}")) && !anchors.contains(&slug) {
bail!("broken cross-file anchor: compact links #{slug} but full has no such heading");
}
}
stats.bytes = out.len();
stats.full_bytes = full.len();
stats.compact_tokens = approx_tokens(&out);
stats.full_tokens = approx_tokens(&full);
if stats.compact_tokens > stats.compact_budget {
stats.over_budget = true;
warnings.push(format!(
"compact pack ~{} tokens exceeds budget {} — trim duplicate prose / external deps / trivial APIs (never wiring/security/failure/task/capability/invariant content)",
stats.compact_tokens, stats.compact_budget
));
}
if stats.full_tokens > stats.full_budget {
stats.over_budget = true;
warnings.push(format!(
"full pack ~{} tokens exceeds budget {} — reduce rustdoc repetition / generated boilerplate",
stats.full_tokens, stats.full_budget
));
}
Ok(AiContextOutput {
compact: out,
full,
stats,
warnings,
})
}
fn approx_tokens(s: &str) -> usize {
s.chars().count().div_ceil(4)
}
// ─── Banners + snapshot ──────────────────────────────────────────────────────
fn source_line(git: Option<&(String, bool)>) -> String {
match git {
Some((sha, dirty)) => {
format!(
"source {}{}",
sha,
if *dirty {
" (dirty tree — non-reproducible)"
} else {
""
}
)
}
None => "source unknown".into(),
}
}
fn emit_banner_compact(
out: &mut String,
repo_name: &str,
mode: GenMode,
git: Option<&(String, bool)>,
) {
out.push_str(&format!(
"> **GENERATED — do not edit; regenerate with `nornir docs ai-context {repo}` (nornir {ver}).**\n>\n\
> This is the **compact primary context — read this file first.** It carries everything an \
AI coding agent needs to place and make a change safely: architecture, runtime wiring, \
capability matrix, failure semantics, invariants, and the task→file map.\n>\n\
> Consult **`docs/ai-context-full.md`** (the exhaustive companion) ONLY when you need exact \
API signatures, complete rustdoc, private implementation detail, or precise source \
references. Do not load it for routine reasoning.\n\n\
_Generated {date} · {src} · mode {mode}._\n\n",
repo = repo_name,
ver = env!("CARGO_PKG_VERSION"),
date = chrono::Utc::now().format("%Y-%m-%d"),
src = source_line(git),
mode = mode.label(),
));
}
fn emit_banner_full(
out: &mut String,
repo_name: &str,
mode: GenMode,
git: Option<&(String, bool)>,
) {
out.push_str(&format!(
"> **GENERATED — do not edit; regenerate with `nornir docs ai-context {repo}` (nornir {ver}).**\n>\n\
> This is the **exhaustive companion. Read `docs/ai-context.md` first** for architecture, \
wiring and invariants. This file is **not intended to be loaded for every task** — its \
purpose is exact lookup (signatures, rustdoc, source `file:line`), not initial \
architectural orientation.\n\n\
_Generated {date} · {src} · mode {mode}._\n\n",
repo = repo_name,
ver = env!("CARGO_PKG_VERSION"),
date = chrono::Utc::now().format("%Y-%m-%d"),
src = source_line(git),
mode = mode.label(),
));
}
fn emit_snapshot(out: &mut String, mode: GenMode, git: Option<&(String, bool)>, has_meta: bool) {
let (rev, dirty) = match git {
Some((s, d)) => (s.clone(), *d),
None => ("unknown".into(), false),
};
out.push_str("## Snapshot\n\n");
out.push_str("| field | value |\n|---|---|\n");
out.push_str(&format!("| source revision | `{rev}` |\n"));
out.push_str(&format!(
"| working tree | {} |\n",
if dirty { "dirty" } else { "clean" }
));
out.push_str(&format!(
"| reproducible snapshot | {} |\n",
if dirty { "no" } else { "yes" }
));
out.push_str(&format!("| generation mode | {} |\n", mode.label()));
if dirty && mode.reproducible_required() {
out.push_str("| dirty override | yes (--allow-dirty) |\n");
}
out.push_str(&format!(
"| generator | `nornir {}` |\n",
env!("CARGO_PKG_VERSION")
));
out.push_str(&format!(
"| maintained metadata | {} |\n\n",
if has_meta {
format!("`{MAINTAINED_METADATA_PATH}` (validated)")
} else {
"none".into()
}
));
if dirty {
out.push_str("> ⚠ Working tree is dirty — this snapshot is **not reproducible**; `file:line` references may drift from committed code.\n\n");
}
}
// ─── Identity / usage / summary ──────────────────────────────────────────────
fn emit_identity(
out: &mut String,
repo_name: &str,
crates: &[ApiCrate],
manifests: &BTreeMap<String, CrateMeta>,
maintained: Option<&Maintained>,
) {
out.push_str("## Repository identity\n\n");
let bins: usize = manifests.values().map(|m| m.bins.len()).sum();
out.push_str("| | |\n|---|---|\n");
out.push_str(&format!("| repository | `{repo_name}` |\n"));
if let Some(m) = maintained {
if !m.repository.purpose.is_empty() {
out.push_str(&format!(
"| purpose | {} |\n",
md_cell(&m.repository.purpose)
));
}
if !m.repository.role.is_empty() {
out.push_str(&format!(
"| system role | {} |\n",
md_cell(&m.repository.role)
));
}
}
out.push_str(&format!(
"| language / workspace | Rust · {} crates · {} binaries |\n",
crates.len(),
bins
));
out.push('\n');
if let Some(m) = maintained {
if !m.repository.non_goals.is_empty() {
out.push_str("Non-goals:\n\n");
for g in &m.repository.non_goals {
out.push_str(&format!("- {g}\n"));
}
out.push('\n');
}
}
}
fn emit_usage_rules(out: &mut String) {
out.push_str("## AI usage instructions\n\n");
out.push_str(
"**Source-of-truth hierarchy** (highest first): (1) executable tests, (2) active runtime \
wiring, (3) implementation, (4) configuration and feature definitions, (5) rustdoc, \
(6) current architecture documents, (7) generated summaries (this pack), (8) historical \
documents, (9) inferred intent. Lower-priority evidence never overrides verified code.\n\n\
Rules:\n\n\
- Read this compact file first; open `docs/ai-context-full.md` only for exact signatures/rustdoc/source refs.\n\
- Inspect the referenced source files before modifying behavior; `file:line` refs are exact at generation time.\n\
- An exported API does not prove it is called; check runtime registration before assuming a backend/connector/auth mode is available.\n\
- Check feature gates before concluding code is active; feature-gated code may be compiled out of a tier.\n\
- Read and write support are not symmetric; verify writability separately (see the capability matrix).\n\
- Where partial failure is represented as data (tallies/reports), a successful return can still carry failures (see failure semantics).\n\
- Preserve path, size, origin, and immutability protections in the security map and invariants.\n\
- Do not edit generated files (this pack included); regenerate them.\n\
- Run the subsystem's tests (Task-to-file map + Tests inventory) after changes.\n\n",
);
}
fn emit_system_summary(out: &mut String, crates: &[ApiCrate], maintained: Option<&Maintained>) {
out.push_str("## System summary\n\n");
if let Some(m) = maintained {
if !m.repository.system_summary.is_empty() {
out.push_str(m.repository.system_summary.trim());
out.push_str("\n\n");
return;
}
}
for c in crates {
if !c.crate_doc.is_empty() {
out.push_str(&format!(
"**`{}`** — {}\n\n",
c.name,
strip_lead_name(&c.name, &first_para(&c.crate_doc))
));
}
}
}
fn emit_crates_table(
out: &mut String,
crates: &[ApiCrate],
manifests: &BTreeMap<String, CrateMeta>,
maintained: Option<&Maintained>,
) {
out.push_str("## Crates and layers\n\n");
if crates.is_empty() {
out.push_str("_No Rust crates with a `[package]` were found under this repo._\n\n");
return;
}
out.push_str("| crate | layer | path | description | modules | items | note |\n");
out.push_str("|---|---|---|---|---:|---:|---|\n");
let empty = CrateMeta::default();
for c in crates {
let meta = manifests.get(&c.name).unwrap_or(&empty);
let layer = layer_of(&c.name, meta, maintained);
let note = maintained
.and_then(|m| m.crates.get(&c.name))
.map(|mc| mc.note.clone())
.unwrap_or_default();
let n_items: usize = c.modules.iter().map(|m| m.items.len()).sum();
out.push_str(&format!(
"| `{}` | {} | `{}` | {} | {} | {} | {} |\n",
c.name,
layer,
meta.rel_dir,
md_cell(&first_line(&c.description).unwrap_or_default()),
c.modules.len(),
n_items,
md_cell(¬e),
));
}
out.push('\n');
}
fn emit_binaries(out: &mut String, manifests: &BTreeMap<String, CrateMeta>) {
let mut rows = Vec::new();
for (name, meta) in manifests {
for b in &meta.bins {
rows.push((
b.name.clone(),
name.clone(),
meta.rel_dir.clone(),
b.required_features.join(", "),
));
}
}
if rows.is_empty() {
return;
}
rows.sort();
out.push_str("## Binaries\n\n");
out.push_str("| binary | crate | path | required features |\n|---|---|---|---|\n");
for (bin, krate, dir, feats) in rows {
out.push_str(&format!(
"| `{bin}` | `{krate}` | `{dir}` | {} |\n",
if feats.is_empty() {
"—".into()
} else {
format!("`{feats}`")
}
));
}
out.push('\n');
}
// ─── Dependency views (3.1–3.4) ──────────────────────────────────────────────
fn emit_workspace_depgraph(out: &mut String, manifests: &BTreeMap<String, CrateMeta>) {
if !manifests.values().any(|m| !m.deps.is_empty()) {
return;
}
out.push_str("## Workspace dependency graph\n\n");
out.push_str("Edges BETWEEN workspace crates only (external deps are summarized separately). Deduplicated; a runtime edge supersedes a dev/build edge to the same crate.\n\n");
out.push_str("| crate | depends on (workspace) | kind | feature-gated |\n|---|---|---|---|\n");
for (name, meta) in manifests {
if meta.deps.is_empty() {
continue;
}
for kind in [DepKind::Runtime, DepKind::Dev, DepKind::Build] {
let of_kind: Vec<&DepEdge> = meta.deps.iter().filter(|d| d.kind == kind).collect();
if of_kind.is_empty() {
continue;
}
let deps = of_kind
.iter()
.map(|d| format!("`{}`", d.to))
.collect::<Vec<_>>()
.join(", ");
let gated: Vec<String> = of_kind
.iter()
.filter(|d| d.optional)
.map(|d| d.to.clone())
.collect();
let gated_cell = if gated.is_empty() {
"—".into()
} else {
gated
.iter()
.map(|g| format!("`{g}`"))
.collect::<Vec<_>>()
.join(", ")
};
out.push_str(&format!(
"| `{name}` | {deps} | {} | {gated_cell} |\n",
kind.label()
));
}
}
out.push('\n');
}
fn emit_hotspots(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.hotspots.is_empty() {
return;
}
out.push_str("## Central dependency hotspots\n\n");
out.push_str("External dependencies that materially shape architecture, security, build tiers, binary size, async behavior, storage, or portability. Utility deps are omitted.\n\n");
out.push_str("| dependency | used by | architectural effect | feature/tier impact |\n|---|---|---|---|\n");
for h in &m.hotspots {
out.push_str(&format!(
"| `{}` | {} | {} | {} |\n",
md_cell(&h.dependency),
md_cell(&h.used_by),
md_cell(&h.effect),
md_cell(&h.tier_impact)
));
}
out.push('\n');
}
fn emit_high_coupling(out: &mut String, manifests: &BTreeMap<String, CrateMeta>) {
let mut incoming: BTreeMap<String, usize> = BTreeMap::new();
for meta in manifests.values() {
let mut seen = BTreeSet::new();
for d in &meta.deps {
if seen.insert(d.to.clone()) {
*incoming.entry(d.to.clone()).or_default() += 1;
}
}
}
let mut rows: Vec<(String, usize, usize)> = manifests
.iter()
.map(|(name, meta)| {
let out_edges = meta
.deps
.iter()
.map(|d| d.to.clone())
.collect::<BTreeSet<_>>()
.len();
(name.clone(), *incoming.get(name).unwrap_or(&0), out_edges)
})
.filter(|(_, inc, _)| *inc >= 3)
.collect();
if rows.is_empty() {
return;
}
rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
out.push_str("## High-coupling crates\n\n");
out.push_str("Workspace crates with ≥3 downstream consumers — a signature change here ripples widely. See the change-impact map for the exact contracts.\n\n");
out.push_str("| crate | incoming edges | outgoing edges | why changes are high impact |\n|---|---:|---:|---|\n");
for (name, inc, outg) in rows {
out.push_str(&format!("| `{name}` | {inc} | {outg} | {inc} workspace crates depend on it; changing its exported types/traits breaks them at compile time |\n"));
}
out.push('\n');
}
fn emit_features(out: &mut String, manifests: &BTreeMap<String, CrateMeta>) {
if !manifests.values().any(|m| !m.features.is_empty()) {
return;
}
out.push_str("## Feature flags\n\n");
out.push_str("Feature-gated code may be compiled out of a given binary — check a tier's build flags before assuming a path is active.\n\n");
out.push_str("| crate | feature | enables |\n|---|---|---|\n");
for (name, meta) in manifests {
for (feat, deps) in &meta.features {
let enables = if deps.is_empty() {
"—".to_string()
} else {
deps.join(", ")
};
out.push_str(&format!(
"| `{name}` | `{feat}` | {} |\n",
md_cell(&enables)
));
}
}
out.push('\n');
}
// ─── Maintained maps ─────────────────────────────────────────────────────────
fn emit_wiring(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.wiring.is_empty() {
return;
}
out.push_str("## Runtime wiring map\n\n");
out.push_str("Where each capability is implemented, registered at runtime, configured, and tested. `status` distinguishes wired behavior from code that merely exists.\n\n");
out.push_str("| capability | interface | implementation | registration | configuration | tests | status |\n|---|---|---|---|---|---|---|\n");
for w in &m.wiring {
out.push_str(&format!(
"| {} | {} | {} | {} | {} | {} | {} |\n",
md_cell(&w.capability),
md_code(&w.interface),
md_code(&w.implementation),
md_code(&w.registration),
md_cell(&w.configuration),
md_code(&w.tests),
md_status(&w.status),
));
}
out.push('\n');
}
fn emit_capabilities(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.capabilities.is_empty() {
return;
}
out.push_str("## Package ecosystem capability matrix\n\n");
out.push_str("Verified per-ecosystem support. Values: `Yes` / `No` / `Partial` / `Feature-gated` / `Dev-only` / `Unverified`. Read and write are NOT symmetric — do not infer one from the other.\n\n");
out.push_str("| ecosystem | protocol fetch | protocol upload | generic fetch | browse/list | native index | writable backend | znippy backend | proxy/upstream | limitation | evidence |\n");
out.push_str("|---|---|---|---|---|---|---|---|---|---|---|\n");
for c in &m.capabilities {
out.push_str(&format!(
"| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
md_cell(&c.ecosystem),
cap(&c.protocol_fetch),
cap(&c.protocol_upload),
cap(&c.generic_fetch),
cap(&c.browse),
cap(&c.native_index),
cap(&c.writable_backend),
cap(&c.znippy_backend),
cap(&c.proxy_upstream),
md_cell(&c.limitation),
md_code_list(&c.evidence),
));
}
out.push('\n');
}
fn emit_change_impact(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.change_impact.is_empty() {
return;
}
out.push_str("## Change impact map\n\n");
out.push_str("Shared interfaces/types and what a signature change ripples into. Consult before altering a central trait.\n\n");
out.push_str("| interface / type | defined in | implementers | main consumers | runtime significance | required tests |\n|---|---|---|---|---|---|\n");
for c in &m.change_impact {
out.push_str(&format!(
"| {} | {} | {} | {} | {} | {} |\n",
md_code(&c.interface),
md_code(&c.defined_in),
md_cell(&c.implementers),
md_cell(&c.consumers),
md_cell(&c.significance),
md_code_list(&c.tests),
));
}
out.push('\n');
}
fn emit_failures(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.failures.is_empty() {
return;
}
out.push_str("## Failure semantics\n\n");
out.push_str("Per-operation: what aborts, what is counted-and-skipped, whether an `Ok` result still carries failures, retries, cleanup, and how the outcome becomes observable.\n\n");
out.push_str("| operation | fatal errors | partial success | retry | cleanup / rollback | observable result | evidence |\n|---|---|---|---|---|---|---|\n");
for f in &m.failures {
out.push_str(&format!(
"| {} | {} | {} | {} | {} | {} | {} |\n",
md_cell(&f.operation),
md_cell(&f.fatal),
md_cell(&f.partial),
md_cell(&f.retry),
md_cell(&f.cleanup),
md_cell(&f.observable),
md_code_list(&f.evidence),
));
}
out.push('\n');
}
fn emit_security(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.security.is_empty() {
return;
}
out.push_str("## Security-sensitive code map\n\n");
out.push_str("| concern | invariant | implementation | tests | consequence if broken |\n|---|---|---|---|---|\n");
for s in &m.security {
out.push_str(&format!(
"| {} | {} | {} | {} | {} |\n",
md_cell(&s.concern),
md_cell(&s.invariant),
md_code_list(&s.implementation),
md_code_list(&s.tests),
md_cell(&s.consequence),
));
}
out.push('\n');
}
fn emit_invariants(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.invariants.is_empty() {
return;
}
out.push_str("## Non-negotiable invariants\n\n");
for inv in &m.invariants {
let st = if inv.status.is_empty() {
String::new()
} else {
format!(" · {}", md_status(&inv.status))
};
out.push_str(&format!("**`{}`**{st} — {}\n\n", inv.id, inv.statement));
if !inv.enforced.is_empty() {
out.push_str(&format!("- enforced: {}\n", md_code_list(&inv.enforced)));
}
if !inv.tests.is_empty() {
out.push_str(&format!("- verified: {}\n", md_code_list(&inv.tests)));
}
if !inv.break_risk.is_empty() {
out.push_str(&format!("- break risk: {}\n", inv.break_risk));
}
out.push('\n');
}
}
fn emit_cli(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.cli.is_empty() {
return;
}
out.push_str("## CLI command inventory\n\n");
out.push_str("| binary | command | important flags | calls into | feature/tier | tests |\n|---|---|---|---|---|---|\n");
for c in &m.cli {
out.push_str(&format!(
"| `{}` | `{}` | {} | {} | {} | {} |\n",
md_cell(&c.binary),
md_cell(&c.command),
md_cell(&c.flags),
md_code(&c.calls),
md_cell(&c.restrictions),
md_code_list(&c.tests),
));
}
out.push('\n');
}
fn emit_config(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.config.is_empty() {
return;
}
out.push_str("## RON configuration inventory\n\n");
out.push_str("| field | type | default | required | sensitive | runtime consumer | feature/tier |\n|---|---|---|---|---|---|---|\n");
for c in &m.config {
out.push_str(&format!(
"| `{}` | {} | {} | {} | {} | {} | {} |\n",
md_cell(&c.field),
md_cell(&c.kind),
md_cell(&c.default),
yn(c.required),
yn(c.sensitive),
md_code(&c.consumer),
md_cell(&c.restrictions),
));
}
out.push('\n');
}
fn emit_precedence(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.precedence.is_empty() {
return;
}
out.push_str("## Configuration precedence\n\n");
out.push_str("Verified precedence where inputs overlap (lowest → highest). Unverified overlaps are marked.\n\n");
for (i, step) in m.precedence.iter().enumerate() {
out.push_str(&format!("{}. {}\n", i + 1, step));
}
out.push('\n');
}
fn emit_env_vars(
out: &mut String,
scanned: &BTreeMap<String, BTreeSet<String>>,
maintained: Option<&Maintained>,
warnings: &mut Vec<String>,
) {
out.push_str("## Environment variables\n\n");
let documented: BTreeSet<String> = maintained
.map(|m| m.env.iter().map(|e| e.name.clone()).collect())
.unwrap_or_default();
if let Some(m) = maintained {
if !m.env.is_empty() {
out.push_str("Maintained inventory (effect/sensitivity are not derivable). `HOME`/`USER`/`XDG_*` are excluded unless they materially change behavior.\n\n");
out.push_str("| variable | kind | default | sensitive | phase | effect | evidence |\n|---|---|---|---|---|---|---|\n");
for e in &m.env {
out.push_str(&format!(
"| `{}` | {} | {} | {} | {} | {} | {} |\n",
md_cell(&e.name),
md_cell(&e.kind),
md_cell(&e.default),
yn(e.sensitive),
md_cell(&e.phase),
md_cell(&e.effect),
md_code_list(&e.evidence),
));
}
out.push('\n');
}
}
const BENIGN: &[&str] = &[
"HOME",
"USER",
"XDG_RUNTIME_DIR",
"TMPDIR",
"TMP",
"PATH",
"LANG",
"TERM",
"SHELL",
"PWD",
"HOSTNAME",
];
let undocumented: Vec<String> = scanned
.keys()
.filter(|v| !documented.contains(*v) && !BENIGN.contains(&v.as_str()))
.cloned()
.collect();
if !undocumented.is_empty() {
out.push_str("Found in source but not in the maintained inventory (add them to `.nornir/ai-context.toml [[env]]`):\n\n");
for v in &undocumented {
let files: Vec<_> = scanned[v]
.iter()
.take(2)
.map(|f| format!("`{f}`"))
.collect();
out.push_str(&format!("- `{v}` — {}\n", files.join(", ")));
}
out.push('\n');
warnings.push(format!(
"{} env var(s) found in source but undocumented in metadata",
undocumented.len()
));
}
if let Some(m) = maintained {
let stale: Vec<&str> = m
.env
.iter()
.map(|e| e.name.as_str())
.filter(|n| !scanned.contains_key(*n))
.collect();
if !stale.is_empty() {
warnings.push(format!(
"documented env var(s) not found in source scan: {}",
stale.join(", ")
));
}
}
}
fn emit_tests(out: &mut String, tests: &BTreeMap<String, TestMeta>) {
if !tests
.values()
.any(|t| t.test_fns > 0 || !t.integration_files.is_empty())
{
return;
}
out.push_str("## Tests inventory\n\n");
out.push_str("Run a crate's tests with `cargo test -p <crate>` (some need features/env — see the Task-to-file map). `#[ignore]`d arms are opt-in heavy tests.\n\n");
out.push_str("| crate | test fns | ignored | integration test files |\n|---|---:|---:|---|\n");
for (name, t) in tests {
if t.test_fns == 0 && t.integration_files.is_empty() {
continue;
}
let mut files = t.integration_files.clone();
let extra = files.len().saturating_sub(6);
files.truncate(6);
let mut cell = files
.iter()
.map(|f| format!("`{f}`"))
.collect::<Vec<_>>()
.join(", ");
if extra > 0 {
cell.push_str(&format!(" (+{extra} more)"));
}
if cell.is_empty() {
cell = "—".into();
}
out.push_str(&format!(
"| `{name}` | {} | {} | {cell} |\n",
t.test_fns, t.ignored
));
}
out.push('\n');
}
fn emit_tasks(out: &mut String, maintained: Option<&Maintained>) {
let Some(m) = maintained else { return };
if m.tasks.is_empty() {
return;
}
out.push_str("## Task-to-file map\n\n");
out.push_str("Test columns are runnable commands. Prefer `fast`; run `full` before landing; `heavy` needs containers/network or `-- --ignored`.\n\n");
out.push_str("| task | start here | follow-up | invariants | fast test | full subsystem test | heavy/optional |\n|---|---|---|---|---|---|---|\n");
for t in &m.tasks {
let fast = if t.fast_tests.is_empty() {
&t.tests
} else {
&t.fast_tests
};
out.push_str(&format!(
"| {} | {} | {} | {} | {} | {} | {} |\n",
md_cell(&t.name),
md_code_list(&t.start),
md_code_list(&t.related),
md_cell(&t.invariants.join(", ")),
md_code_list(fast),
md_code_list(&t.full_tests),
md_code_list(&t.heavy_tests),
));
}
out.push('\n');
}
fn emit_todos(out: &mut String, todos: &[TodoHit]) {
if todos.is_empty() {
return;
}
out.push_str("## Incomplete-work markers\n\n");
out.push_str("Derived scan: TODO / FIXME / `unimplemented!` / `todo!` / `#[ignore]`. An `#[ignore]`d test is opt-in (often heavy), not broken. Treat these as spots where rustdoc may promise more than the code delivers.\n\n");
const CAP: usize = 120;
out.push_str("| kind | site | text |\n|---|---|---|\n");
for t in todos.iter().take(CAP) {
out.push_str(&format!(
"| {} | `{}:{}` | {} |\n",
t.kind,
t.file,
t.line,
md_cell(&t.text)
));
}
if todos.len() > CAP {
out.push_str(&format!(
"\n_({} more markers not shown.)_\n",
todos.len() - CAP
));
}
out.push('\n');
}
fn emit_status_legend(out: &mut String) {
out.push_str("## Status legend\n\n");
out.push_str("Statuses used across this pack mean exactly:\n\n");
for s in ALLOWED_STATUSES {
out.push_str(&format!("- `{s}`\n"));
}
out.push('\n');
}
fn emit_warnings(out: &mut String, maintained: Option<&Maintained>) {
out.push_str("## AI reasoning warnings\n\n");
for w in [
"An enum variant does not prove runtime support; check the dispatch site.",
"A dependency in Cargo.toml does not prove wiring; check construction/registration.",
"An exported function does not prove it is called.",
"A backend may support reads but not writes (or vice versa) — see the capability matrix.",
"A successful operation result may still carry failed-item tallies — see failure semantics.",
"Test-only/mock behavior (mannequins, fixtures, dev pairs) is not production behavior.",
"A crate description may be stale relative to feature wiring.",
"Generated files must be regenerated, never hand-edited.",
] {
out.push_str(&format!("- {w}\n"));
}
if let Some(m) = maintained {
for w in &m.warnings {
out.push_str(&format!("- {w}\n"));
}
}
out.push('\n');
}
fn emit_documents(out: &mut String, maintained: Option<&Maintained>) {
out.push_str("## Prose documents\n\n");
out.push_str(
"Human-facing documentation excluded from compact AI context.\n\
Relevant verified implementation facts are extracted into the system summary,\n\
runtime wiring, capability matrices, warnings, and invariants.\n\n",
);
let Some(m) = maintained else { return };
if m.documents.is_empty() {
return;
}
out.push_str("Status classification of the repo's prose (drives full-pack inclusion; historical/superseded/marketing prose never overrides code):\n\n");
out.push_str("| document | status | in compact | in full | reason |\n|---|---|---|---|---|\n");
for d in &m.documents {
out.push_str(&format!(
"| `{}` | {} | {} | {} | {} |\n",
md_cell(&d.path),
md_status(&d.status),
yn(d.include_compact),
yn(d.include_full),
md_cell(&d.reason),
));
}
out.push('\n');
}
fn emit_api_summary(out: &mut String, crates: &[ApiCrate]) {
out.push_str("## API summary\n\n");
out.push_str("Per-crate exported surface. Exact signatures + docs are in `docs/ai-context-full.md` under the linked crate heading.\n\n");
out.push_str(
"| crate | modules | items | largest modules | full reference |\n|---|---:|---:|---|---|\n",
);
for c in crates {
let n_items: usize = c.modules.iter().map(|m| m.items.len()).sum();
let mut mods: Vec<_> = c
.modules
.iter()
.filter(|m| !m.items.is_empty())
.map(|m| (m.items.len(), m.path.clone()))
.collect();
mods.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
let top = mods
.iter()
.take(4)
.map(|(n, p)| format!("`{p}` ({n})"))
.collect::<Vec<_>>()
.join(", ");
let slug = format!("crate-{}", slugify(&c.name));
out.push_str(&format!(
"| `{}` | {} | {} | {} | [full](docs/ai-context-full.md#{slug}) |\n",
c.name,
c.modules.len(),
n_items,
if top.is_empty() { "—".into() } else { top },
));
}
out.push('\n');
}
fn emit_final_index(
out: &mut String,
crates: &[ApiCrate],
manifests: &BTreeMap<String, CrateMeta>,
maintained: Option<&Maintained>,
env_vars: &BTreeMap<String, BTreeSet<String>>,
) {
out.push_str("## Compact index\n\n");
let mut by_layer: BTreeMap<String, Vec<String>> = BTreeMap::new();
let empty = CrateMeta::default();
for c in crates {
let meta = manifests.get(&c.name).unwrap_or(&empty);
by_layer
.entry(layer_of(&c.name, meta, maintained))
.or_default()
.push(c.name.clone());
}
for (layer, names) in &by_layer {
out.push_str(&format!(
"- **{layer}**: {}\n",
names
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", ")
));
}
let bins: Vec<String> = manifests
.values()
.flat_map(|m| m.bins.iter().map(|b| format!("`{}`", b.name)))
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
if !bins.is_empty() {
out.push_str(&format!("- **binaries**: {}\n", bins.join(", ")));
}
if !env_vars.is_empty() {
out.push_str(&format!(
"- **env vars (scanned)**: {}\n",
env_vars
.keys()
.map(|v| format!("`{v}`"))
.collect::<Vec<_>>()
.join(", ")
));
}
if let Some(m) = maintained {
if !m.tasks.is_empty() {
out.push_str(&format!(
"- **tasks mapped**: {}\n",
m.tasks
.iter()
.map(|t| t.name.clone())
.collect::<Vec<_>>()
.join(" · ")
));
}
if !m.invariants.is_empty() {
out.push_str(&format!(
"- **invariants**: {}\n",
m.invariants
.iter()
.map(|i| format!("`{}`", i.id))
.collect::<Vec<_>>()
.join(", ")
));
}
if !m.capabilities.is_empty() {
out.push_str(&format!(
"- **ecosystems**: {}\n",
m.capabilities
.iter()
.map(|c| c.ecosystem.clone())
.collect::<Vec<_>>()
.join(", ")
));
}
}
out.push('\n');
}
// ─── Full-pack emitters ──────────────────────────────────────────────────────
fn emit_full_backlinks(out: &mut String) {
out.push_str("## Orientation\n\n");
out.push_str(
"Start in the compact pack for reasoning context, then return here for exact API detail:\n\n\
- Runtime wiring → `docs/ai-context.md#runtime-wiring-map`\n\
- Security invariants → `docs/ai-context.md#non-negotiable-invariants`\n\
- Failure semantics → `docs/ai-context.md#failure-semantics`\n\
- Capability matrix → `docs/ai-context.md#package-ecosystem-capability-matrix`\n\
- Task-to-file map → `docs/ai-context.md#task-to-file-map`\n\n",
);
}
/// Emit the exhaustive API reference. Returns the set of crate-heading anchors
/// emitted. Trivial accessors are dropped; the generated protobuf module is
/// summarized rather than dumped; `file:line` refs are made repo-relative.
fn emit_api_reference(out: &mut String, crates: &[ApiCrate], repo_root: &Path) -> BTreeSet<String> {
out.push_str("## API reference\n\n");
out.push_str("Every exported item, grouped by crate then module, with signature, repo-relative `file:line`, and documentation. Trivial accessors are omitted; generated protobuf is summarized (see `proto/holger.proto`).\n\n");
let mut anchors = BTreeSet::new();
for c in crates {
let slug = format!("crate-{}", slugify(&c.name));
anchors.insert(slug);
out.push_str(&format!("### Crate `{}`\n\n", c.name));
let crate_rel = c
.root
.strip_prefix(repo_root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
if !c.description.is_empty() {
out.push_str(&format!(
"_{}_\n\n",
first_line(&c.description).unwrap_or_default()
));
}
if !c.crate_doc.is_empty() {
out.push_str(&c.crate_doc);
out.push_str("\n\n");
}
let mut proto_done = false;
for m in &c.modules {
if is_generated_proto(&m.path) {
if !proto_done {
emit_proto_summary(out, c, &crate_rel);
proto_done = true;
}
continue;
}
let visible: Vec<_> = m.items.iter().filter(|it| !is_trivial_item(it)).collect();
if visible.is_empty() && m.doc.is_empty() {
continue;
}
out.push_str(&format!("#### Module `{}`\n\n", m.path));
if !m.doc.is_empty() {
out.push_str(&blockquote(&m.doc));
out.push_str("\n\n");
}
for it in visible {
let visprefix = if it.visibility == "pub" || it.visibility == "priv" {
String::new()
} else {
format!("{} ", it.visibility)
};
let file = repo_rel_file(&crate_rel, &it.file);
out.push_str(&format!(
"**`{}{}`** · `{}:{}`\n\n",
visprefix, it.signature, file, it.line
));
if !it.doc.is_empty() {
out.push_str(&it.doc);
out.push_str("\n\n");
}
}
}
}
anchors
}
/// True for any module inside the generated protobuf tree (`…generated::…v1…`).
fn is_generated_proto(module_path: &str) -> bool {
module_path.contains("generated::") && module_path.contains(".v1")
}
fn emit_proto_summary(out: &mut String, c: &ApiCrate, crate_rel: &str) {
let mut services = BTreeSet::new();
let mut messages = BTreeSet::new();
for m in &c.modules {
if !is_generated_proto(&m.path) {
continue;
}
if let Some(svc) = m
.path
.split("::")
.find(|s| s.ends_with("_service_server") || s.ends_with("_service_client"))
{
let base = svc.trim_end_matches("_server").trim_end_matches("_client");
services.insert(base.to_string());
}
for it in &m.items {
if it.kind == "struct" && m.path.ends_with(".v1") {
messages.insert(it.name.clone());
}
}
}
out.push_str("#### Generated protobuf (summarized)\n\n");
out.push_str(&format!(
"The proto-generated Rust lives at `{}` (do not edit — regenerate via `cargo xtask gen-proto` from `proto/holger.proto`). Summary instead of a full dump:\n\n",
repo_rel_file(crate_rel, "src/generated/holger.v1.rs")
));
if !services.is_empty() {
let svc = services
.iter()
.map(|s| format!("`{s}`"))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!("- **services**: {svc}\n"));
}
if !messages.is_empty() {
let msg = messages
.iter()
.map(|s| format!("`{s}`"))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!("- **messages**: {msg}\n"));
}
out.push_str("- source of truth: `proto/holger.proto`\n\n");
}
/// Drop no-signal accessors: undocumented getters/setters/`is_*`/`as_*`/`len`/
/// `is_empty`/`default`/`fmt`/`clone`. Anything documented is kept.
fn is_trivial_item(it: &apidoc::ApiItem) -> bool {
if !it.doc.is_empty() || it.kind != "fn" {
return false;
}
let n = it.name.as_str();
n == "len"
|| n == "is_empty"
|| n == "default"
|| n == "fmt"
|| n == "clone"
|| n.starts_with("get_")
|| n.starts_with("set_")
|| (n.starts_with("is_") && it.signature.contains("-> bool"))
|| (n.starts_with("as_") && it.signature.contains('&'))
}
fn repo_rel_file(crate_rel: &str, file: &str) -> String {
if crate_rel.is_empty() || crate_rel == "." || file.starts_with(crate_rel) {
file.to_string()
} else {
format!("{crate_rel}/{file}")
}
}
fn emit_prose_current(
out: &mut String,
repo_root: &Path,
ctx: &Ctx,
maintained: Option<&Maintained>,
) {
let Ok(chapters) = super::book::collect_chapters(repo_root, ctx) else {
return;
};
let allow = |src: &Path| -> bool {
let rel = src
.strip_prefix(repo_root)
.unwrap_or(src)
.to_string_lossy()
.to_string();
match maintained {
Some(m) => m
.documents
.iter()
.find(|d| rel.ends_with(&d.path) || d.path.ends_with(&rel))
.map(|d| d.include_full)
.unwrap_or_else(|| {
src.file_name()
.is_some_and(|f| f.to_string_lossy().eq_ignore_ascii_case("readme.md"))
}),
None => src
.file_name()
.is_some_and(|f| f.to_string_lossy().eq_ignore_ascii_case("readme.md")),
}
};
let kept: Vec<_> = chapters.iter().filter(|c| allow(&c.source)).collect();
if kept.is_empty() {
return;
}
out.push_str("## Project documentation (current status only)\n\n");
out.push_str("Only prose classified `current implementation`/`current architecture` is reproduced. Historical/superseded/marketing/human-guide prose is excluded and, per the source-of-truth hierarchy, never overrides code.\n\n");
for ch in kept {
out.push_str(&demote_headings(&ch.markdown, 2));
out.push_str("\n\n");
}
}
// ─── Small helpers ───────────────────────────────────────────────────────────
fn first_line(s: &str) -> Option<String> {
s.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.map(str::to_string)
}
fn strip_lead_name(name: &str, para: &str) -> String {
let p = para.trim().trim_start_matches('`').trim_start();
if let Some(rest) = p.strip_prefix(name) {
let rest = rest.trim_start_matches('`').trim_start();
for sep in [" — ", " – ", " - ", ": "] {
if let Some(r) = rest.strip_prefix(sep) {
return r.trim().to_string();
}
}
}
para.trim().to_string()
}
fn first_para(s: &str) -> String {
let mut buf = String::new();
for line in s.lines() {
if line.trim().is_empty() {
break;
}
if !buf.is_empty() {
buf.push(' ');
}
buf.push_str(line.trim());
}
buf
}
fn md_cell(s: &str) -> String {
s.replace('|', "\\|").replace('\n', " ")
}
fn md_code(s: &str) -> String {
let s = s.trim();
if s.is_empty() {
"—".into()
} else if s.contains(' ') {
md_cell(s)
} else {
format!("`{}`", md_cell(s))
}
}
fn md_code_list(items: &[String]) -> String {
if items.is_empty() {
return "—".into();
}
items
.iter()
.map(|i| md_code(i))
.collect::<Vec<_>>()
.join(", ")
}
/// Render a capability-matrix cell value, keeping the controlled vocabulary.
fn cap(s: &str) -> String {
let s = s.trim();
if s.is_empty() {
"Unverified".into()
} else {
md_cell(s)
}
}
fn yn(b: bool) -> &'static str {
if b { "yes" } else { "no" }
}
/// Render a status as `` `label` annotation`` (the label controlled, the rest
/// free text). Empty → em-dash; unrecognized (shouldn't reach here — validated
/// upstream) → escaped as-is.
fn md_status(s: &str) -> String {
match leading_status(s) {
Some((label, rest)) if rest.is_empty() => format!("`{label}`"),
Some((label, rest)) => format!("`{label}` — {}", md_cell(&rest)),
None if s.trim().is_empty() => "—".into(),
None => md_cell(s),
}
}
/// GitHub-style heading slug: lowercase, non-alnum → '-', collapse, trim.
fn slugify(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_dash = false;
for ch in s.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
prev_dash = false;
} else if !prev_dash {
out.push('-');
prev_dash = true;
}
}
out.trim_matches('-').to_string()
}
fn blockquote(s: &str) -> String {
s.lines()
.map(|l| {
if l.is_empty() {
">".to_string()
} else {
format!("> {l}")
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn demote_headings(s: &str, by: usize) -> String {
let mut out = String::with_capacity(s.len());
let mut in_fence = false;
for line in s.lines() {
let t = line.trim_start();
if t.starts_with("```") || t.starts_with("~~~") {
in_fence = !in_fence;
out.push_str(line);
out.push('\n');
continue;
}
if !in_fence && t.starts_with('#') {
let hashes = t.chars().take_while(|&c| c == '#').count();
if (1..=6).contains(&hashes) {
let add = "#".repeat((hashes + by).min(6) - hashes);
out.push_str(add.as_str());
out.push_str(line);
out.push('\n');
continue;
}
}
out.push_str(line);
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn widget_repo(tmp: &std::path::Path) -> PathBuf {
let dir = tmp.join("widget");
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(
dir.join("Cargo.toml"),
"[package]\nname = \"widget\"\ndescription = \"a widget\"\n\n\
[features]\ndefault = []\nextra = [\"dep:serde\"]\n\n\
[dependencies]\nserde = { version = \"1\", optional = true }\ngadget = { path = \"../gadget\" }\n",
)
.unwrap();
std::fs::write(
dir.join("src/lib.rs"),
"//! The widget crate.\n\
/// Build a widget.\n\
pub fn make(size: u32) -> u32 {\n\
// TODO: sizes\n\
let _ = std::env::var(\"WIDGET_HOME\");\n\
size\n\
}\n\
pub fn get_size() -> u32 { 0 }\n\
#[cfg(test)]\nmod tests {\n #[test]\n fn t() {}\n}\n",
)
.unwrap();
let g = tmp.join("gadget");
std::fs::create_dir_all(g.join("src")).unwrap();
std::fs::write(g.join("Cargo.toml"), "[package]\nname = \"gadget\"\n").unwrap();
std::fs::write(g.join("src/lib.rs"), "//! gadget\n").unwrap();
tmp.to_path_buf()
}
fn opts() -> AiContextOpts {
AiContextOpts {
include_prose: false,
..Default::default()
}
}
#[test]
fn builds_compact_and_full_with_expected_sections() {
let tmp = tempfile::tempdir().unwrap();
let root = widget_repo(tmp.path());
let dir = root.join("widget");
let ctx = Ctx::new(&dir, &dir, None);
let o = build(&dir, &ctx, &opts()).unwrap();
let md = &o.compact;
for want in [
"AI context pack (compact)",
"compact primary context",
"## Snapshot",
"## Repository identity",
"## AI usage instructions",
"## Crates and layers",
"## Feature flags",
"## Status legend",
"## Environment variables",
"## Incomplete-work markers",
"## Tests inventory",
"## AI reasoning warnings",
"## Prose documents",
"## API summary",
"## Compact index",
"Human-facing documentation excluded from compact AI context",
] {
assert!(md.contains(want), "compact missing {want:?}");
}
assert!(!md.contains("#### Module"), "no api dump in compact");
let full = &o.full;
assert!(full.contains("full API reference"));
assert!(full.contains("exact lookup"));
assert!(full.contains("Crate `widget`"));
assert!(full.contains("fn make(size: u32) -> u32"));
assert!(
!full.contains("get_size"),
"trivial getter should be filtered: {full}"
);
assert_eq!(o.stats.crates, 1);
assert!(o.stats.compact_tokens > 0 && o.stats.full_tokens > 0);
}
#[test]
fn deterministic_across_runs() {
let tmp = tempfile::tempdir().unwrap();
let root = widget_repo(tmp.path());
let dir = root.join("widget");
let ctx = Ctx::new(&dir, &dir, None);
let a = build(&dir, &ctx, &opts()).unwrap();
let b = build(&dir, &ctx, &opts()).unwrap();
assert_eq!(a.compact, b.compact);
assert_eq!(a.full, b.full);
}
#[test]
fn maintained_metadata_emitted_and_validated() {
let tmp = tempfile::tempdir().unwrap();
let root = widget_repo(tmp.path());
let dir = root.join("widget");
std::fs::create_dir_all(dir.join(".nornir")).unwrap();
std::fs::write(
dir.join(MAINTAINED_METADATA_PATH),
r#"
[repository]
purpose = "test widgetry"
[[capabilities]]
ecosystem = "Widget"
protocol_fetch = "Yes"
writable_backend = "No"
[[failures]]
operation = "make"
fatal = "no"
[[cli]]
binary = "widget"
command = "make"
[[change_impact]]
interface = "Widgetize"
defined_in = "src/lib.rs:3"
[[invariants]]
id = "w1"
statement = "widgets never shrink"
enforced = ["src/lib.rs:3"]
status = "Verified implementation"
[[tasks]]
name = "resize"
start = ["src/lib.rs"]
fast_tests = ["cargo test -p widget"]
"#,
)
.unwrap();
let ctx = Ctx::new(&dir, &dir, None);
let md = build(&dir, &ctx, &opts()).unwrap().compact;
for want in [
"test widgetry",
"## Package ecosystem capability matrix",
"## Failure semantics",
"## CLI command inventory",
"## Change impact map",
"## Non-negotiable invariants",
"`w1`",
"## Task-to-file map",
] {
assert!(md.contains(want), "missing {want:?}");
}
}
#[test]
fn dangling_reference_fails() {
let tmp = tempfile::tempdir().unwrap();
let root = widget_repo(tmp.path());
let dir = root.join("widget");
std::fs::create_dir_all(dir.join(".nornir")).unwrap();
std::fs::write(
dir.join(MAINTAINED_METADATA_PATH),
"[[invariants]]\nid = \"bad\"\nstatement = \"x\"\nenforced = [\"src/nope.rs\"]\n",
)
.unwrap();
let ctx = Ctx::new(&dir, &dir, None);
let err = build(&dir, &ctx, &opts()).unwrap_err().to_string();
assert!(
err.contains("dangling") && err.contains("src/nope.rs"),
"{err}"
);
}
#[test]
fn unknown_status_fails() {
let tmp = tempfile::tempdir().unwrap();
let root = widget_repo(tmp.path());
let dir = root.join("widget");
std::fs::create_dir_all(dir.join(".nornir")).unwrap();
std::fs::write(
dir.join(MAINTAINED_METADATA_PATH),
"[[wiring]]\ncapability = \"x\"\nstatus = \"totally made up\"\n",
)
.unwrap();
let ctx = Ctx::new(&dir, &dir, None);
let err = build(&dir, &ctx, &opts()).unwrap_err().to_string();
assert!(err.contains("unknown status"), "{err}");
}
#[test]
fn status_contradiction_fails() {
let tmp = tempfile::tempdir().unwrap();
let root = widget_repo(tmp.path());
let dir = root.join("widget");
std::fs::create_dir_all(dir.join(".nornir")).unwrap();
std::fs::write(
dir.join(MAINTAINED_METADATA_PATH),
"[[wiring]]\ncapability = \"Promote\"\nstatus = \"Runtime wired\"\n\n\
[[invariants]]\nid = \"Promote\"\nstatement = \"x\"\nstatus = \"Not implemented\"\n",
)
.unwrap();
let ctx = Ctx::new(&dir, &dir, None);
let err = build(&dir, &ctx, &opts()).unwrap_err().to_string();
assert!(err.contains("contradiction"), "{err}");
}
#[test]
fn bad_test_command_crate_fails() {
let tmp = tempfile::tempdir().unwrap();
let root = widget_repo(tmp.path());
let dir = root.join("widget");
std::fs::create_dir_all(dir.join(".nornir")).unwrap();
std::fs::write(
dir.join(MAINTAINED_METADATA_PATH),
"[[tasks]]\nname = \"t\"\nfast_tests = [\"cargo test -p not-a-crate\"]\n",
)
.unwrap();
let ctx = Ctx::new(&dir, &dir, None);
let err = build(&dir, &ctx, &opts()).unwrap_err().to_string();
assert!(
err.contains("unknown crate") && err.contains("not-a-crate"),
"{err}"
);
}
#[test]
fn mode_semantics() {
assert!(GenMode::Release.reproducible_required());
assert!(GenMode::Ci.reproducible_required());
assert!(!GenMode::Development.reproducible_required());
assert_eq!(GenMode::parse("release"), GenMode::Release);
assert_eq!(GenMode::parse("whatever"), GenMode::Development);
}
#[test]
fn workspace_edges_deduped_and_internal_only() {
let tmp = tempfile::tempdir().unwrap();
let root = widget_repo(tmp.path());
let manifests = scan_manifests(&root);
let w = &manifests["widget"];
let gadget_edges: Vec<_> = w.deps.iter().filter(|d| d.to == "gadget").collect();
assert_eq!(gadget_edges.len(), 1, "one deduped internal edge to gadget");
assert!(
!w.deps.iter().any(|d| d.to == "serde"),
"external dep is not a workspace edge"
);
}
#[test]
fn slugify_matches_github() {
assert_eq!(slugify("holger-server-lib"), "holger-server-lib");
assert_eq!(slugify("Crate foo"), "crate-foo");
}
#[test]
fn helpers_behave() {
assert_eq!(
path_of_reference("src/lib.rs:42"),
Some("src/lib.rs".into())
);
assert_eq!(path_of_reference("cargo test -p x"), None);
assert_eq!(
cargo_test_crate("cargo test -p holger-ui --features gui"),
Some("holger-ui".into())
);
assert_eq!(normalize_status("runtime wired"), Some("Runtime wired"));
assert_eq!(status_polarity("Not implemented"), Some("absent"));
assert_eq!(status_polarity("Runtime wired"), Some("live"));
}
#[test]
fn demote_headings_skips_code_fences() {
let src = "# Title\n\n```\n# not a heading\n```\n## Sub\n";
let got = demote_headings(src, 2);
assert!(got.contains("### Title") && got.contains("#### Sub"));
assert!(got.contains("\n# not a heading\n"));
}
}