Files
boxmaker/docs/plans/M5a/files/crates/memoryd/src/journal.rs
T
kyleandClaude Opus 5.5 de1032157b Plan M5a: local memory in 15 tasks
memoryd (files, the check, edits, journal, index, recall, serving),
the memory tools and built-in grants in brokerd, the memory block in
loopd, bxctl reindex and memory, and the device checks with memoryd.

Each task was built as a reference and gated. Each was replayed on its
own at its end state (854 to 936 tests) and mutation-checked. Each
skeleton compiles and fails its tests. Every todo comment was read
beside its signature.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 15:32:54 -07:00

99 lines
4.6 KiB
Rust

//! The edit journal: one JSON line per applied edit in `memory/edits/YYYY-MM.jsonl`, the owner's
//! record of every write, and `undo` of the latest edit of a file (M5a spec, section 5).
use serde::{Deserialize, Serialize};
use proto::{MemoryFile, Timestamp};
use crate::edit::{Context, Outcome};
use crate::files::Store;
use crate::notes::Note;
/// One line of the journal.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Record {
/// `YYYY-MM-DD-NNNN`, numbered from 1 each day.
pub id: String,
pub time: String,
pub session: String,
pub call: u64,
/// `add`, `replace`, `remove`, `consolidate`, `note`, or `undo`.
pub action: String,
/// The identity file changed, if any.
pub file: Option<String>,
/// Where the write went: `memory.md`, `user.md` or `notes.md`.
pub to: String,
pub before: Vec<String>,
pub after: Vec<String>,
/// The `notes.md` entries appended, as written.
pub notes: Vec<String>,
pub untrusted: bool,
/// For an undo, the edit it reverted.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub undoes: Option<String>,
}
fn file_of(name: &str) -> Option<MemoryFile> {
match name {
"memory.md" => Some(MemoryFile::Memory),
"user.md" => Some(MemoryFile::User),
_ => None,
}
}
/// Every record, oldest first, from every month's file.
pub fn records(store: &Store) -> Result<Vec<Record>, String> {
// Read the directory `store.path("edits")`: NotFound -> Ok(empty); another error ->
// Err(format!("{}: {e}", dir.display())). Take the names that end ".jsonl" and sort them. For
// each file, `read_to_string` and parse every line as a Record with serde_json; a bad line ->
// Err(format!("{}: line {}: {e}", path.display(), n + 1)) with n counted from 0. Return all
// records, oldest first.
todo!()
}
/// Append a record, giving it the next id of its day.
pub(crate) fn append(store: &Store, mut record: Record, now: Timestamp) -> Result<String, String> {
// 1. `day` is the first 10 characters of `now.to_rfc3339()` ("0000-00-00" if shorter).
// 2. `taken` = how many of `records(store)?` have an id starting format!("{day}-").
// 3. record.id = format!("{day}-{:04}", taken + 1).
// 4. `create_dir_all(store.path("edits"))` (an error names the directory),
// `serde_json::to_string(&record)`, then `store.append(&format!("edits/{month}.jsonl"),
// &format!("{line}\n"))?` where month is the first 7 characters of day. Ok(record.id).
todo!()
}
/// Journal an applied call; the id it was given.
pub fn record(store: &Store, ctx: &Context, outcome: &Outcome) -> Result<String, String> {
// `append` a Record: id empty (append sets it), time `ctx.now.to_rfc3339()`, session and call
// from ctx, action `outcome.action`, file `change.file` as its name, to that name or "notes.md"
// when there is no file, before and after from the change, notes the change's notes each
// `render()`ed, untrusted `ctx.untrusted`, undoes None; `now` is ctx.now.
todo!()
}
/// The last `count` records, one line each, oldest first.
pub fn log(store: &Store, count: usize) -> Result<String, String> {
// The last `count` of `records(store)?` (all of them when fewer), oldest first, one line each:
// format!("{} {} {} {} {}{}", id, time, session, action, to, and " untrusted" when the
// record is untrusted), joined by newlines. With no records at all: "no memory edits yet".
todo!()
}
/// Revert edit `id`, if it is the latest edit of its identity file and the file still holds
/// exactly what it left. Notes it appended stay (the file is append-only). The undo is journalled.
pub fn undo(store: &Store, id: &str, now: Timestamp) -> Result<String, String> {
// 1. Find the record with this id in `records(store)?`, else Err(format!("no edit {id}")).
// 2. Its file must be Some and known to `file_of`, else Err(format!("{id} changed no identity
// file; notes are append-only")).
// 3. The latest record whose file is the same name must be this one, else Err(format!("{id} is
// not the latest edit of {name}")).
// 4. `store.entries(file)?` must equal record.after, else Err(format!("{name} has changed since
// {id}; nothing was undone")).
// 5. `store.write_entries(file, &record.before)?`, then `append` a Record: session "owner",
// call 0, action "undo", file and to the name, before the current entries, after
// record.before, no notes, untrusted false, undoes Some(id).
// 6. Ok(format!("undone: {id} ({name} restored; journalled as {undo_id})")).
todo!()
}