Skip to main content

Module ledger

Module ledger 

Source
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§

LedgerTarget
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 run and 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_outcome and a run record’s totals both use.