crates/nornir-purgatory/src/lib.rs
//! **The purgatorium** — the evidence half of a two-stage kernel minimiser.
//!
//! ```text
//! stage 1 build-thing variant, COMMITTED fragments = POLICY a floor, cannot regress
//! stage 2 purgatorium, MEASURED reachability = EVIDENCE a subtraction on that floor
//! ```
//!
//! Stage 2 runs **before** the kernel is configured, because its output is an
//! *input* to kconfig selection — the same shape `data.io = "uring"` already has
//! in `nornir-build-thing`'s `data.rs`.
//!
//! The plan is `workspace_nordisk/mimir-v2.md`. **What is here:**
//!
//! * step 1 — the four view modes ([`kconfig::effective`]), pure set arithmetic;
//! * step 3 — the three invariants ([`subtract`]), each a test;
//! * the first real implementations of the three traits:
//! [`kconfig::tree::KconfigTree`] is a [`KconfigSource`] over a real kernel
//! tree (the `select`/`depends on` graph, read, not solved);
//! [`syscall::ProbeEvidence`] is an [`EvidenceSource`] — a measured syscall
//! list joined to CONFIG symbols through [`syscall::SyscallJoin`], derive >
//! probe > checked table (plan §2, step 5);
//! [`subtract::KconfigAwareSubtractor`] is a [`Subtractor`] whose immunity is
//! transitive through the graph and which reports a futile subtraction by
//! naming the `select`or that would undo it.
//!
//! What is NOT here, and why:
//!
//! * **The gate itself** (`dwarves purgatorium`, the `buildthing` step) — a
//! dwarves path. The evidence it needs from the CALL GRAPH is still absent in
//! one direction: nornir `dbdea8a` REFUSES `ra_ingest`'s def-site-keyed scans
//! from the cross-repo join (so the 2026-09-07 fabricated edge can no longer
//! happen), but `ra_ap_ide::moniker` is not wired, so calls that cross a
//! workspace member are MISSING rather than wrong. An [`EvidenceSource`] over
//! that graph must report [`Freshness::Unsound`] until it is — an incomplete
//! demand set subtracts symbols another member reaches. The syscall probe
//! here does not have that problem: it measures the whole process.
//! * **`RECIPE_SCHEMA` 4→5** and **whether stage 2 may subtract at all on the
//! assured appliance** — both explicitly the owner's (plan §8), so neither is
//! decided here. [`subtract::Policy::report_only`] carries the second as a
//! switch with no default.
//!
//! Dep-light by design: consumed by dwarves, nornir and dwarves-gui, so it takes
//! nothing but `std`. A kernel tree is read from disk; a warehouse is behind
//! [`EvidenceSource`].
pub mod kconfig;
pub mod subtract;
pub mod syscall;
pub use kconfig::{
Directive, Effective, EffectiveReport, KconfigTree, Requested, Unavailable, Value,
};
pub use subtract::{
propose, Decision, Demand, Freshness, KconfigAwareSubtractor, Policy, PolicySubtractor, Scope,
Subtraction, Subtractor,
};
use std::collections::BTreeSet;
/// The warehouse side, so the gate is testable without one.
pub trait EvidenceSource {
/// Kconfig symbols the measured code reaches, for `repo`.
fn demand(&self, repo: &str) -> Demand;
/// How much that evidence can be trusted — drives FAIL CLOSED.
fn freshness(&self) -> Freshness;
}
/// The kernel-tree side: Kconfig's own `select`/`depends on` graph, which is why
/// no C scan is needed (plan §2 — *"Kconfig itself IS a declarative dependency
/// graph"*).
pub trait KconfigSource {
fn selects(&self, symbol: &str) -> Vec<String>;
fn depends(&self, symbol: &str) -> Vec<String>;
}
/// Stage 1's symbol set: everything a committed fragment named, and therefore
/// everything invariant 2 makes immune.
#[must_use]
pub fn immune_from_fragments<'a, I>(fragments: I) -> BTreeSet<String>
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
Requested::from_fragments(fragments)
.symbols()
.into_iter()
.map(str::to_string)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
/// Immunity is derived FROM the fragments, never hand-listed — so a symbol
/// added to `hardened.config` tomorrow is immune the same day, with nobody
/// remembering to update a second list.
#[test]
fn every_symbol_a_fragment_names_is_immune_including_its_vetoes() {
let im = immune_from_fragments([(
"hardened.config",
"CONFIG_SECCOMP=y\nCONFIG_STACKPROTECTOR_STRONG=y\n# CONFIG_DEVMEM is not set\n",
)]);
assert!(im.contains("SECCOMP"));
assert!(im.contains("STACKPROTECTOR_STRONG"));
assert!(
im.contains("DEVMEM"),
"a VETO is policy too — evidence must not be able to re-open the question"
);
assert_eq!(im.len(), 3);
}
}