review: an undo concludes again only what it had deleted

Undoing a consolidate concluded its moved entries a second time, and a later
removal deleted only the newer conclusion. An undo's added entry is now
concluded only when the ledger decided a Conclude for it and then an
Unconclude: an entry still concluded, or never concluded (a secret session's,
one from before sync), gets nothing. Ledger::concluded and unconcluded answer
from the decisions, in a module of their own to keep ledger.rs under 500 lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:06:28 -07:00
co-authored by Claude Opus 5.5
parent b392af9ca4
commit 7c207f6ec6
4 changed files with 283 additions and 3 deletions
+23 -3
View File
@@ -185,6 +185,22 @@ pub fn actions(
kept
}
/// Whether an undo's `action` is kept: an entry it brings back is concluded again only when its
/// conclusion was made and then deleted (M5c spec, section 4: "any it had deleted, written
/// again"). One never concluded (a secret session's, from before sync, skipped) or still
/// concluded (moved out by `consolidate`) gets nothing. Every other action is kept.
fn brought_back(ledger: &Ledger, action: &Action) -> bool {
match action {
Action::Conclude {
workspace,
peer,
text,
..
} => ledger.unconcluded(*workspace, peer, text),
_ => true,
}
}
/// With `[honcho]`: decides each journalled edit into the ledger.
pub struct Mirror {
ledger: Arc<Mutex<Ledger>>,
@@ -207,8 +223,9 @@ impl Mirror {
}
/// Decide the journalled edit `id` (the latest when `None`), called under the index lock
/// right after it was journalled, so the ledger has the journal's order. Its parts are
/// `Edit { id, part }` from 0. An edit already decided, or asking nothing, adds no line. A
/// right after it was journalled, so the ledger has the journal's order; the ledger's lock is
/// taken inside the index lock, and before an undo is decided against it. Its parts are
/// `Edit { id, part }` from 0, numbering what is decided. An edit already decided, or asking nothing, adds no line. A
/// failure is logged; the edit itself stands.
pub fn journalled(&self, store: &Store, id: Option<&str>, now: Timestamp, log: &dyn Fn(&str)) {
let records = match journal::records(store) {
@@ -229,8 +246,11 @@ impl Mirror {
return;
};
let session = session_of(record, &records);
let actions = actions(record, session.as_ref(), &self.agent, &self.owner);
let mut book = self.ledger.lock().unwrap_or_else(|p| p.into_inner());
let mut actions = actions(record, session.as_ref(), &self.agent, &self.owner);
if record.undoes.is_some() {
actions.retain(|a| brought_back(&book, a));
}
for (part, action) in actions.into_iter().enumerate() {
let Ok(part) = u32::try_from(part) else {
return;