use super::entry::{short, Entry};
use super::store::{Memory, Status, Store};
const BANNER: &str =
"<!-- generated by `rustio-admin memory render` — edit entries in .rustio/memory/entries/, \
not this file. Contract: docs/design/DESIGN_CLOUD.md -->";
pub(crate) fn render(mem: &Memory) -> String {
let mut out = String::new();
out.push_str(BANNER);
out.push_str("\n\n# Project Memory\n\n");
out.push_str(
"> Non-authoritative project memory — the *why* behind this project's decisions.\n\
> Subordinate to code; on any conflict, the code wins. See `docs/design/DESIGN_CLOUD.md`.\n\n",
);
if mem.entries.is_empty() {
out.push_str("_No memory entries yet._\n");
return out;
}
for e in &mem.entries {
out.push_str(&render_entry(e, &mem.status_of(&e.id)));
out.push('\n');
}
out
}
fn render_entry(e: &Entry, status: &Status) -> String {
let mut s = String::new();
let foundational = if e.foundational {
" · ⭑ foundational"
} else {
""
};
s.push_str(&format!(
"## {} · {} · `{}`{}\n\n",
e.date,
e.entry_type.as_str(),
short(&e.id),
foundational
));
if !e.subjects.is_empty() {
s.push_str(&format!("*subjects:* {}\n\n", e.subjects.join(", ")));
}
match status {
Status::Active => {}
Status::Superseded(by) => {
s.push_str(&format!("> **Superseded** by `{}`.\n\n", short(by)));
}
Status::Forked(bys) => {
let list = bys
.iter()
.map(|b| format!("`{}`", short(b)))
.collect::<Vec<_>>()
.join(", ");
s.push_str(&format!(
"> **⚠ Open tension** — superseded by multiple entries ({list}); \
an unresolved fork. Resolve by appending a superseding entry.\n\n"
));
}
}
if e.entry_type == super::entry::EntryType::OpenTension {
s.push_str(
"> **⚠ Open tension** — records an unresolved disagreement; closes by supersession.\n\n",
);
}
if e.redacted {
s.push_str("> 🔒 **Redacted** — prohibited content was removed from this entry.\n\n");
}
s.push_str(&e.body);
s.push('\n');
if !e.sources.is_empty() {
s.push_str(&format!("\n*sources:* {}\n", e.sources.join(", ")));
}
s.push_str(&format!(
"\n<sub>recorded by {} · ratified by {} · audit {}</sub>\n",
e.author, e.ratified_by, e.correlation_id
));
s
}
pub(crate) fn write_view(store: &Store) -> Result<usize, String> {
let mem = Memory::build(store.load_entries()?)?;
let out = render(&mem);
let path = store.cloud_md_path();
std::fs::write(&path, &out).map_err(|e| format!("could not write {}: {e}", path.display()))?;
Ok(mem.entries.len())
}
pub(crate) fn verify(store: &Store) -> Result<(), String> {
let mem = Memory::build(store.load_entries()?)?;
let expected = render(&mem);
let actual = std::fs::read_to_string(store.cloud_md_path()).unwrap_or_default();
if actual != expected {
return Err(
"CLOUD.md is stale or missing — run `rustio-admin memory render` to regenerate it"
.to_string(),
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::ulid_gen::new_ulid;
fn store_with(entries: &[(&str, &str, &str, Option<&str>)]) -> Store {
let root = std::env::temp_dir().join(format!("rustio-render-{}", new_ulid()));
let dir = root.join(".rustio").join("memory").join("entries");
std::fs::create_dir_all(&dir).unwrap();
for (id, ty, date, sup) in entries {
let content = format!(
"+++\nid = \"{id}\"\ntype = \"{ty}\"\nsubjects = [\"core\"]\n\
supersedes = \"{}\"\nfoundational = false\nsources = []\n\
author = \"ai:test\"\nratified_by = \"t@e\"\ndate = \"{date}\"\n\
correlation_id = \"c\"\n+++\n\nBody of {id}.\n",
sup.unwrap_or("")
);
std::fs::write(dir.join(format!("{id}.md")), content).unwrap();
}
Store::new(root)
}
#[test]
fn empty_memory_renders_banner_and_placeholder() {
let s = store_with(&[]);
let mem = Memory::build(s.load_entries().unwrap()).unwrap();
let out = render(&mem);
assert!(out.contains(BANNER));
assert!(out.contains("_No memory entries yet._"));
}
#[test]
fn render_includes_entry_and_marks_superseded() {
let s = store_with(&[
("aaa", "assumption", "2026-01-01", None),
("bbb", "assumption", "2026-02-01", Some("aaa")),
]);
let mem = Memory::build(s.load_entries().unwrap()).unwrap();
let out = render(&mem);
assert!(out.contains("Body of aaa."));
assert!(out.contains("Body of bbb."));
assert!(out.contains("**Superseded** by"));
}
#[test]
fn render_marks_open_tension_for_fork() {
let s = store_with(&[
("aaa", "assumption", "2026-01-01", None),
("bbb", "assumption", "2026-02-01", Some("aaa")),
("ccc", "assumption", "2026-03-01", Some("aaa")),
]);
let mem = Memory::build(s.load_entries().unwrap()).unwrap();
let out = render(&mem);
assert!(out.contains("Open tension"));
}
#[test]
fn render_is_deterministic() {
let s = store_with(&[("aaa", "decision", "2026-01-01", None)]);
let mem = Memory::build(s.load_entries().unwrap()).unwrap();
assert_eq!(render(&mem), render(&mem));
}
#[test]
fn verify_flags_missing_then_passes_after_write() {
let s = store_with(&[("aaa", "decision", "2026-01-01", None)]);
assert!(verify(&s).is_err());
let mem = Memory::build(s.load_entries().unwrap()).unwrap();
std::fs::write(s.cloud_md_path(), render(&mem)).unwrap();
assert!(verify(&s).is_ok());
std::fs::write(s.cloud_md_path(), "tampered").unwrap();
assert!(verify(&s).is_err());
}
#[test]
fn verify_propagates_referential_errors() {
let s = store_with(&[("bbb", "assumption", "2026-02-01", Some("ghost"))]);
let err = verify(&s).unwrap_err();
assert!(err.contains("dangling link"), "{err}");
}
}