Skip to main content

Module ledger

Module ledger 

Source
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, then session when this checkout has an open episode, then the event’s own payload, in that order; head is NONE in an unborn repository and branch is DETACHED off a branch, because that is what the shell writes and a reader may not meet two spellings;
  • the same vocabulary — share/events.yaml declares 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§

LedgerError
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.