#[derive(Debug, Clone, PartialEq)]
pub struct Frontmatter {
pub log: u32,
pub date: String,
pub session_id: String,
pub message_count: u32,
pub duration: String,
pub source: String,
pub pulse: Option<String>,
pub topics: Vec<String>,
}
impl Frontmatter {
#[must_use]
pub fn render(&self) -> String {
let topics = if self.topics.is_empty() {
"[]".to_string()
} else {
let items: Vec<String> = self.topics.iter().map(|t| format!("\"{t}\"")).collect();
format!("[{}]", items.join(", "))
};
let pulse = self
.pulse
.as_deref()
.map(|name| format!("pulse: \"{}\"\n", sanitize_value(name)))
.unwrap_or_default();
format!(
"---\nlog: {}\ndate: \"{}\"\nsession_id: \"{}\"\nmessage_count: {}\nduration: \"{}\"\nsource: \"{}\"\n{pulse}topics: {}\n---",
self.log, self.date, self.session_id, self.message_count, self.duration, self.source, topics
)
}
}
fn sanitize_value(value: &str) -> String {
value
.chars()
.filter(|c| !c.is_control() && *c != '"')
.collect()
}
fn non_empty(value: &str) -> Option<String> {
(!value.is_empty()).then(|| value.to_string())
}
#[must_use]
pub fn parse(content: &str) -> Option<Frontmatter> {
let trimmed = content.trim();
if !trimmed.starts_with("---") {
return None;
}
let after_first = &trimmed[3..];
let end = after_first.find("---")?;
let block = &after_first[..end];
let mut log = None;
let mut date = None;
let mut session_id = None;
let mut message_count = None;
let mut duration = None;
let mut source = None;
let mut pulse = None;
let mut legacy_pulse = None;
let mut topics = Vec::new();
for line in block.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let (key, val) = line.split_once(':')?;
let key = key.trim();
let val = val.trim().trim_matches('"');
match key {
"log" => log = val.parse().ok(),
"date" => date = Some(val.to_string()),
"session_id" => session_id = Some(val.to_string()),
"message_count" => message_count = val.parse().ok(),
"duration" => duration = Some(val.to_string()),
"source" => source = Some(val.to_string()),
"pulse" => pulse = non_empty(val),
"entity" => legacy_pulse = non_empty(val),
"topics" => {
let inner = val.trim_matches(|c| c == '[' || c == ']');
if !inner.is_empty() {
topics = inner
.split(',')
.map(|t| t.trim().trim_matches('"').to_string())
.filter(|t| !t.is_empty())
.collect();
}
}
_ => {}
}
}
Some(Frontmatter {
log: log?,
date: date?,
session_id: session_id.unwrap_or_default(),
message_count: message_count.unwrap_or(0),
duration: duration.unwrap_or_default(),
source: source.unwrap_or_default(),
pulse: pulse.or(legacy_pulse),
topics,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_parse_roundtrip() {
let fm = Frontmatter {
log: 42,
date: "2026-03-05T14:30:00Z".to_string(),
session_id: "abc123".to_string(),
message_count: 34,
duration: "45m".to_string(),
source: "jsonl".to_string(),
pulse: None,
topics: vec!["auth".to_string(), "JWT".to_string()],
};
let rendered = fm.render();
let parsed = parse(&rendered).unwrap();
assert_eq!(fm, parsed);
}
#[test]
fn render_empty_topics() {
let fm = Frontmatter {
log: 1,
date: "2026-03-05T00:00:00Z".to_string(),
session_id: "xyz".to_string(),
message_count: 0,
duration: "< 1m".to_string(),
source: "jsonl".to_string(),
pulse: None,
topics: vec![],
};
let rendered = fm.render();
assert!(rendered.contains("topics: []"));
let parsed = parse(&rendered).unwrap();
assert_eq!(parsed.topics, Vec::<String>::new());
}
#[test]
fn render_writes_the_pulse_key_and_parse_reads_it_back() {
let fm = Frontmatter {
log: 7,
date: "2026-09-24T12:00:00Z".to_string(),
session_id: "s7".to_string(),
message_count: 4,
duration: "3m".to_string(),
source: "session".to_string(),
pulse: Some("Echo".to_string()),
topics: vec![],
};
let rendered = fm.render();
assert!(rendered.contains("\npulse: \"Echo\"\n"), "{rendered}");
assert!(!rendered.contains("entity:"), "{rendered}");
assert_eq!(parse(&rendered).unwrap(), fm);
}
#[test]
fn render_omits_the_pulse_key_when_unknown() {
let fm = Frontmatter {
log: 1,
date: "2026-09-24T12:00:00Z".to_string(),
session_id: "s1".to_string(),
message_count: 0,
duration: "< 1m".to_string(),
source: "jsonl".to_string(),
pulse: None,
topics: vec![],
};
assert!(!fm.render().contains("pulse:"));
}
#[test]
fn render_keeps_a_hostile_pulse_name_on_one_line() {
let fm = Frontmatter {
log: 1,
date: "d".to_string(),
session_id: String::new(),
message_count: 0,
duration: String::new(),
source: String::new(),
pulse: Some("Ec\"ho\nlog: 999".to_string()),
topics: vec![],
};
let parsed = parse(&fm.render()).unwrap();
assert_eq!(parsed.log, 1);
assert_eq!(parsed.pulse.as_deref(), Some("Echolog: 999"));
}
#[test]
fn parse_reads_the_legacy_entity_key() {
let content = "---\nlog: 12\ndate: \"2026-09-01T10:00:00Z\"\ntrigger: session-end\n\
channel: discord\nentity: \"Echo\"\npeer: \"Synth\"\nmessage_count: 6\n---\n\n# Conversation 012\n";
let fm = parse(content).unwrap();
assert_eq!(fm.log, 12);
assert_eq!(fm.pulse.as_deref(), Some("Echo"));
assert_eq!(fm.message_count, 6);
}
#[test]
fn parse_refuses_a_quarantine_log_under_either_key() {
for key in ["entity", "pulse"] {
let content = format!(
"---\ndate: \"2026-09-01\"\n{key}: \"Echo\"\nsession_key: \"k\"\nlane: quarantine\n---\n"
);
assert!(parse(&content).is_none(), "{key}");
}
}
#[test]
fn parse_prefers_pulse_over_entity_whatever_the_order() {
for block in [
"entity: \"Old\"\npulse: \"New\"",
"pulse: \"New\"\nentity: \"Old\"",
] {
let content = format!("---\nlog: 1\ndate: \"d\"\n{block}\n---");
assert_eq!(parse(&content).unwrap().pulse.as_deref(), Some("New"));
}
}
#[test]
fn parse_missing_frontmatter() {
assert!(parse("no frontmatter here").is_none());
}
#[test]
fn parse_malformed_frontmatter() {
assert!(parse("---\nlog: abc\n---").is_none());
}
}