Expand description
The ledger: the latest execution of every test, as a tracked file.
§Why a file, and why this one
Evidence that lives only in a CI job’s log is evidence nobody can query, and evidence
that lives in a database is evidence a clone does not carry. The repository already has
a precedent for recorded, committed, reviewable measurement — the benchmark baselines
under .ai/repo/benchmarks/, which are JSON, per platform, written by a tool and read
by a gate. This is the same kind of artifact for the same reason, and it is deliberately
shaped like them rather than like something new.
§What it holds, and what it refuses to hold
One entry per test: the latest execution. Not a history — a run per commit for a hundred tests would grow without bound and the question it answers (“is this claim proven now?”) only ever reads the newest. Not stdout, not artifacts: a captured log of every case is megabytes per run and belongs where the run happened. What is kept is the durable semantic part — outcome, duration, commit, tree state, digest, time, origin, and the command that produces it again — which is what a reader needs to decide whether to believe it and how to check.
§JSON, not the layer’s YAML
This repository reads a deliberately small YAML subset (docs/SCHEMAS.md), and a file
only our own reader can parse is a trap this repository has already fallen into once.
The ledger is machine-written and machine-read, so it is JSON: serde on both sides,
no subset to respect, and jq works on it.
§The whole lifecycle
Load what is there (nothing, in a repository that has recorded nothing), merge the executions a run produced, save, and read back the same value:
use majordomus_cli::evidence::{Execution, Ledger, Origin, Outcome, Runner};
fn execution(name: &str, outcome: Outcome) -> Execution {
Execution {
test: format!("suite:{name}"),
runner: Runner::Suite,
source: format!("test/cases/{name}.sh"),
outcome,
seconds: 1,
commit: "0".repeat(40),
working_tree: "clean".into(),
digest: "sha256:0".into(),
at: "2026-09-11T00:00:00Z".into(),
origin: Origin::Local,
command: format!("bash test/run.sh {name}"),
run: None,
}
}
let root = tempfile::tempdir().unwrap();
// a repository that has recorded nothing has an empty ledger, and that is not an error
assert!(!Ledger::present(root.path()));
let mut ledger = Ledger::load(root.path()).unwrap();
assert!(ledger.executions.is_empty());
ledger.merge([
execution("08_other", Outcome::Fail),
execution("07_scope", Outcome::Pass),
]);
ledger.save(root.path()).unwrap();
// what came back is what went in, ordered by test id so the tracked file diffs cleanly
let read = Ledger::load(root.path()).unwrap();
assert_eq!(read, ledger);
let ids: Vec<&str> = read.executions.iter().map(|e| e.test.as_str()).collect();
assert_eq!(ids, ["suite:07_scope", "suite:08_other"]);
assert_eq!(read.summary().executions, 2);
assert_eq!(read.by_outcome().get("pass"), Some(&1));§Example
A tree that has recorded nothing has no ledger, and says so rather than refusing to answer — a repository that could not say “nothing was recorded” would say nothing.
use majordomus_cli::evidence::Ledger;
use majordomus_cli::synthetic::SyntheticRepository;
let repo = SyntheticRepository::small().unwrap();
let ledger = Ledger::load(repo.root()).unwrap();
assert!(!Ledger::present(repo.root()));
assert!(ledger.latest("suite:07_scope").is_none());Structs§
- Ledger
- Example
Enums§
- Ledger
Target - Which ledger a recording writes: the tracked one a verdict is derived from, or the local one nothing reads to judge anything.
Constants§
- LEDGER_
PATH - Where the ledger lives, repository-relative. One spelling, used by the recorder, the reader and the documentation.
- LEDGER_
VERSION - The version of the ledger’s own shape. A reader that meets a version it does not know
refuses rather than guessing: a misread ledger is worse than an absent one, because an
absent one reports
not runand a misread one could report a pass. - LOCAL_
LEDGER_ PATH - Where the untracked, per-checkout ledger lives: under
.ai/local/, which a repository ignores, so a recording made there changes no tracked file and moves no verdict.
Functions§
- outcome_
counts - How many of these executions there are of each outcome, keyed by the outcome’s serde
word; an outcome nothing recorded is absent rather than 0. The one count
Ledger::by_outcomeand a run record’s totals both use.