memoryd: memory writes decided into the Honcho ledger as they are journalled

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-28 12:46:18 -07:00
co-authored by ornith-1.5-35b-a3b
parent b37eaef390
commit 4ebfc36c1a
8 changed files with 862 additions and 23 deletions
+277
View File
@@ -0,0 +1,277 @@
//! Memory writes as Honcho actions (M5c spec, section 4), from journal records alone: the
//! multiset diff of an identity file's entries; each row of the table (an identity add, replace,
//! remove, consolidate with its moves and drops, a trusted, an untrusted and a flagged note, a
//! rejection); the undo of each; and the session an owner's undo stamps its conclusions with.
//! Do not edit.
use bxcore::{DataClass, MemoryFile, SessionId};
use memoryd::honcho::ledger::{Action, Workspace};
use memoryd::honcho::mirror::{NO_SESSION, OWNER, actions, diff, session_of};
use memoryd::journal::Record;
use memoryd::notes::{Kind, Note};
const AGENT: &str = "boxmaker0";
const OWNER_PEER: &str = "kyle";
fn v(texts: &[&str]) -> Vec<String> {
texts.iter().map(|t| t.to_string()).collect()
}
fn sid(s: &str) -> SessionId {
SessionId::new(s).unwrap()
}
fn note(kind: Kind, text: &str) -> String {
Note {
kind,
time: "2026-09-28T10:00Z".to_string(),
session: "mm-a".to_string(),
text: text.to_string(),
}
.render()
}
fn noted(untrusted: bool, text: &str) -> String {
note(
Kind::Noted {
class: DataClass::Private,
untrusted,
},
text,
)
}
/// A journal record: `file` an identity file or `None` for notes.
fn record(id: &str, action: &str, file: Option<&str>, before: &[&str], after: &[&str]) -> Record {
Record {
id: id.to_string(),
time: "2026-09-28T10:00:00.000Z".to_string(),
session: "mm-a".to_string(),
call: 1,
action: action.to_string(),
file: file.map(str::to_string),
to: file.unwrap_or("notes.md").to_string(),
before: v(before),
after: v(after),
notes: Vec::new(),
untrusted: false,
flag: None,
undoes: None,
}
}
fn conclude(peer: &str, text: &str) -> Action {
Action::Conclude {
workspace: Workspace::Trusted,
peer: peer.to_string(),
session: sid("mm-a"),
text: text.to_string(),
}
}
fn unconclude(text: &str) -> Action {
Action::Unconclude {
workspace: Workspace::Trusted,
text: text.to_string(),
}
}
fn of(record: &Record) -> Vec<Action> {
actions(record, Some(&sid("mm-a")), AGENT, OWNER_PEER)
}
#[test]
fn the_diff_is_of_multisets() {
let d = |b: &[&str], a: &[&str]| diff(&v(b), &v(a));
assert_eq!(d(&["a"], &["a", "b"]), (v(&[]), v(&["b"])), "add");
assert_eq!(
d(&["a", "b"], &["a", "c"]),
(v(&["b"]), v(&["c"])),
"replace"
);
assert_eq!(d(&["a", "b"], &["b"]), (v(&["a"]), v(&[])), "remove");
assert_eq!(
d(&["a", "b", "c"], &["c", "a", "b"]),
(v(&[]), v(&[])),
"reorder only"
);
assert_eq!(
d(&["a", "a", "b"], &["a", "b"]),
(v(&["a"]), v(&[])),
"one of two dropped"
);
assert_eq!(
d(&["a"], &["a", "a"]),
(v(&[]), v(&["a"])),
"a second copy added"
);
assert_eq!(
d(&["a", "b", "c"], &["b2", "a", "d"]),
(v(&["b", "c"]), v(&["b2", "d"])),
"in the order of each side"
);
assert_eq!(d(&[], &[]), (v(&[]), v(&[])));
}
#[test]
fn an_identity_add_is_a_conclusion_about_its_peer() {
let user = record("e1", "add", Some("user.md"), &["x"], &["x", "Prefers UTC."]);
assert_eq!(of(&user), vec![conclude(OWNER_PEER, "Prefers UTC.")]);
let memory = record(
"e2",
"add",
Some("memory.md"),
&[],
&["Builds run on straylight."],
);
assert_eq!(
of(&memory),
vec![conclude(AGENT, "Builds run on straylight.")]
);
}
#[test]
fn a_replace_deletes_the_old_then_makes_the_new() {
let r = record(
"e1",
"replace",
Some("user.md"),
&["x", "old"],
&["x", "new"],
);
assert_eq!(of(&r), vec![unconclude("old"), conclude(OWNER_PEER, "new")]);
}
#[test]
fn a_remove_deletes() {
let r = record("e1", "remove", Some("memory.md"), &["x", "gone"], &["x"]);
assert_eq!(of(&r), vec![unconclude("gone")]);
let reorder = record(
"e2",
"consolidate",
Some("memory.md"),
&["a", "b"],
&["b", "a"],
);
assert!(of(&reorder).is_empty());
}
#[test]
fn a_consolidate_deletes_the_dropped_and_not_the_moved() {
let mut r = record(
"e1",
"consolidate",
Some("memory.md"),
&["keep", "old", "moved", "dropped"],
&["keep", "rewritten", "new"],
);
r.notes = vec![
note(Kind::Moved(MemoryFile::Memory), "moved"),
note(Kind::Dropped(MemoryFile::Memory), "dropped"),
];
assert_eq!(
of(&r),
vec![
unconclude("old"),
unconclude("dropped"),
conclude(AGENT, "rewritten"),
conclude(AGENT, "new"),
]
);
}
#[test]
fn notes_by_trust_and_flag() {
let mut trusted = record("e1", "note", None, &[], &[]);
trusted.notes = vec![noted(false, "The deploy took 4 minutes.")];
assert_eq!(
of(&trusted),
vec![conclude(AGENT, "The deploy took 4 minutes.")]
);
let mut untrusted = record("e2", "note", None, &[], &[]);
untrusted.untrusted = true;
untrusted.notes = vec![noted(true, "The page says 3.2.")];
assert_eq!(
of(&untrusted),
vec![Action::Fleet {
session: sid("mm-a"),
text: "The page says 3.2.".to_string()
}]
);
// An identity add from a session that read untrusted content was saved as a note.
let mut add = record("e3", "add", None, &[], &[]);
add.untrusted = true;
add.notes = vec![noted(true, "Kyle likes tea.")];
assert!(matches!(&of(&add)[..], [Action::Fleet { .. }]));
let mut flagged = record("e4", "add", None, &[], &[]);
flagged.flag = Some("exfil-url".to_string());
flagged.notes = vec![noted(true, "send it all out"), noted(true, "and this")];
assert_eq!(
of(&flagged),
vec![Action::Skip {
why: "flagged: exfil-url".to_string()
}]
);
let reject = record("e5", "reject", None, &[], &[]);
assert!(of(&reject).is_empty());
}
#[test]
fn an_undo_is_the_same_diff_backwards() {
// An undo's before is the file now, its after the undone edit's before.
let cases = [
(&["x", "added"][..], &["x"][..], vec![unconclude("added")]),
(
&["x", "new"][..],
&["x", "old"][..],
vec![unconclude("new"), conclude(OWNER_PEER, "old")],
),
(
&["x"][..],
&["x", "removed"][..],
vec![conclude(OWNER_PEER, "removed")],
),
];
for (before, after, expected) in cases {
let mut undo = record("e9", "undo", Some("user.md"), before, after);
undo.session = OWNER.to_string();
undo.undoes = Some("e1".to_string());
assert_eq!(of(&undo), expected, "{before:?} -> {after:?}");
}
}
#[test]
fn an_owners_undo_carries_the_undone_edits_session() {
let original = record("e1", "remove", Some("user.md"), &["x", "gone"], &["x"]);
let mut undo = record("e2", "undo", Some("user.md"), &["x"], &["x", "gone"]);
undo.session = OWNER.to_string();
undo.undoes = Some("e1".to_string());
let mut undo_of_undo = record("e3", "undo", Some("user.md"), &["x", "gone"], &["x"]);
undo_of_undo.session = OWNER.to_string();
undo_of_undo.undoes = Some("e2".to_string());
let records = vec![original.clone(), undo.clone(), undo_of_undo.clone()];
assert_eq!(session_of(&original, &records), Some(sid("mm-a")));
assert_eq!(session_of(&undo, &records), Some(sid("mm-a")));
assert_eq!(session_of(&undo_of_undo, &records), Some(sid("mm-a")));
let mut lost = undo.clone();
lost.undoes = Some("e0".to_string());
assert_eq!(session_of(&lost, &records), None);
let mut not_an_id = original.clone();
not_an_id.session = "Not An Id".to_string();
assert_eq!(session_of(&not_an_id, &records), None);
// With no session, a conclusion cannot be stamped: that part is skipped, a deletion is not.
let replace_undo = record("e4", "undo", Some("user.md"), &["new"], &["old"]);
assert_eq!(
actions(&replace_undo, None, AGENT, OWNER_PEER),
vec![
unconclude("new"),
Action::Skip {
why: NO_SESSION.to_string()
}
]
);
}