pub struct Ledger {
pub version: u32,
pub executions: Vec<Execution>,
}Expand description
§Example
use majordomus_cli::evidence::Ledger;
let l = Ledger::empty();
assert_eq!(l.summary().executions, 0);
assert!(l.latest("suite:07_scope").is_none());The latest execution of every test this repository has recorded.
One entry per test, never a history: the question it answers (“is this claim proven
now?”) only ever reads the newest, and a run per commit for a hundred tests would grow
without bound. It is a value first and a file second — Ledger::load and
Ledger::save move it to and from LEDGER_PATH under a repository root, and a
repository with no file has an empty one rather than an error.
use majordomus_cli::evidence::ledger::{Ledger, LEDGER_PATH, LEDGER_VERSION};
let mut ledger = Ledger::empty();
assert_eq!(ledger.version, LEDGER_VERSION);
assert_eq!(LEDGER_PATH, ".ai/repo/evidence/ledger.json");
ledger.merge([execution("07_scope", Outcome::Pass)]);
assert_eq!(ledger.executions.len(), 1);
assert_eq!(ledger.latest("suite:07_scope").unwrap().outcome, Outcome::Pass);Fields§
§version: u32The shape’s version.
executions: Vec<Execution>The executions, one per test, ordered by test id so the file diffs cleanly.
Implementations§
Source§impl Ledger
impl Ledger
Sourcepub fn empty() -> Ledger
pub fn empty() -> Ledger
An empty ledger — what a repository that has recorded nothing has.
Indistinguishable from a missing file everywhere it matters: Ledger::load
answers one for the other, Ledger::summary reports present: false for both,
and both make every claim read not run, which is the honest answer in either
case. Only Ledger::present, which asks the filesystem, tells them apart.
use majordomus_cli::evidence::ledger::{Ledger, LEDGER_VERSION};
let ledger = Ledger::empty();
assert!(ledger.executions.is_empty());
assert_eq!(ledger.version, LEDGER_VERSION, "it still declares the shape it is");
assert!(!ledger.summary().present, "nothing recorded is not a present ledger");Sourcepub fn parse(text: &str) -> Result<Ledger, Error>
pub fn parse(text: &str) -> Result<Ledger, Error>
Read a ledger from its text, wherever the text came from: the working copy under a repository root, or the file as a commit holds it.
The one reading of the shape, so that a ledger read from a commit and a ledger read from the working tree are refused for the same reasons in the same words. A version this executable does not know is refused rather than reinterpreted.
use majordomus_cli::evidence::ledger::{Ledger, LEDGER_VERSION};
let ledger = Ledger::parse(r#"{"version": 1, "executions": []}"#).unwrap();
assert_eq!(ledger.version, LEDGER_VERSION);
assert!(ledger.executions.is_empty());
let refused = Ledger::parse(r#"{"version": 99, "executions": []}"#).unwrap_err();
assert!(refused.to_string().contains("version 99"), "{refused}");
assert!(Ledger::parse("not json at all").is_err());Sourcepub fn load(root: &Path) -> Result<Ledger, Error>
pub fn load(root: &Path) -> Result<Ledger, Error>
Read the ledger of a repository. A repository with no ledger has an empty one; that is not an error, and the summary says which it was.
A ledger declaring a version this executable does not know is refused rather than
reinterpreted: an absent ledger reports not run, and a misread one could report a
pass that was never recorded.
use majordomus_cli::evidence::ledger::{Ledger, LEDGER_PATH};
let root = tempfile::tempdir().unwrap();
assert_eq!(Ledger::load(root.path()).unwrap(), Ledger::empty());
std::fs::create_dir_all(root.path().join(".ai/repo/evidence")).unwrap();
std::fs::write(
root.path().join(LEDGER_PATH),
r#"{"version": 99, "executions": []}"#,
)
.unwrap();
let refused = Ledger::load(root.path()).unwrap_err().to_string();
assert!(refused.contains("version 99"), "{refused}");Sourcepub fn present(root: &Path) -> bool
pub fn present(root: &Path) -> bool
Whether the file is there at all, as opposed to there and empty.
use majordomus_cli::evidence::Ledger;
let root = tempfile::tempdir().unwrap();
assert!(!Ledger::present(root.path()));
Ledger::empty().save(root.path()).unwrap();
assert!(Ledger::present(root.path()), "written and empty is still written");Sourcepub fn latest(&self, test: &str) -> Option<&Execution>
pub fn latest(&self, test: &str) -> Option<&Execution>
The latest recorded execution of one test, by super::TestId::as_string.
None is the answer for a test no run has ever recorded, which the report renders
as not run rather than as a failure.
use majordomus_cli::evidence::Ledger;
let mut ledger = Ledger::empty();
ledger.merge([execution("07_scope", Outcome::Fail)]);
assert_eq!(ledger.latest("suite:07_scope").unwrap().outcome, Outcome::Fail);
assert!(ledger.latest("suite:99_ghost").is_none(), "never run, never recorded");Sourcepub fn merge(
&mut self,
executions: impl IntoIterator<Item = Execution>,
) -> usize
pub fn merge( &mut self, executions: impl IntoIterator<Item = Execution>, ) -> usize
Merge executions into the ledger: each one replaces the entry for its test and leaves every other alone.
A partial run therefore updates only what it ran, which is what makes bash test/run.sh 84_distribution_model worth recording. The alternative — a whole-ledger
write per run — would silently delete the evidence for every test the run did not
include, turning a one-case run into a repository that has proven one thing.
use majordomus_cli::evidence::Ledger;
let mut ledger = Ledger::empty();
ledger.merge([
execution("07_scope", Outcome::Pass),
execution("08_other", Outcome::Pass),
]);
// a second run of one case replaces that case and touches nothing else
assert_eq!(ledger.merge([execution("07_scope", Outcome::Fail)]), 1);
assert_eq!(ledger.executions.len(), 2, "it replaced rather than appended");
assert_eq!(ledger.latest("suite:07_scope").unwrap().outcome, Outcome::Fail);
assert_eq!(
ledger.latest("suite:08_other").unwrap().outcome,
Outcome::Pass,
"recording one test must never erase another test's evidence",
);Sourcepub fn save(&self, root: &Path) -> Result<(), Error>
pub fn save(&self, root: &Path) -> Result<(), Error>
Write the ledger, creating its directory. Trailing newline, two-space indent: a tracked file a person reads in a diff.
use majordomus_cli::evidence::ledger::{Ledger, LEDGER_PATH};
let root = tempfile::tempdir().unwrap();
let mut ledger = Ledger::empty();
ledger.merge([execution("07_scope", Outcome::Pass)]);
ledger.save(root.path()).unwrap();
let text = std::fs::read_to_string(root.path().join(LEDGER_PATH)).unwrap();
assert!(text.ends_with('\n'), "a tracked file ends in a newline");
assert!(text.contains("\n \"version\": 1"), "two-space indent: {text}");
assert_eq!(Ledger::load(root.path()).unwrap(), ledger);Sourcepub fn load_from(root: &Path, target: LedgerTarget) -> Result<Ledger, Error>
pub fn load_from(root: &Path, target: LedgerTarget) -> Result<Ledger, Error>
Read the ledger target names. An absent one is empty, as with Ledger::load, and
a bad one is refused in its own name.
use majordomus_cli::evidence::ledger::{Ledger, LedgerTarget, LOCAL_LEDGER_PATH};
let root = tempfile::tempdir().unwrap();
assert_eq!(Ledger::load_from(root.path(), LedgerTarget::Local).unwrap(), Ledger::empty());
Ledger::empty().save_to(root.path(), LedgerTarget::Local).unwrap();
assert!(root.path().join(LOCAL_LEDGER_PATH).is_file());
assert!(!Ledger::present(root.path()), "the tracked ledger is not written");Sourcepub fn save_to(&self, root: &Path, target: LedgerTarget) -> Result<(), Error>
pub fn save_to(&self, root: &Path, target: LedgerTarget) -> Result<(), Error>
Write the ledger target names, as Ledger::save writes the tracked one.
use majordomus_cli::evidence::ledger::{Ledger, LedgerTarget, LEDGER_PATH};
let root = tempfile::tempdir().unwrap();
Ledger::empty().save_to(root.path(), LedgerTarget::Repo).unwrap();
assert!(root.path().join(LEDGER_PATH).is_file());Sourcepub fn summary(&self) -> LedgerSummary
pub fn summary(&self) -> LedgerSummary
What the ledger is, for a report that has to say where its evidence came from: where it lives, how much it holds, how new the newest of it is, and every distinct commit its executions were recorded against.
use majordomus_cli::evidence::ledger::{Ledger, LEDGER_PATH};
let mut ledger = Ledger::empty();
ledger.merge([
execution("07_scope", Outcome::Pass),
execution("08_other", Outcome::Fail),
]);
let summary = ledger.summary();
assert_eq!(summary.path, LEDGER_PATH);
assert_eq!(summary.executions, 2);
assert_eq!(summary.newest.as_deref(), Some("2026-09-11T00:00:00Z"));
assert_eq!(summary.commits, ["0".repeat(40)], "distinct commits, deduplicated");
assert!(!Ledger::empty().summary().present);Sourcepub fn by_outcome(&self) -> BTreeMap<String, usize>
pub fn by_outcome(&self) -> BTreeMap<String, usize>
How many executions there are of each outcome, for a one-line summary. An outcome nothing recorded is absent from the map rather than present as a zero.
use majordomus_cli::evidence::Ledger;
let mut ledger = Ledger::empty();
ledger.merge([
execution("07_scope", Outcome::Pass),
execution("08_other", Outcome::Fail),
execution("09_third", Outcome::Pass),
]);
let counts = ledger.by_outcome();
assert_eq!(counts.get("pass"), Some(&2));
assert_eq!(counts.get("fail"), Some(&1));
assert_eq!(counts.get("skip"), None, "an outcome nothing recorded is absent");Trait Implementations§
Source§impl<'de> Deserialize<'de> for Ledger
impl<'de> Deserialize<'de> for Ledger
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for Ledger
impl StructuralPartialEq for Ledger
Auto Trait Implementations§
impl Freeze for Ledger
impl RefUnwindSafe for Ledger
impl Send for Ledger
impl Sync for Ledger
impl Unpin for Ledger
impl UnsafeUnpin for Ledger
impl UnwindSafe for Ledger
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.