Expand description
The ledger: the append-only record of what happened to this checkout, and the one writer of it in this executable.
lib/common.sh’s mj_ledger_append has been the only writer since the record existed.
This is the second, and it exists for one reason: a capability of kind command must be
able to record what it did, and a capability cannot shell out to the tool that calls it.
Two writers of one record is the failure this repository exists to prevent, so the two
are held to the same contract rather than left to drift:
- the same file —
.ai/local/state/ledger.jsonl, never tracked, created on first write; - the same envelope —
ts,event,head,branch,by, thensessionwhen this checkout has an open episode, then the event’s own payload, in that order;headisNONEin an unborn repository andbranchisDETACHEDoff a branch, because that is what the shell writes and a reader may not meet two spellings; - the same vocabulary —
share/events.yamldeclares every name the ledger accepts and the payload keys each one must carry. An unregistered name is an error here exactly as it is there: before that file existed a mistyped name produced a durable line that every reader silently ignored.
test/cases/133_plan_transition.sh holds the two writers to byte equality: it moves one
issue through each engine and compares the envelope and the key order of the lines they
append. Where they disagree the shell is the incumbent and is right; this file is the one
that changes.
use majordomus_cli::git::{GitInfo, GitState};
use majordomus_cli::ledger::{self, Vocabulary};
let vocabulary = Vocabulary::load(&dir.join("events.yaml")).unwrap();
let git = GitState::Available(GitInfo {
toplevel: dir.clone(), head: Some("a".repeat(40)),
branch: Some("master".into()), working_tree: "clean".into(),
});
// One line, appended; the envelope is composed in a fixed order and the payload follows.
let line = ledger::append(&dir, &vocabulary, &git, "2026-09-11T12:00:00Z",
"plan_start", &[("issue", "I0001".into())]).unwrap();
assert!(line.starts_with(r#"{"ts":"2026-09-11T12:00:00Z","event":"plan_start""#));
// A name nothing declares never reaches the file, which is why the vocabulary exists.
assert!(ledger::append(&dir, &vocabulary, &git, "2026-09-11T12:00:00Z",
"plan_strat", &[("issue", "I0001".into())]).is_err());
let (entries, skipped) = ledger::read(&dir);
assert_eq!((entries.len(), skipped), (1, 0));Structs§
- Entry
- One line of the record, as a reader sees it.
- Vocabulary
- The event vocabulary, read once and shared.
Enums§
- Ledger
Error - Why a line was not written.
Functions§
- append
- Append one line to the ledger of
root. - now
- The envelope’s timestamp, in the one spelling both writers use:
date -u +%Y-%m-%dT%H:%M:%SZ, to the second, always UTC. - read
- Read the record of
root, oldest first.
Type Aliases§
- Field
- One payload key and its value, as it will be written.