utfallsrum — adoption guide (how to score a function's outcome space)
A 6-step recipe to give any function first-class outcome-space coverage.
1. Identify the outcome space
List the equivalence partitions + boundary values of the function's output. Derive them from the function's OWN branches/clamps/divides + the degenerate cases of its types (the same analysis the (input boundaries → exact contract bound) table in agent-ui-test-fixing.md drives). For clamp01(x):
| class | contract (a correct output) |
below-0 |
input < 0 ⇒ output == 0.0 |
in-range |
0 ≤ input ≤ 1 ⇒ output == input |
above-1 |
input > 1 ⇒ output == 1.0 |
2. Declare it
use nornir_testmatrix::utfallsrum::Outcome;
let mut o = Outcome::for_fn("clamp01")
.class("below-0", "input < 0 ⇒ output == 0.0")
.class("in-range", "0 ≤ input ≤ 1 ⇒ output == input")
.class("above-1", "input > 1 ⇒ output == 1.0");
3. Sweep + record — one case per class, with the EXACT output
Assert the contract for a representative value of EACH class (boundaries first), and record the class with the exact asserted output as evidence:
assert_eq!(clamp01(-0.5), 0.0); o.hit("below-0", "clamp01(-0.5) == 0.0");
assert_eq!(clamp01(0.3), 0.3); o.hit("in-range", "clamp01(0.3) == 0.3");
assert_eq!(clamp01(1.7), 1.0); o.hit("above-1", "clamp01(1.7) == 1.0");
(Want a full grid? Sweep many values per class as usual — the SCORE moves with the number of CLASSES covered, so cover every class at least once; extra values inside a class harden the assertion but don't raise the score.)
4. Score + assert it
assert_eq!(o.covered(), 1.0); // every declared class swept
assert!(o.is_complete());
assert!(o.summary().meets_threshold(2)); // ≥2 classes — passes the gate bar
A partial sweep is honest: o.covered() == 2.0/3.0, o.unexercised() == ["above-1"].
5. Emit it into the matrix
#[cfg(feature = "testmatrix")]
o.emit("my-component"); // per-class rows + a scored `<fn>/utfallsrum` roll-up row
nornir test drains those into Aspect::Functional rows, so the warehouse carries the outcome-space coverage; release builds (feature off) strip the call to a no-op.
6. Let the gate consume it (surface coverage)
For a UI/CLI/MCP surface, map surface_key → o.summary() and feed it to the gate's covered-set computation:
use nornir_testmatrix::coverage::{covered_with_utfallsrum, rows_for_with_utfallsrum, DEFAULT_UTFALLSRUM_THRESHOLD};
let covered = covered_with_utfallsrum(&ran, &utfallsrum, DEFAULT_UTFALLSRUM_THRESHOLD);
let rows = rows_for_with_utfallsrum(run_id, ws, &surface, &covered, &allowlist, &utfallsrum, ts);
// nornir: append_surface_coverage(wh, &rows) → persisted with utfallsrum_covered.
A surface is covered only when it ran end-to-end AND swept ≥K classes — a one-value smoke stays in the gap until the rest of its outcome space is swept.
The two-axis rule (why this matters)
Every check is assert(invariant(f(input))) and has two orthogonal ways to be worthless: weak input coverage (one value) and a loose oracle (a slack threshold). utfallsrum measures the FIRST axis as a number — it cannot be faked by asserting harder on one input. Combine it with the contract-tight oracle (Axis B in agent-ui-test-fixing.md) for a check that is both broad and exact.
Reference
Worked end-to-end slice: tests/viz_utfallsrum_slice.rs (two workspaces × {Test,Bench}@{fat,thin}, clean-slate→populate→verify, recording one/many classes across the workspace pair, scoring + persisting). Design: testmatrix-utfallsrum.md.