Skip to main content

Module ledger

Module ledger 

Source
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_APPEND is positioned and written in one operation by the kernel, so two processes cannot interleave at the offset; below PIPE_BUF the write itself is atomic as well. Ledger::append additionally takes an exclusive flock for 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::read parses 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.
DeclaredEvent
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.
LedgerEvent
One event on its way into the ledger: the name, the payload, and the envelope the writer computes.
LedgerRead
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§

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