use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use anyhow::{bail, Context, Result};
use toml::Value;
use crate::memory::entries::StructuredMemoryEntry;
const OPENING_FENCE: &str = "```ccd-memory";
const CLOSING_FENCE: &str = "```";
pub fn append_block_to_contents(contents: &str, entry_block: &str) -> String {
if contents.trim().is_empty() {
return format!("{entry_block}\n");
}
format!(
"{}\n\n{entry_block}\n",
contents.trim_end_matches(['\n', '\r'])
)
}
pub fn rewrite_entry_blocks(
contents: &str,
replacements: &HashMap<String, String>,
) -> Result<String> {
let actions = replacements
.iter()
.map(|(id, replacement)| (id.clone(), Some(replacement.clone())))
.collect::<HashMap<_, _>>();
rewrite_with_actions(contents, &actions)
}
pub fn remove_entry_blocks(contents: &str, entry_ids: &[String]) -> Result<String> {
let actions = entry_ids
.iter()
.cloned()
.map(|id| (id, None))
.collect::<HashMap<_, _>>();
rewrite_with_actions(contents, &actions)
}
pub fn render_entry_block(entry: &StructuredMemoryEntry) -> String {
let mut lines = vec![
"```ccd-memory".to_owned(),
format!("id = {}", toml_string(&entry.id)),
format!("type = {}", toml_string(&entry.entry_type)),
format!("state = {}", toml_string(&entry.state)),
format!("created_at = {}", toml_string(&entry.created_at)),
format!("last_touched_session = {}", entry.last_touched_session),
format!("origin = {}", toml_string(&entry.origin)),
];
if let Some(superseded_at) = &entry.superseded_at {
lines.push(format!("superseded_at = {}", toml_string(superseded_at)));
}
if let Some(decay_class) = &entry.decay_class {
lines.push(format!("decay_class = {}", toml_string(decay_class)));
}
if let Some(expires_at) = &entry.expires_at {
lines.push(format!("expires_at = {}", toml_string(expires_at)));
}
if !entry.tags.is_empty() {
lines.push(format!("tags = {}", toml_string_array(&entry.tags)));
}
if let Some(source_ref) = &entry.source_ref {
lines.push(format!("source_ref = {}", toml_string(source_ref)));
}
if !entry.supersedes.is_empty() {
lines.push(format!(
"supersedes = {}",
toml_string_array(&entry.supersedes)
));
}
lines.push(format!("content = {}", toml_string(&entry.content)));
lines.push("```".to_owned());
lines.join("\n")
}
pub fn write_memory_file(path: &Path, contents: &str) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
}
fs::write(path, contents).with_context(|| format!("failed to write {}", path.display()))
}
fn parsed_block_id(payload: &str) -> Result<Option<String>> {
let raw = toml::from_str::<Value>(payload)
.context("failed to reparse validated `ccd-memory` block while rewriting memory file")?;
let Some(table) = raw.as_table() else {
bail!("validated `ccd-memory` block payload must remain a TOML table")
};
Ok(table
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned))
}
fn rewrite_with_actions(
contents: &str,
actions: &HashMap<String, Option<String>>,
) -> Result<String> {
if actions.is_empty() {
return Ok(contents.to_owned());
}
let mut result = String::with_capacity(contents.len());
let mut matched = HashSet::<String>::new();
let mut offset = 0usize;
let mut cursor = 0usize;
let mut active_start = None::<usize>;
let mut payload = String::new();
for segment in contents.split_inclusive('\n') {
let line = segment.trim_end_matches(['\n', '\r']);
let trimmed = line.trim();
if let Some(start) = active_start {
if trimmed == CLOSING_FENCE {
let end = offset + segment.len();
let block_id = parsed_block_id(&payload)?;
if let Some(block_id) = block_id {
if let Some(action) = actions.get(&block_id) {
result.push_str(&contents[cursor..start]);
if let Some(replacement) = action {
result.push_str(replacement);
}
matched.insert(block_id);
cursor = end;
}
}
active_start = None;
payload.clear();
offset = end;
continue;
}
if !payload.is_empty() {
payload.push('\n');
}
payload.push_str(line);
offset += segment.len();
continue;
}
if trimmed.starts_with(OPENING_FENCE) {
active_start = Some(offset);
}
offset += segment.len();
}
if active_start.is_some() {
bail!("unterminated `ccd-memory` block encountered while rewriting memory file")
}
result.push_str(&contents[cursor..]);
let missing = actions
.keys()
.filter(|id| !matched.contains(*id))
.cloned()
.collect::<Vec<_>>();
if !missing.is_empty() {
bail!(
"failed to rewrite expected structured memory entries: {}",
missing.join(", ")
);
}
Ok(result)
}
fn toml_string(value: &str) -> String {
Value::String(value.to_owned()).to_string()
}
fn toml_string_array(values: &[String]) -> String {
Value::Array(
values
.iter()
.cloned()
.map(Value::String)
.collect::<Vec<_>>(),
)
.to_string()
}
#[cfg(test)]
mod tests {
use super::{remove_entry_blocks, render_entry_block};
use crate::memory::entries::StructuredMemoryEntry;
#[test]
fn renders_lifecycle_metadata_in_structured_order() {
let entry = StructuredMemoryEntry {
id: "mem_rule".to_owned(),
entry_type: "rule".to_owned(),
state: "superseded".to_owned(),
created_at: "2026-03-10T10:00:00Z".to_owned(),
last_touched_session: 12,
origin: "manual".to_owned(),
superseded_at: Some("2026-03-12T09:30:00Z".to_owned()),
decay_class: Some("stable".to_owned()),
expires_at: Some("2026-12-31T23:59:59Z".to_owned()),
tags: vec!["memory".to_owned()],
source_ref: None,
supersedes: Vec::new(),
content: "Prefer deterministic writes.".to_owned(),
};
let rendered = render_entry_block(&entry);
let superseded_at = rendered.find("superseded_at = ").unwrap();
let decay_class = rendered.find("decay_class = ").unwrap();
let expires_at = rendered.find("expires_at = ").unwrap();
assert!(superseded_at < decay_class);
assert!(decay_class < expires_at);
}
#[test]
fn removes_selected_entry_blocks() {
let keep = StructuredMemoryEntry {
id: "mem_keep".to_owned(),
entry_type: "rule".to_owned(),
state: "active".to_owned(),
created_at: "2026-03-10T10:00:00Z".to_owned(),
last_touched_session: 12,
origin: "manual".to_owned(),
superseded_at: None,
decay_class: None,
expires_at: None,
tags: Vec::new(),
source_ref: None,
supersedes: Vec::new(),
content: "Keep me.".to_owned(),
};
let drop = StructuredMemoryEntry {
id: "mem_drop".to_owned(),
entry_type: "rule".to_owned(),
state: "promotion_candidate".to_owned(),
created_at: "2026-03-10T10:00:00Z".to_owned(),
last_touched_session: 12,
origin: "manual".to_owned(),
superseded_at: None,
decay_class: None,
expires_at: Some("2026-03-11T00:00:00Z".to_owned()),
tags: Vec::new(),
source_ref: None,
supersedes: Vec::new(),
content: "Drop me.".to_owned(),
};
let contents = format!(
"{}\n\n{}\n",
render_entry_block(&keep),
render_entry_block(&drop)
);
let rewritten = remove_entry_blocks(&contents, &[drop.id.clone()]).unwrap();
assert!(rewritten.contains("mem_keep"));
assert!(!rewritten.contains("mem_drop"));
}
}