From a77609c025395198747692152e04373d7fdcfaec Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Fri, 6 Mar 2026 15:08:02 -0500 Subject: [PATCH] journal-tail: add --level= for digest hierarchy Support viewing daily, weekly, and monthly digests through the same journal-tail interface: poc-memory journal-tail --level=daily 3 poc-memory journal-tail --level=weekly --full poc-memory journal-tail --level=2 1 Levels: 0/journal (default), 1/daily, 2/weekly, 3/monthly. Accepts both names and integer indices. Refactored title extraction into shared extract_title() and split the journal vs digest display paths for clarity. --- src/main.rs | 109 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 36 deletions(-) diff --git a/src/main.rs b/src/main.rs index 044969d..1f45309 100644 --- a/src/main.rs +++ b/src/main.rs @@ -195,7 +195,9 @@ Commands: import FILE [FILE...] Import markdown file(s) into the store export [FILE|--all] Export store nodes to markdown file(s) journal-write TEXT Write a journal entry to the store - journal-tail [N] [--full] Show last N journal entries (default 20, --full for content) + journal-tail [N] [--level=L] [--full] + Show last N entries (default 20, --full for content) + --level: 0/journal, 1/daily, 2/weekly, 3/monthly query 'EXPR | stages' Query the memory graph Stages: sort F [asc], limit N, select F,F, count Ex: \"degree > 15 | sort degree | limit 10\" @@ -1696,9 +1698,19 @@ fn cmd_journal_write(args: &[String]) -> Result<(), String> { fn cmd_journal_tail(args: &[String]) -> Result<(), String> { let mut n: usize = 20; let mut full = false; + let mut level: u8 = 0; // 0=journal, 1=daily, 2=weekly, 3=monthly + for arg in args { if arg == "--full" || arg == "-f" { full = true; + } else if let Some(val) = arg.strip_prefix("--level=") { + level = match val { + "0" | "journal" => 0, + "1" | "daily" => 1, + "2" | "weekly" => 2, + "3" | "monthly" => 3, + _ => return Err(format!("unknown level '{}': use 0-3 or journal/daily/weekly/monthly", val)), + }; } else if let Ok(num) = arg.parse::() { n = num; } @@ -1706,8 +1718,20 @@ fn cmd_journal_tail(args: &[String]) -> Result<(), String> { let store = store::Store::load()?; - // Collect journal nodes (EpisodicSession), sorted by created_at. - // Legacy nodes (created_at == 0) fall back to key/content parsing. + if level == 0 { + // Original journal-tail behavior + journal_tail_entries(&store, n, full) + } else { + let prefix = match level { + 1 => "daily-", + 2 => "weekly-", + _ => "monthly-", + }; + journal_tail_digests(&store, prefix, n, full) + } +} + +fn journal_tail_entries(store: &store::Store, n: usize, full: bool) -> Result<(), String> { let date_re = regex::Regex::new(r"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})").unwrap(); let key_date_re = regex::Regex::new(r"j-(\d{4}-\d{2}-\d{2}[t-]\d{2}-\d{2})").unwrap(); @@ -1720,13 +1744,10 @@ fn cmd_journal_tail(args: &[String]) -> Result<(), String> { } }; - // Returns (sort_key, display_string) for a journal node. - // Prefer created_at (stable, rename-safe); fall back to key/content. let extract_sort = |node: &store::Node| -> (i64, String) { if node.created_at > 0 { return (node.created_at, store::format_datetime(node.created_at)); } - // Legacy: parse from key or content if let Some(caps) = key_date_re.captures(&node.key) { return (0, normalize_date(&caps[1])); } @@ -1749,48 +1770,64 @@ fn cmd_journal_tail(args: &[String]) -> Result<(), String> { } }); - // Show last N — each entry: [timestamp] ## Title let skip = if journal.len() > n { journal.len() - n } else { 0 }; for node in journal.iter().skip(skip) { let (_, ts) = extract_sort(node); - // Find a meaningful title: first ## header, or first non-date non-empty line - let mut title = String::new(); - for line in node.content.lines() { - let stripped = line.trim(); - if stripped.is_empty() { continue; } - // Skip date-only lines like "## 2026-03-01T01:22" - if date_re.is_match(stripped) && stripped.len() < 25 { continue; } - if stripped.starts_with("## ") { - title = stripped[3..].to_string(); - break; - } else if stripped.starts_with("# ") { - title = stripped[2..].to_string(); - break; - } else { - // Use first content line, truncated - title = if stripped.len() > 70 { - let mut end = 67; - while !stripped.is_char_boundary(end) { end -= 1; } - format!("{}...", &stripped[..end]) - } else { - stripped.to_string() - }; - break; - } - } - if title.is_empty() { - title = node.key.clone(); - } + let title = extract_title(&node.content); if full { println!("--- [{}] {} ---\n{}\n", ts, title, node.content); } else { println!("[{}] {}", ts, title); } } - Ok(()) } +fn journal_tail_digests(store: &store::Store, prefix: &str, n: usize, full: bool) -> Result<(), String> { + let mut digests: Vec<_> = store.nodes.values() + .filter(|node| node.key.starts_with(prefix)) + .collect(); + // Sort by key — the date/week label sorts lexicographically + digests.sort_by(|a, b| a.key.cmp(&b.key)); + + let skip = if digests.len() > n { digests.len() - n } else { 0 }; + for node in digests.iter().skip(skip) { + let label = node.key.strip_prefix(prefix) + .and_then(|s| s.strip_suffix(".md")) + .unwrap_or(&node.key); + let title = extract_title(&node.content); + if full { + println!("--- [{}] {} ---\n{}\n", label, title, node.content); + } else { + println!("[{}] {}", label, title); + } + } + Ok(()) +} + +fn extract_title(content: &str) -> String { + let date_re = regex::Regex::new(r"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})").unwrap(); + for line in content.lines() { + let stripped = line.trim(); + if stripped.is_empty() { continue; } + if date_re.is_match(stripped) && stripped.len() < 25 { continue; } + if stripped.starts_with("## ") { + return stripped[3..].to_string(); + } else if stripped.starts_with("# ") { + return stripped[2..].to_string(); + } else { + return if stripped.len() > 70 { + let mut end = 67; + while !stripped.is_char_boundary(end) { end -= 1; } + format!("{}...", &stripped[..end]) + } else { + stripped.to_string() + }; + } + } + String::from("(untitled)") +} + fn cmd_interference(args: &[String]) -> Result<(), String> { let mut threshold = 0.4f32; let mut i = 0;