Expand description
The typed event substrate: the existing append-only ledger, read and written with types.
§Why this is not a new store
ADR 0052 says it in as many words and this module keeps the promise: the append-only
ledger at .ai/local/state/ledger.jsonl and the record directories beside it are the
substrate, and the write path moves under the domain rather than being duplicated
beside it. Every defect this subsystem has produced has the same shape — two accounts of
one fact — and a second store would be the largest instance of it yet.
§Why JSONL and not a database
The requirement was stated as concurrency-safe append, corruption detection and recovery. It was not stated as query, transactions across records, or indexes, and measuring what the ledger is actually asked for bears that out: every reader in the tree walks the file once and filters. So the question is whether a locked append-only text file meets the three requirements, and it does:
- Concurrent append. A write opened
O_APPENDis positioned and written in one operation by the kernel, so two processes cannot interleave at the offset; belowPIPE_BUFthe write itself is atomic as well.Ledger::appendadditionally takes an exclusiveflockfor the duration, which covers a long line and a filesystem with weaker guarantees. A ledger line is one line, so there is no cross-line transaction to want. - Corruption detection. The unit of corruption is a line, because the format has no
cross-line state.
Ledger::readparses line by line and reports each unparseable one with its number and what was wrong with it. - Recovery. A file whose last line was truncated by a machine losing power is still the complete record of everything before it. Reading skips the bad line, counts it, and names it; nothing fails, because a ledger with one truncated line that refuses to be read is worse than one that reports the truncation.
A database would add a binary file to a store whose value is partly that a person can
tail -f it, a second thing to migrate at cutover, a locking model that behaves no
better than flock over a network filesystem, and a schema that would immediately
become a second account of share/events.yaml. It is refused, and ADR 0047 records the
refusal so that it is a decision rather than an omission.
§The vocabulary is not this module’s
share/events.yaml declares every event name the ledger accepts and what each one must
carry, and mj_ledger_append already refuses an unregistered name. Vocabulary reads
that file. It does not contain a list of event names, and a test below asserts that the
names this domain writes are ones the shipped file declares — which is the assertion
that fails if somebody adds an event here and forgets the vocabulary, rather than a
durable line every reader silently ignores.
use majordomus_cli::session::{Envelope, Ledger, LedgerEvent, Vocabulary};
let dir = tempfile::tempdir().unwrap();
let ledger = Ledger::at(dir.path().join("ledger.jsonl"));
let vocabulary = Vocabulary::parse(
"version: 1\nevents:\n - id: session.started\n requires: [owner]\n public: true\n",
).expect("a vocabulary");
let envelope = Envelope {
ts: "2026-09-11T11:02:35Z".into(), head: "1a651e64".into(),
branch: "master".into(), by: "majordomus/0.5.0".into(), session: None,
};
ledger.append(&LedgerEvent::new("session.started").with("owner", "korczis"), &vocabulary, &envelope).unwrap();
assert_eq!(ledger.read().events.len(), 1);
// and a name nothing declares never becomes a durable line
assert!(ledger.append(&LedgerEvent::new("session.invented"), &vocabulary, &envelope).is_err());Structs§
- Corruption
- One line that could not be read as an event: which line it was, and what was wrong with it.
- Declared
Event - One declared event: what it is called, what writes it, what it must carry, and whether it is part of the documented record a user reads back.
- Envelope
- The computed half of a ledger line: what the writer knows and the event does not.
- Ledger
- The append-only ledger of one checkout: the canonical record of what happened, written only by Majordomus and in the order the commands ran.
- Ledger
Event - One event on its way into the ledger: the name, the payload, and the envelope the writer computes.
- Ledger
Read - What a read of the ledger found: the lines that parsed, in ledger order, and the lines that did not, named rather than dropped.
- Vocabulary
- The event vocabulary, read from
share/events.yaml.
Enums§
- Ledger
Error - What can go wrong writing or reading the ledger. The first two are refusals of a malformed event and happen before anything is written; the last two are the disk.
Constants§
- EVENTS_
FILE share/events.yaml, relative to the share directory.- LEDGER_
PATH - The ledger, relative to the repository root.